burneh
Product

Every customer in one codebase. None in each other's data.

Most teams bolt multi-tenancy on after a big customer asks the hard security question. We put it in the first commit and let Postgres enforce it on every query, so a bad line of code can't leak a thing.

Acme Labs
workspace_id · acme
18 / 25 seats
$4,200
Globex
workspace_id · globex
6 / 10 seats
$890
Initech
workspace_id · initech
3 / 5 seats
$150
three tenants · one database · a wall between them nothing crosses
01

Switch the tenant. The wall doesn't move.

Sign in as any of the three. The screen re-scopes to that workspace, and the same query pointed at a neighbour comes back empty. Not hidden in the UI, refused by the database.

signed in as
Members
4
Seats
18 / 25
MRR
$4,200
DODana Okafor
Owner2h ago
PNPriya Nair
Adminjust now
MRMarco Rossi
Memberyesterday
YTYuki Tanaka
Member3d ago
read members where workspace = acme → 4 rows
read members where workspace = globex → 0 rows, blocked by RLS
02

Three lines of Postgres, not a promise in the app.

A forgotten where clause shouldn't be able to leak a customer. So isolation isn't app logic you have to remember. It's a policy the database enforces on every read, whatever the code does.

policy.sql
1-- members belong to a workspace, and row-level security is on
2alter table members enable row level security;
3
4-- a request can only ever read rows from its own workspace
5create policy tenant_isolation on members
6 using (workspace_id = current_setting('app.workspace')::uuid);
The whole rule is one line

The using clause is the wall. A row is only visible when its workspace matches the one bound to this request.

Code can't forget it

Even a query with no filter at all still only returns the current workspace. The database is the last word, not the ORM.

03

It picks a side before it reads a byte.

No middleware anyone can forget to add. The workspace is resolved and bound once, at the top, and everything downstream inherits it. Watch the token thread through.

01Request

A call comes in for a workspace.

POST app.acme.com/api/members
02Resolve

The workspace is read from the host or the token.

workspace = acme
03Bind

One statement scopes the whole connection.

set local app.workspace = 'acme'
04Query

Postgres returns that workspace, nothing else.

select * from members → 4 rows

Built multi-tenant from the first commit.

Workspaces, roles, row-level isolation, and per-tenant metering, in from the start, not retrofitted the week a big customer asks the hard security question.