For developers2 min read
Enforcing tenant isolation in CI with Supabase
Every salon in Stippa lives in the same database. How I make sure nobody can peek into the neighbours' place, and why the check now looks at reads as well.
Every salon in Stippa lives in the same database. A bit like a block of flats: one building, lots of front doors. It's called multitenancy (opens an external site), it's completely normal for this kind of software, and it keeps the app simple and affordable.
It also means the most important work is invisible. Every front door has to be locked. One salon must never see another salon's appointments, customers or payments, let alone change them.
Two locks on the door
The first lock lives in the database. Stippa runs on Supabase, which is PostgreSQL (opens an external site) with row level security (opens an external site): on every query, the database checks which salon the data belongs to. If you don't belong to that salon, you simply get nothing back.
The second lock lives in the code. Since late May, every change runs an automated check across the repositories, the parts of the code that talk to the database. It runs in CI (opens an external site), and a change that doesn't pass it doesn't get in.
Why two locks? A database rule protects you from the mistake you forgot about, and a check in the code forces you to think about it up front. Together they catch more than either does alone.
What the check looks for
The script reads the source of every repository and looks at three things.
- Writes. Every update, delete and upsert has to be filtered by salon. Changing an appointment by its id alone, without checking which salon it belongs to? Not getting through.
- New rows. Adding a row to a table that belongs to a salon means writing that salon along with it.
- Reads. This one was added today. A read that could reach another salon's data through a reference to another table is now flagged too.
Sometimes an exception is legitimate, for instance code that deliberately works across all salons. It then goes on a short list, with the reason written next to each file. That way every exception is a deliberate choice you can find again later.
Why reads matter too
Updates and inserts are the obvious risk: that's where things change. Reads feel harmless. But a customer list that accidentally includes the customers of the salon down the road is just as bad. When the check got this pass, it turned up a few places that needed tightening. They were locked down in the same change.
There's more on how Stippa is built in the case study.