Skip to main content

Real multi-restaurant: how we model groups and franchises

Product & Platform

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.

ESENCA