Skip to content
diviteb

Software · 2026-03-04 · 10 min

Row-level security: the multi-tenant pattern we ship every time.

Tenant isolation in Postgres RLS, not application code. The trade-offs, the gotchas, and the audit story it lets you tell. Plus the migration we run when we inherit a multi-tenant codebase that puts isolation in the app layer.

ET

Engineering team

Software practice

Where to enforce isolation

There are two places to enforce tenant isolation in a multi-tenant database app: in your application code (every query manually scoped to the tenant) or in the database (RLS policies that the database enforces regardless of what the app sends).

Application-layer isolation works until it doesn't. The first ORM upgrade, the first new engineer, the first 'just this once' debug query — and now you have a cross-tenant data leak in your audit log.

RLS enforces the rule at the database. The application can't get it wrong, even when it tries.

The pattern we ship

Every tenanted table has a tenant_id column NOT NULL. Every connection sets a current_tenant() session variable on connect. Every table has an RLS policy that filters rows by current_tenant() = tenant_id.

  • Connection middleware sets current_tenant() on every connection — there's no other way to get a connection.
  • Policies are CREATE POLICY, not table grants — auditors see them in pg_policies.
  • We test the RLS policies the same way we test code — a deny-by-default test for every table.
  • Admin queries use a separate database role that bypasses RLS — gated by IAM, audit-logged.

What it costs you

Mostly: query plan complexity. RLS adds a predicate to every query the planner sees, which can confuse it. Index tenant_id first on every tenanted table. On tables with hundreds of millions of rows, compare EXPLAIN plans with and without the policy before you ship.

The other cost: developers will sometimes forget the tenant context in scripts and one-off queries. RLS catches them — they get an empty result set instead of a leaked one — but they'll be confused for a minute.

Migrating off application-layer isolation

Inherited multi-tenant codebases often enforce tenancy in the application. The migration to RLS is straightforward but not free.

The steps: add tenant_id to every tenanted table (if missing); backfill tenant_id from the application's current understanding; add the RLS policy in audit-only mode (logs violations, doesn't block) for two weeks; review the violations log; flip the policy to enforce. Plan for the audit-only window plus the backfill — the policy itself is the short part.

Run this in your team

Talk through it on a call.

A 30-minute discovery call. We'll walk you through how this would apply to your stack.