
August 6, 2026
Most Rails applications that implement multi-tenancy eventually face the same question:
Where should the PostgreSQL tenant context be established?
Many implementations set the tenant at the controller or middleware level. While that identifies the current tenant, it doesn’t necessarily guarantee that every database connection carries the correct PostgreSQL session state.
A cleaner approach is to hook into ActiveRecord’s connection lifecycle.
Understanding the Connection Pool
ActiveRecord does not open a new database connection for every query.
Instead, it maintains a connection pool:
Connection Pool
+-----------------------------+
| Connection #1 (idle) |
| Connection #2 (busy) |
| Connection #3 (idle) |
+-----------------------------+
checkout
│
▼
Rails borrows a connection
... executes SQL ...
checkin
│
▼
Connection returns to the pool
Every request, background job, or console command borrows a connection (checkout), performs its work, and returns it (checkin).
This lifecycle is the ideal place to configure PostgreSQL session variables.
Waiting Until ActiveRecord Loads
The implementation typically starts inside an initializer:
ActiveSupport.on_load(:active_record) do # ...end
This hook delays execution until ActiveRecord has finished loading.
Without it, referencing internal ActiveRecord classes during boot may fail because they have not yet been defined.
Rails provides similar hooks for other frameworks:
- :active_record
- :action_controller
- :action_mailer
- :action_view
- :active_job
- :active_storage
These are initialization hooks—not callbacks that execute on every request.
Extending the Connection Pool
Inside the hook, we can extend ConnectionPool using Module#prepend:
ActiveSupport.on_load(:active_record) do ActiveRecord::ConnectionAdapters::ConnectionPool.prepend( Module.new do def checkout(*args) conn = super if Current.tenant conn.execute("SET app.tenant_id = '#{Current.tenant.id}'") end conn end end )end
At first glance this looks like a monkey patch.
Technically, it is—but it is one of the safest ways to extend framework behavior.
Unlike reopening the class and replacing methods directly, prepend inserts a module before the original implementation in Ruby’s method lookup chain.
Method lookupOur module │ ▼ConnectionPool │ ▼Superclass
Calling super invokes the original implementation.
This allows us to augment the behavior instead of replacing it.
What Exactly Is checkout?
checkout is the method responsible for lending a database connection from the pool.
Conceptually:
Application │ ▼checkout() │ ▼Database connection
Normally, Rails performs this internally whenever ActiveRecord needs to execute SQL.
By intercepting this method, we gain a reliable point where every borrowed connection can be configured before any query runs.
Why Set the Tenant Here?
Suppose the current request belongs to tenant Acme.
When Rails borrows a connection:
SET app.tenant_id = 'acme-uuid';
Now every subsequent query executed through that connection automatically has access to the tenant identifier.
PostgreSQL Row-Level Security policies can then use:
current_setting('app.tenant_id')
to enforce tenant isolation.
The application no longer needs to remember:
where(tenant_id: Current.tenant.id)
The database guarantees isolation.
Why Not Do This in a Controller?
Controllers only represent HTTP requests.
Database connections are also used by:
- Sidekiq workers
- Rails console
- Active Job
- Rake tasks
- Action Mailer
- Background processing
The connection pool is the common denominator for all of them.
Configuring the tenant when a connection is borrowed ensures consistent behavior everywhere ActiveRecord is used.
Dont Forget checkin
One important consideration is connection reuse.
Connections return to the pool after use.
Some implementations also override checkin:
def checkin(conn) conn.execute("RESET app.tenant_id") superend
Resetting session variables before returning a connection leaves it in a clean state for the next borrower.
Whether this is strictly necessary depends on the implementation. If every checkout always executes SET app.tenant_id, stale values will be overwritten. Nevertheless, resetting the session on checkin is a good defensive practice.
Final Thoughts
The interesting part of this pattern is not the initializer itself.
The initializer is simply where the extension is registered.
The real technique is understanding Rails’ initialization hooks, the ActiveRecord connection lifecycle, and using Module#prepend to inject behavior at exactly the point where database connections are acquired.
This small extension allows PostgreSQL Row-Level Security to become almost invisible to the rest of the application, keeping multi-tenancy enforcement inside the database where it belongs.