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