Skip to main content

PostgreSQL RLS for multi-tenant SaaS security

Secure tenant data with PostgreSQL row-level security, restricted roles, transaction-scoped tenant context, safe policies and isolation tests.

In this guide

What is PostgreSQL row-level security for SaaS?

PostgreSQL row-level security (RLS) applies policies to the rows a database role may read or change. A multi-tenant SaaS can use it as a database-side guardrail so a missed tenant filter in an ordinary query does not automatically expose every customer's rows. RLS supplements application authorization and table privileges; it does not replace either one. When RLS is enabled and no applicable policy allows a row, PostgreSQL uses default deny for normal row access.

Put a stable tenant key on every tenant-owned row

Choose an immutable tenant identifier and carry it through parent and child records, files, jobs and audit events. Make the key non-null where every row must belong to a tenant, and add foreign keys or other integrity rules so a child record cannot silently point to another customer's parent. Identify global reference data that is intentionally shared.

Write policies for each operation the application performs

A SELECT policy controls visible rows, while INSERT, UPDATE and DELETE need policies that cover the relevant write paths. PostgreSQL supports USING and WITH CHECK expressions for different parts of those decisions. Test create, read, update, delete, upsert, bulk operations and background jobs instead of assuming one policy handles every command correctly.

Sources for this point: Row Security Policies

Treat a missing tenant context as a denied request

Set the active tenant from a server-validated session or job record after authorization. Never accept an arbitrary tenant identifier supplied only by a URL, form or SQL parameter. Define what the policy does when context is absent or malformed and verify it returns no tenant rows rather than falling back to a shared or default tenant.

Tenant-aware table and policy worksheet
Table or data pathTenant key and integrity ruleAllowed database roleRead/write policy testsOwner and review date
Customer records
Child records and attachments
Background job data

How should a SaaS team configure PostgreSQL RLS safely?

Run application queries as a restricted role, not the table owner

PostgreSQL table owners normally bypass RLS, as do superusers and roles with BYPASSRLS. Use a separate migration owner and a limited application role that cannot change policies or assume a privileged role. Consider FORCE ROW LEVEL SECURITY for owner behavior where appropriate, while remembering it does not constrain superusers or BYPASSRLS roles.

Sources for this point: Row Security Policies

Set tenant context inside the same database transaction

For connection-pooled applications, assign context with a transaction-local setting or an equivalent verified mechanism, then run the tenant queries before the transaction ends. Session state can survive when a pooled connection is reused. Test rollback, exception, retry and connection reuse paths so the next request cannot inherit the previous tenant's context.

Keep application authorization and database grants explicit

RLS is evaluated only after ordinary SQL privileges permit an operation. Grant the application role access only to the tables and columns it needs, keep administrative and reporting roles separate, and review SECURITY DEFINER functions carefully. A privileged helper, view or connection path can reintroduce cross-tenant access if its behavior is not designed and tested deliberately.

How do you test and operate tenant row policies?

Test with two tenants and the real production application role

Create fixtures for tenants A and B, then verify that a caller authorized for A cannot read or alter B's rows through every supported route. Run tests as the same role used by the application, because a database owner or superuser test can bypass the policy and create false confidence. Include negative tests for missing context.

Review integrity side channels and non-row operations

PostgreSQL documents that referential-integrity checks bypass row-security filtering to preserve database integrity. Unique constraints, foreign keys, TRUNCATE, REFERENCES, exports and administrative queries therefore need separate review. Avoid error messages that reveal another tenant's identifiers or whether a protected row exists.

Sources for this point: Row Security Policies

Make migrations, backups and support work tenant-aware

Use privileged paths only for a documented purpose and restrict who can invoke them. Confirm backups and restore jobs include all intended tenant data; PostgreSQL offers a row_security setting that can make a backup fail if policy filtering would omit rows. Test restore and migration scripts against isolated environments and record any controlled RLS bypass.

PostgreSQL RLS for SaaS FAQs

Does row-level security replace application authorization?

No. The application must still authenticate the user, decide which tenant they may act for and authorize the requested action. RLS adds a database boundary that can limit accidental row exposure when a query omits a tenant condition.

Is enabling RLS on one table enough to isolate a tenant?

No. Map every tenant-owned table and indirect data path, then check policies, grants, views, functions, exports, jobs and shared storage. A table without RLS or a privileged code path can defeat an otherwise correct policy design.

Can the PostgreSQL table owner bypass RLS?

Normally, yes. Superusers and roles with BYPASSRLS also bypass policies. Use restricted runtime roles and verify the effective role in tests; FORCE ROW LEVEL SECURITY changes ordinary table-owner behavior but does not remove superuser or BYPASSRLS privileges.

Sources for this point: Row Security Policies

What is the safest source for the tenant ID in a query?

A server-side identity and authorization decision. Derive the tenant from a verified session or trusted job payload, set it for the active transaction, and test that missing or forged tenant context fails closed.