Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 096 / 100

Rails Current.user patterns for Ruby on Rails developers

Access the signed-in user safely.

Expona Admin6 min read

Rails Current.user patterns for Ruby on Rails developers

Accessing the currently signed-in user in Rails happens constantly—from controller actions to model callbacks to background jobs. The pattern you use to surface that user across your codebase will either keep your code clean and testable, or introduce subtle bugs and hidden dependencies that compound over months. Rails Current.user patterns for Ruby on Rails developers offer a structured approach that many production teams have settled on after learning the hard way.

Why Current.user matters

In a typical Rails application, you authenticate a user in your session or via a token, then need to reference that user throughout your request lifecycle. A naive approach passes current_user as a method argument through layers of your codebase. A more elegant approach centralizes it in a thread-local or request-scoped context that any component can safely access.

The difference becomes obvious when you're in a deeply nested service, model callback, or background job and need to know who initiated the action. Threads passing arguments become verbose. Thread-local context becomes a hidden dependency. We'll explore how to thread the needle between those extremes.

Set Current.user in a before_action

The foundation of this pattern is to populate a request-scoped Current object in a controller callback, early in the request lifecycle. Rails provides ActiveSupport::CurrentAttributes for exactly this purpose.

Start by creating a Current class that inherits from ActiveSupport::CurrentAttributes:

ruby
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
  attribute :account
  attribute :request_id
end

Then, in your ApplicationController, set Current.user in a before_action that runs after authentication:

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
    Current.account = current_user&.account
    Current.request_id = request.id
  end
end

This ensures that by the time any controller action, model, service, or job runs within that request, Current.user is already populated and accessible from anywhere.

Use in models only when necessary

Now that Current.user is available, the temptation is to reference it everywhere in your models. Resist that. Models should remain as stateless and testable as possible. Use Current.user in models sparingly and only when the alternative is significantly more awkward.

A good use case is an audit trail or timestamp callback that needs to record who made a change:

ruby
# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :audit_logs

  before_save :record_editor

  private

  def record_editor
    return unless user_id_changed?
    audit_logs.build(
      action: 'edited',
      editor_id: Current.user.id,
      previous_value: user_id_was
    )
  end
end

A poor use case is reading Current.user just because it's convenient, when the caller should pass the user explicitly:

ruby
# Don't do this
class Post < ApplicationRecord
  def viewable?
    Current.user.role == 'admin' || user_id == Current.user.id
  end
end

# Do this instead
class Post < ApplicationRecord
  def viewable_by?(user)
    user.role == 'admin' || user_id == user.id
  end
end

When methods accept a user parameter, your code remains testable in isolation. You can pass any user without setting up Current.user. You can also unit test the model without any controller context.

Avoid hidden dependencies

A hidden dependency is when a method silently relies on Current.user being set, but that fact isn't obvious from the method signature. Teammates eventually call that method from an unexpected context—a background job, a rake task, an API endpoint with no user—and it breaks.

Make dependencies explicit. If a method needs the current user, accept it as a parameter:

ruby
# Hidden dependency—fragile
class NotificationService
  def send_digest
    digest = Digest.build_for(Current.user)
    Notifier.send_email(digest)
  end
end

# Explicit dependency—robust
class NotificationService
  def send_digest(user)
    digest = Digest.build_for(user)
    Notifier.send_email(digest)
  end
end

# In controller
def send_user_digest
  NotificationService.new.send_digest(current_user)
  redirect_to root_path, notice: 'Digest sent'
end

# In background job
class SendDigestJob
  def perform(user_id)
    user = User.find(user_id)
    NotificationService.new.send_digest(user)
  end
end

This approach scales. Your service works in the controller, the job, a rake task, or an internal tool. You're not trapped by implicit context.

Reset per request

ActiveSupport::CurrentAttributes automatically resets between requests in normal Rails operation. However, in certain scenarios—particularly in tests, background jobs, and multi-threaded contexts—you must be intentional about cleanup.

In tests, if you don't reset Current between examples, one test's Current.user can leak into another:

ruby
# spec/rails_helper.rb
RSpec.configure do |config|
  config.after(:each) do
    Current.reset
  end
end

In background jobs, if you manually set Current.user, reset it afterward:

ruby
# app/jobs/process_invoice_job.rb
class ProcessInvoiceJob
  include Sidekiq::Job

  def perform(invoice_id, user_id)
    user = User.find(user_id)
    Current.user = user

    begin
      Invoice.find(invoice_id).process!
    ensure
      Current.reset
    end
  end
end

If you're running code in a thread pool or concurrent context, each thread gets its own instance of CurrentAttributes. Be explicit about what you pass:

ruby
# Don't assume Current.user crosses threads
Thread.new do
  # Current.user is nil here, even if it was set in the parent thread
  puts Current.user  # nil
end

# Pass what you need
user = Current.user
Thread.new do
  puts user  # the user object, passed explicitly
end

Common mistakes

Setting Current in the wrong place. If you set Current.user after a redirect or in a middleware that runs too late, some actions won't see it. Place it in a before_action in ApplicationController or in a Rack middleware that runs early.

Forgetting to reset in tests. Test data pollution is silent and insidious. Always reset Current between examples, or use a test helper that does it automatically.

Over-relying on Current in models. It's tempting to reach for Current.user instead of passing the user as a parameter. This makes models hard to test and creates invisible contracts between the model and the request context.

Not handling nil. If authentication fails or a request runs without a user (webhook, public endpoint), Current.user will be nil. Always guard against it:

ruby
def set_current_user
  Current.user = current_user  # can be nil for public routes
end

# In a model or service
def editor_name
  Current.user&.name || 'System'
end

Assuming thread safety without understanding it. CurrentAttributes uses thread-local storage, so it's safe across request threads. It's not safe across background job threads or explicitly spawned threads unless you're careful. When in doubt, pass the data explicitly.

Key takeaways

  • Use ActiveSupport::CurrentAttributes to create a Current class and populate Current.user in a before_action early in the request lifecycle.
  • Keep models stateless by accepting the user as a method parameter when possible, and reach for Current.user only for audit logs or framework hooks where passing it would be awkward.
  • Make dependencies explicit—if a service or class needs a user, accept it as a parameter rather than relying on Current.user being set, so your code works in any context.
  • Always reset Current between test examples and be intentional about cleanup in background jobs and multi-threaded code.
  • Guard against nil—Current.user may be nil on public endpoints or if authentication hasn't run yet.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles