Postgres row-level-security multi-tenancy for Rails and ActiveRecord.
rls_tenant makes every database query tenant aware at the source. You set the
active tenant once per request and Postgres enforces isolation for the rest of
the request. A forgotten where clause cannot leak another tenant's rows,
because the database itself filters them out.
It gives you three pieces that fit together:
- A per-request tenant switcher that pushes the active tenant into the database session.
- A model concern that stamps new rows with the tenant and adds readable tenant scopes.
- A migration helper that enables and FORCEs row level security with a tenant isolation policy.
rls_tenant speaks the standard Postgres RLS mechanism. RLS policies read a
session setting (a GUC) with current_setting. This gem writes that setting at
the start of each tenant scope using set_config(name, value, true), so the
value is local to the transaction and never leaks across pooled connections.
Your migration installs a policy that compares each row's tenant column against
that setting. Because the policy is FORCEd, it applies even to the table
owner, so application code cannot accidentally bypass it.
Add it to your Gemfile:
gem "rls_tenant"Then run:
bundle install# config/initializers/rls_tenant.rb
RlsTenant.configure do |config|
config.tenant_setting = "app.current_tenant" # the Postgres setting policies read
config.tenant_column = "tenant_id" # the column on tenant-scoped tables
endThe defaults shown above work out of the box.
class EnableRlsOnInvoices < ActiveRecord::Migration[7.1]
include RlsTenant::Migration
def up
enable_rls :invoices
end
def down
disable_rls :invoices
end
endenable_rls turns on RLS, FORCEs it, and creates a policy named
invoices_tenant_isolation that matches rows whose tenant_id equals the
current tenant setting, for both reads (USING) and writes (WITH CHECK).
You can override the column, policy name, or setting per table:
enable_rls :bills, column: "org_id", setting: "app.org"class Invoice < ApplicationRecord
include RlsTenant::Model
endNew records are stamped with the active tenant id automatically, and you get two scopes:
Invoice.for_current_tenant # rows for the active tenant
Invoice.for_tenant(account.id) # rows for a specific tenantThe hard guarantee comes from the database policy; these scopes keep your Ruby code expressive.
Wrap each request in with_tenant. In Rails this is typically an
around_action:
class ApplicationController < ActionController::Base
around_action :scope_to_tenant
private
def scope_to_tenant(&block)
RlsTenant.with_tenant(current_account.id, &block)
end
endInside the block, every query and write is scoped to that tenant by Postgres:
RlsTenant.with_tenant(account.id) do
Invoice.create!(amount_cents: 5_000) # stamped + visible only to this tenant
Invoice.count # counts only this tenant's invoices
endwith_tenant nests cleanly. The previous tenant is restored when the block
returns, even if it raises.
This gem is a thin, idiomatic layer over native Postgres row level security:
- Policies read a session GUC via
current_setting(name, true). - The gem sets that GUC with
set_config(name, value, true)(transaction local). - Tables use
ENABLEplusFORCE ROW LEVEL SECURITY.
If you already know Postgres RLS, there is nothing new to learn. If you do not, the migration helper writes the correct policy for you.
bundle install
bundle exec rspecMIT. See LICENSE.