← Back to blog
·6 min·The SwyDex team

When tenant isolation isn't enough — defense in depth

The first thing every multi-tenant platform learns: every database query needs a tenant_id filter. Miss one, and tenant A's API key reads tenant B's data.

That's necessary but not sufficient. Two layers we added on top:

Static analysis test in CI

We have a Vitest file that parses every routes/*.routes.ts and looks for prisma.<model>.findUnique / findFirst / update calls that take an id but don't reference tenantId or req.tenant!.id within a 40-line window. The test fails the build if it finds an unscoped query in a money-moving route. There's an allowlist for legitimate cross-tenant routes (admin surfaces, public market data) that we review every release.

Row-level security as a backstop

For the tables we move money through, Postgres row-level security policies enforce tenant_id = current_setting('app.tenant_id', true)::text on every query. The application sets app.tenant_id at the start of each request via a Prisma middleware. If the application code forgets the filter, RLS rejects the query entirely.

The second layer is annoying when it triggers — you get an opaque error from Postgres rather than a clean app-level message. But that's the point. The static check is for catching honest bugs in code review; RLS is for catching the bugs the static check missed.


More posts