Designing a multi-tenant core for a hospitality SaaS
How Basiq isolates every venue on shared infrastructure without drowning in complexity.

Ali Najm
· 1 min read

When you build SaaS for small businesses, the first architectural decision quietly decides your next two years: how do tenants share infrastructure without ever seeing each other's data?
For Basiq — a platform that runs a restaurant's menu, bookings, and accounting — I made that call early. Here is the reasoning.
The three ways to isolate a tenant
- Database per tenant — strongest isolation, painful to migrate and operate at scale.
- Schema per tenant — a middle ground, still heavy once you have hundreds.
- Shared schema, tenant column — one database, a
tenant_idon every row.
For a product aimed at thousands of small venues, option three is the only one that
stays operable. The risk is obvious: one missing where tenant_id = ? and a café in
one city sees another's orders.
Make isolation impossible to forget
The fix is to never rely on a developer remembering. Isolation belongs in one place, enforced automatically:
// Every query is scoped by a global trait — you cannot opt out by accident.
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope('tenant', function ($query) {
$query->where('tenant_id', Tenant::currentId());
});
static::creating(function ($model) {
$model->tenant_id ??= Tenant::currentId();
});
}
}
What this unlocked for the product
With isolation as a default, modules (menu, booking, accounting) can ship independently without inventing a new tenancy story each time. The product grows by composition — not by rewriting the core every quarter.
That is the real win of a multi-tenant core: not clever SQL, but a product that can keep shipping while staying safe.

