PostgreSQL 16 RLS for multi-tenant Colombian SaaS: real isolation, not by-convention
In a multi-tenant SaaS, the worst bug is not a crash — it is a data leak between tenants. In Colombia, that is a Law 1581/2012 violation with SIC consequences and immediate client loss. PostgreSQL Row Level Security (RLS) guarantees isolation at the database level, not just in application code.
Why by-convention isolation fails
Adding .filter(tenant_id=current_tenant_id) to every ORM query fails when: a developer forgets it on a new query, middleware doesn't set the correct tenant, or an aggregation query loses the filter across joins. With RLS, isolation happens in the PostgreSQL engine before query execution — transparent regardless of what application code does.
Setup: one app role, session variable per transaction
We use a single app role (trujoapp_role, no superuser). On every connection from SQLAlchemy's pool, we execute SET LOCAL app.current_tenant_id = :tid. SET LOCAL scopes the variable to the current transaction only — auto-cleared on commit/rollback.
Four policies per table
SELECT, INSERT (WITH CHECK), UPDATE (USING + WITH CHECK), DELETE — all using current_setting('app.current_tenant_id', true). The true parameter returns NULL if the variable isn't set, rather than raising an error. NULL matches no UUID: all data blocked if no tenant in context. Safe by default.
FORCE ROW LEVEL SECURITY matters
Without FORCE, the table owner bypasses RLS. With FORCE, even the owner needs an explicit policy. We enable FORCE on all 29 tenant-scoped tables.
The 186 orphan products bug
April 2026 audit found 186 rows in catalog_products with NULL tenant_id — early migration data created before RLS was implemented. With RLS active, they were invisible to all tenants (NULL != any UUID). A silent bug: products in DB but appearing in no catalog. Fix: assign to demo tenant UUID, then ALTER COLUMN tenant_id SET NOT NULL.
Performance
RLS overhead is marginal with an index on tenant_id. Production p95 for order listing queries: under 12ms. EXPLAIN ANALYZE confirms the RLS condition is applied as an Index Scan Condition on the composite (tenant_id, created_at) index — not as a post-result filter. After 29 policies across all tenant-scoped tables: zero cross-tenant leaks in automated tests and manual audit review.