Row-level security without losing your mind
Postgres can enforce multi-tenancy for you. The catch is every connection has to tell the truth about who it is.
30 Jul 2026 · 6 min readPostgresSecurityBackend
Every multi-tenant app starts with the same quiet agreement: every query will remember to filter by tenant. It holds until the day someone writes a report query at 6pm on a Friday and forgets. Row-level security moves that promise from your discipline into the database.
The whole idea in one policy
sql
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid);From that moment a bare SELECT * FROM invoices returns only the current tenant's rows. Not because the query said so, but because the table refuses to hand over anything else.
The part that bites
- The setting has to be set per connection, on every checkout from the pool, or the query errors instead of leaking — which is the failure mode you want.
- The table owner bypasses RLS by default. Your migration user is not the user your app should connect as.
- Policies apply per statement, so a join across two protected tables is filtered twice. That is correct, and occasionally surprising.
Security you have to remember is a bug waiting for a deadline.