Hooking into ActiveRecord’s Connection Pool: A Clean Way to Enable PostgreSQL Row-Level Security

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.


Tokyo Topographic Map
Built for Ruby on Rails

Build Maps Without
Google APIs

Generate beautiful production-ready maps directly from your Rails backend. Fast rendering, zero external dependencies, full control.

✓ No API fees ✓ Self-hosted ✓ Rails Native ✓ Fast Rendering
Why developers switch
Replace expensive map stacks.

Stop relying on third-party map billing and bloated JS libraries. Render static or dynamic maps directly in Ruby.

Try It Now
Tokyo MapView Demo

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 lookup
Our 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")
super
end

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.

Article content

Leave a comment