LinkedIn Carousel: PostgreSQL Row-Level Security
Post type: Technical · 10 slides
Blog post: https://www.nuvikatech.com/blog/posts/postgresql-row-level-security
SLIDE 1 — Cover
Headline: One database. 100+ tenants. No data leaks possible — not because we’re disciplined, but because the database won’t allow it.
Sub-line: How PostgreSQL Row-Level Security eliminates the entire class of cross-tenant data leaks at the engine level.
SLIDE 2
Label: THE NIGHTMARE SCENARIO
Headline: A single application bug lets one customer see another customer’s billing data.
Body: On a platform handling cloud financial data for multiple enterprises, this isn’t just embarrassing — it’s a compliance incident and a contract breach. We could not afford to rely on developer discipline alone.
SLIDE 3
Label: APPROACH 1
Headline: Separate databases per tenant: airtight isolation, nightmare at scale.
Body: One database per customer means a migration runs N times, connection pools explode, and infrastructure costs don’t make sense until you have very large customers. Doesn’t scale.
SLIDE 4
Label: APPROACH 2
Headline: Shared tables with application-layer filtering: simple — and dangerous.
Body:
Add WHERE tenant_id = :current_tenant to every query. The problem: it relies entirely on discipline. One missed filter, one raw SQL query, one developer who didn’t know the convention — and you have a data leak.
SLIDE 5
Label: OUR APPROACH
Headline: PostgreSQL RLS: tenant isolation enforced at the database engine level, not the application level.
Body:
Every table has a policy: USING (tenant_id = current_setting('app.current_tenant_id')::uuid). Set the tenant once per request. After that, the database engine applies the filter to every query automatically.
SLIDE 6
Label: THE GUARANTEE
Headline: Even a query with no WHERE clause returns only the current tenant’s rows.
Body:
A developer writes db.query(CloudSubscription).all() — completely forgetting to filter by tenant. They get back only their tenant’s data. The database enforced the boundary. The bug had no blast radius.
SLIDE 7
Label: THE FASTAPI WIRING
Headline:
One Depends() call sets the tenant session variable for the entire request.
Body:
set_tenant_context runs SET app.current_tenant_id = :tid once per session. Every endpoint that needs tenant isolation depends on it. The tenant is set; RLS does the rest.
SLIDE 8
Label: WHAT IT COSTS: MIGRATIONS
Headline: Migrations require explicit BYPASSRLS. Use it deliberately and sparingly.
Body:
Some DDL operations need to run without the RLS filter. We have a BYPASSRLS flag on our migration runner and a policy about when to use it. Superusers bypass RLS — your application should never connect as superuser.
SLIDE 9
Label: WHAT IT COSTS: TESTING
Headline: Every test that touches the database needs the tenant session variable set.
Body: We have a pytest fixture for this. Forgetting it means queries return zero rows, which is a confusing failure until you understand the system. Low learning curve overhead, high ongoing safety value.
SLIDE 10 — CTA
Headline: Want the full story?
Body: The full RLS implementation: the policy setup, the SQLAlchemy wiring, the migration discipline, and what 35 tables with zero cross-tenant leaks actually looks like in practice.
Link: nuvikatech.com/blog/posts/postgresql-row-level-security