Every Rails application eventually needs to access the current user, tenant, or request metadata deep in the model layer. The typical solution—passing context through every method call—creates verbose, cluttered code. Rails solves this elegantly with CurrentAttributes, a thread-safe store for request-scoped globals.
The Problem with Passing Context
Consider a common scenario: tracking which user created or modified a record. Without CurrentAttributes, the implementation becomes awkward:
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
def self.create_with_author(attributes, author)
create(attributes.merge(author: author))
end
end
# app/services/post_creator.rb
class PostCreator
def initialize(user)
@user = user
end
def call(params)
Post.create_with_author(params, @user)
end
endThis pattern forces every service, job, and model method to accept and pass the current user. The code becomes noisy, and forgetting to pass context causes bugs.
Setting Up CurrentAttributes
Create a Current class that inherits from ActiveSupport::CurrentAttributes:
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :user, :request_id, :tenant
resets { Time.zone = 'UTC' }
def user=(user)
super
Time.zone = user&.time_zone || 'UTC'
end
endThe attribute macro defines thread-local accessors. The resets callback runs when attributes clear between requests, ensuring clean state.
Set the current user in the application controller:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current_attributes
private
def set_current_attributes
Current.user = current_user
Current.request_id = request.request_id
end
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
endNow Current.user is accessible anywhere in the request cycle—models, services, mailers, and jobs.
Automatic Audit Trails
With Current available globally, implementing audit trails becomes trivial:
# app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
belongs_to :created_by, class_name: 'User', optional: true
belongs_to :updated_by, class_name: 'User', optional: true
before_create :set_created_by
before_update :set_updated_by
end
private
def set_created_by
self.created_by ||= Current.user
end
def set_updated_by
self.updated_by = Current.user
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Auditable
validates :title, presence: true
endThe migration to support this:
# db/migrate/20260217000001_add_audit_columns_to_posts.rb
class AddAuditColumnsToPosts < ActiveRecord::Migration[8.0]
def change
add_reference :posts, :created_by, foreign_key: { to_table: :users }
add_reference :posts, :updated_by, foreign_key: { to_table: :users }
end
endEvery post now automatically tracks who created and last modified it, with zero changes to existing code.
Multi-Tenancy Made Simple
Multi-tenant applications benefit enormously from CurrentAttributes. Set the tenant once and scope all queries automatically:
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :user, :tenant
def tenant=(tenant)
super
ActsAsTenant.current_tenant = tenant if tenant
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current_tenant
private
def set_current_tenant
Current.tenant = Tenant.find_by!(subdomain: request.subdomain)
rescue ActiveRecord::RecordNotFound
render plain: 'Tenant not found', status: :not_found
end
endFor applications not using the acts_as_tenant gem, implement scoping with a concern:
# app/models/concerns/tenant_scoped.rb
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
default_scope -> { where(tenant: Current.tenant) if Current.tenant }
before_validation :set_tenant, on: :create
end
private
def set_tenant
self.tenant ||= Current.tenant
end
endBackground Jobs and Current Attributes
A critical gotcha: CurrentAttributes reset between requests and don't automatically propagate to background jobs. Serialize necessary context explicitly:
# app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
attr_accessor :current_user_id, :current_tenant_id
before_perform do |job|
Current.user = User.find_by(id: job.current_user_id)
Current.tenant = Tenant.find_by(id: job.current_tenant_id)
end
def serialize
super.merge(
'current_user_id' => Current.user&.id,
'current_tenant_id' => Current.tenant&.id
)
end
def deserialize(job_data)
super
self.current_user_id = job_data['current_user_id']
self.current_tenant_id = job_data['current_tenant_id']
end
end
# app/jobs/send_notification_job.rb
class SendNotificationJob < ApplicationJob
queue_as :default
def perform(post_id)
post = Post.find(post_id)
# Current.user and Current.tenant are available here
NotificationService.notify(post, triggered_by: Current.user)
end
endNow enqueuing a job captures the current context automatically.
Request Logging Enhancement
Improve log correlation by including request context:
# config/initializers/log_tags.rb
Rails.application.configure do
config.log_tags = [
:request_id,
-> { Current.user&.id ? "user:#{Current.user.id}" : 'guest' },
-> { Current.tenant&.subdomain || 'no-tenant' }
]
endLog output now includes user and tenant context for every line:
[req-abc123] [user:42] [acme] Started GET "/posts" for 127.0.0.1
[req-abc123] [user:42] [acme] Processing by PostsController#indexTesting with Current Attributes
Tests require explicit setup of Current values:
# spec/support/current_attributes.rb
RSpec.configure do |config|
config.before(:each) do
Current.reset
end
end
# spec/models/post_spec.rb
RSpec.describe Post, type: :model do
let(:user) { create(:user) }
let(:tenant) { create(:tenant) }
before do
Current.user = user
Current.tenant = tenant
end
it 'automatically assigns created_by' do
post = Post.create!(title: 'Test')
expect(post.created_by).to eq(user)
end
it 'scopes to current tenant' do
post = Post.create!(title: 'Scoped Post')
expect(post.tenant).to eq(tenant)
end
endCommon Mistakes to Avoid
Storing non-request data: Only store values that genuinely belong to the current request. Configuration or computed values belong elsewhere.
Heavy objects: Store IDs rather than full ActiveRecord objects when possible. This reduces memory usage and avoids stale data issues.
Forgetting job serialization: Background jobs run in separate threads without access to request context. Always serialize and restore necessary attributes.
Overusing default scopes: While convenient, default scopes with Current.tenant can cause issues in console sessions and rake tasks where Current isn't set. Consider using explicit scopes or the unscoped method when needed.
Summary
Rails CurrentAttributes provides a clean, thread-safe mechanism for request-scoped globals. The pattern shines for authentication context, multi-tenancy, audit trails, and request correlation. Key points to remember: set attributes early in the request cycle, explicitly propagate to background jobs, and reset in tests. Used judiciously, CurrentAttributes eliminates parameter drilling while maintaining clean, testable code.