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:
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :user
attribute :account
attribute :request_id
endThen, in your ApplicationController, set Current.user in a before_action that runs after authentication:
# 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
endThis 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:
# 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
endA poor use case is reading Current.user just because it's convenient, when the caller should pass the user explicitly:
# 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
endWhen 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:
# 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
endThis 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:
# spec/rails_helper.rb
RSpec.configure do |config|
config.after(:each) do
Current.reset
end
endIn background jobs, if you manually set Current.user, reset it afterward:
# 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
endIf 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:
# 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
endCommon 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:
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'
endAssuming 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::CurrentAttributesto create aCurrentclass and populateCurrent.userin 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.useronly 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.userbeing set, so your code works in any context. - Always reset
Currentbetween test examples and be intentional about cleanup in background jobs and multi-threaded code. - Guard against nil—
Current.usermay be nil on public endpoints or if authentication hasn't run yet.






Comments
Loading comments…