Home / Work / Multi-tenant portals Play the video / 2:55

The portal that proves its own walls

A licensing-portal starter on Next.js, TypeScript and Supabase where one organization can never read another's records, and the tests prove it against the database.

The problem

A portal that serves several client organizations from one database has one failure that matters more than any other: one client seeing another client's records.

Checking that in the browser isn't enough. The wall has to live in the database, and there has to be a test that fails the moment it comes down.

What I built

Every table has row-level security keyed to a membership of user, organization and role. With sample data, one organization's admin reads its 12 licenses; asking for the other organization's 9 by id returns 0 rows, and updating them changes 0 rows.

Admins sign in with a password and an authenticator code, and every admin policy in the database checks that the session passed the second factor, so an admin token without it reads 0 licenses even through the API. Managed and self-service roles are scoped and tested separately.

An Excel importer maps the workbook's columns, validates every row before anything is written, and imports only the valid ones. A renewal job opens a case for each license expiring within 30, 60 or 90 days and writes one reminder per window, with a unique key so a retry writes nothing.

What happened

24 of 24 tests pass with real user sessions against the database, 18 of them on isolation. With row-level security switched off on licenses, 12 of the 18 isolation tests fail.

The sample workbook's 14 rows import as 10, with 4 refused and each reason shown. The renewal job writes 11 reminders on its first run and 0 on the second.

The numbers
What this proves

Tenant isolation enforced in the database and proven by tests that fail when it's removed.

Built with

Same problem?Send it over.

Email Eugene

Questions on this one? Reply in the thread and I'll walk you through it live.

More work

All work
Eugene Leychenko / usethefutureWant the walkthrough? Email Eugene.