Real multi-restaurant: how we model groups and franchises
One of the first things we noticed in the interviews was that many operations have more than one location. We are not just talking about big chains; we are talking about a family group with two grills and a beer hall, or a small franchise with three sites in the same city. If the software forces you to create a separate account for each one and re-enter the data every time, hating it comes free.
So in October we spent a couple of weeks on a topic that sounds internal and dull but defines the ceiling of the product: how we model the data.
Workspace, restaurant, members
The core of the model revolves around three entities:
- Workspace. The container for an organization. A hospitality group, a franchise, or simply an independent restaurant that stays with a single workspace.
- Restaurant. Each physical location. A workspace can have one or many.
- Members. People with access. A manager can have visibility into all three restaurants of the group; a server, only their own.
The trick is that the relationship is real: if you add a guest at location A and they later visit location B in the same group, the team at B knows they are a regular of the house. The customer record lives at the workspace level, not the restaurant level. That opens the door to cross-location loyalty, which we will get to later.
RLS: the safety net
This is where Postgres Row Level Security comes in. Instead of filtering "WHERE workspace_id = X" in every backend query (and praying nobody on the team forgets), the database policies themselves prevent a user from reading data that is not theirs. Even if the application code has a bug, the database returns nothing.
This was costly to set up well. We did:
- One policy per table, restrictive by default.
- An audit script (
npm run audit:cross-restaurant) that walks the tables and verifies none are missing policies. - Explicit migrations with manual tests: create two workspaces, try to read each other's data, confirm zero rows come back.
The cost of this decision
It is not free. Multi-tenant from day one means:
- Every query carries a workspace_id. Forgetting one is a security bug, not a functional bug.
- Database indexes have to be designed with this extra filter in mind.
- Switching restaurants in the interface has to be instant and must not lose state.
But in exchange, we avoid what we have watched competitors go through: starting single-tenant and, when the first three-location customer shows up, rewriting half the product.
What it looks like to the user
All this complexity ends up as a tiny detail in the interface: a restaurant switcher in the top-left. You click, you switch locations, and everything you see adjusts. The hard part stays hidden, which is exactly how it should be.