Brazil 10:56 UTC−3·squads working now
NDA first100% your IPLGPD · GDPR
<alkwar.com/en/insights/postgres-row-level-security-multi-tenant

Multi-tenant SaaS with PostgreSQL row-level security

When every customer shares the same tables, one missing WHERE clause can show one company’s data to another. Row-level security moves that rule from the application into the database.

AUTHOR[ PLACEHOLDER · Author ]
PUBLISHED[ Date ]
READING TIME8 min

01Why the database should enforce it

In a shared-schema SaaS, every table has a tenant_id column and every query must filter by it. Application code can do this, but it has to do it every time, in every query, written by every engineer. Row-level security (RLS) lets PostgreSQL apply the filter itself, so a forgotten clause returns nothing instead of returning someone else’s data.

02The minimal setup

Enable RLS on the table and create a policy that compares the row’s tenant with a setting defined for the current transaction.

<schema.sql
01ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
02ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
03
04CREATE POLICY tenant_isolation ON invoices
05 USING (tenant_id = current_setting('app.tenant_id')::uuid)
06 WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

USING filters what a query can read. WITH CHECK blocks inserts and updates that would write a row into another tenant. FORCE applies the policy to the table owner too.

03Setting the tenant per request

The API resolves the tenant from the authenticated user and sets it at the start of each transaction. The third argument of set_config makes the value local to the transaction, which matters when connections are pooled.

<TenantScope.java
01@Transactional
02public <T> T inTenant(UUID tenantId, Supplier<T> work) {
03 jdbc.queryForObject("SELECT set_config('app.tenant_id', ?, true)",
04 String.class, tenantId.toString());
05 return work.get();
06}

04Three mistakes that break isolation

<Connecting as a superuser or a role with BYPASSRLS. Policies are skipped without any error. Use a dedicated application role.
<Setting the tenant at session level with a connection pool. The next request on the same connection inherits the previous tenant.
<Forgetting new tables. Add a migration check that fails when a table with tenant_id has RLS disabled.
RLS is a second line of defence. Keep tenant filters in the application as well, and test isolation in CI with two tenants and the same queries.

05Testing it in CI

<isolation_test.sql
01BEGIN;
02SELECT set_config('app.tenant_id', '00000000-0000-0000-0000-0000000000a1', true);
03SELECT count(*) FROM invoices
04 WHERE tenant_id = '00000000-0000-0000-0000-0000000000b2';
05-- expected: 0
06ROLLBACK;

We run this check for every table with a tenant column on every pull request. It takes seconds and catches the one mistake that would cost a customer.

[ PHOTO ]
ABOUT THE AUTHOR[ PLACEHOLDER · Name, role and one line of background ]
< All articles
WHY TEAMS TRUST US
NDA firstWe sign your NDA before we see a line of your code.
Your IP100% of the code and intellectual property belongs to you.
LGPD & GDPRPersonal data handled under Brazilian and EU law.
Day-one accessRepository and board shared from the first day.
Weekly demosWorking software shown live every week.
A WGB Soluções de Software company · Santa Rita do Sapucaí, Brazil
[ PLACEHOLDER · Clutch badge ][ PLACEHOLDER · Upwork badge ]
CONTACT

Have a project with a deadline?

Send us your requirements. We come back with a scope, a timeline and a fixed price.

WHAT HAPPENS NEXT
Day 0A 30-minute call with a tech lead. We sign your NDA first if you need it.
Day 1A written summary of the call and our open questions.
Days 3–7A proposal with scope, timeline, team and a fixed price.