SaaS · PostgreSQL

Tenant isolation with PostgreSQL Row-Level Security

A practical design for tenant context, database roles and isolation tests in a shared SaaS database.

TOPIC HUBCloud, DevOps & Kubernetes
Separate protected spaces inside a shared structure, illustrating tenant isolation.
An editorial interpretation of the topic, followed by a practical execution diagram.

A shared SaaS database often starts with a tenant_id condition in application queries. The weak point is the next report or background job that forgets that condition. A stronger design combines database enforcement, trusted request context and tests for every route that can expose data. The example below is an architectural starting point, not a production-ready security configuration.

Choose the isolation model first

Shared tables can simplify central operations when tenants use a similar schema. Separate databases offer different backup, isolation and maintenance options, at the cost of more connections and coordinated migrations. Decide from operational requirements and customer needs; tenant count alone is not a sufficient reason to choose either approach.

Understand the enforcement boundary

PostgreSQL Row-Level Security policies control which rows an applicable role may access or change. Once enabled, missing applicable policies produce default denial for roles subject to RLS. Owners, superusers and roles with BYPASSRLS require particular attention. The application should not routinely connect with a privileged migration role.

Set trusted context per transaction

Resolve the tenant from authenticated membership rather than accepting an arbitrary browser identifier. With pooled connections, avoid leaving tenant state attached to a connection for the next request. A transaction-local setting provides a bounded context. The following example assumes UUID tenant identifiers and requires a properly configured application role.

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY invoice_tenant ON invoices
USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid)
WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid);
BEGIN;
SELECT set_config('app.tenant_id', :trusted_tenant_id, true);
SELECT id, total FROM invoices ORDER BY id LIMIT 20;
COMMIT;
Identity and membership are checked before tenant-scoped database access.
Identity and membership are checked before tenant-scoped database access. Open for a larger view

Carry isolation beyond the database

Consider a correctly authorized invoice query followed by a Redis write to invoice:42. If identifiers overlap across tenants, that cache can return another tenant’s response without touching the protected table. Include tenant identity in cache keys, jobs and file paths. Consider composite relational constraints where a record must reference another record belonging to the same tenant.

Test denial using the real role

Create two tenants and exercise reads, inserts, updates and deletes using the application database role. Test missing context, reused connections, exports and scheduled jobs. Give administrative access a separate, explicit path with an audit trail. A passing list-page test says little about a forgotten export endpoint.

An implementation sequence

Inventory data paths, protect one table, verify the actual connection-pool behavior, then expand. Record every privileged exception. RLS can enforce a valuable database boundary, but membership verification, object storage and caches remain part of the application’s responsibility.

Scenario: exporting a tenant’s invoices

Consider an administrator requesting an invoice export and signing out before the worker starts. Do not make execution depend on a browser session that may expire. Persist the tenant and requesting actor, then apply the operation’s authorization policy when it runs. Establish tenant context for database reads and write the output into the correct storage scope. At download time, verify access again rather than assuming that a previously issued link remains appropriate indefinitely.

Pre-release review

Test permission removal between export creation and download, context changes on reused connections, and simultaneous exports for different tenants. Inspect actual records, not just the tenant identifier printed in logs. Include a test that detects any foreign row and another that denies access after permission is withdrawn. Support access should be explicit, bounded and audited instead of disabling the normal isolation mechanism.

Official references

These references document the tools discussed. Examples and design decisions are illustrative and should be adapted to the project and its versions.

Prepared by: Noor Yasser

FROM DECISION TO DELIVERY

Working through a similar engineering challenge?

I help teams turn architecture decisions into a clear scope and dependable, reviewable implementation.

Book a 30-minute callRelated serviceSaaS & digital product developmentRelevant projectMurshid