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.
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.
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.
1-- members belong to a workspace, and row-level security is on2alter table members enable row level security;34-- a request can only ever read rows from its own workspace5create policy tenant_isolation on members6 using (workspace_id = current_setting('app.workspace')::uuid);The using clause is the wall. A row is only visible when its workspace matches the one bound to this request.
Even a query with no filter at all still only returns the current workspace. The database is the last word, not the ORM.
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.
A call comes in for a workspace.
The workspace is read from the host or the token.
One statement scopes the whole connection.
Postgres returns that workspace, nothing else.
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.