Skip to content
~/benhattab
Back to work
In progressSaaS2025 — present

Gerex Food

Multi-tenant restaurant management SaaS

A multi-tenant SaaS for restaurants: stock, orders, finances, alerts and realtime reporting, with distinct interfaces for front of house and kitchen. A single API serves both the restaurant dashboard and the platform back office.

0
rows served without a tenant
3
applications
Role

Architect & developer — multi-tenant API, dashboard, superadmin

Stack
Laravel 13PHP 8.3Next.js 16React 19Laravel HorizonLaravel ReverbRedisDocker
The problem

In multi-tenant SaaS, cross-tenant data leaks are never a decision — they are an oversight. One query written without the `tenant_id` filter, in a report or an export added six months later, and one restaurant sees another's revenue. Relying on every developer's discipline on every query is a strategy that always fails eventually.

The approach

I made isolation structural rather than declarative. Middleware resolves the current tenant on every request, a request-scoped singleton carries it, and a trait applies a global scope to every tenant-owned model while auto-stamping `tenant_id` on create. The developer has nothing to remember.

The key part is what happens when no tenant resolves: the scope does not open access, it closes it. With no resolvable tenant, queries return zero rows. A resolution bug produces an empty page — visible and fixable — instead of a silent dump of every tenant's data.

An escape hatch exists, but it is explicit: the cross-tenant analytics layer has to drop global scopes by hand and filter on its own. Making the dangerous case verbose and the safe case automatic is the entire point.

Architecture
  1. 01

    Tenant resolution

    Middleware appended to the `api` group reads the `X-Tenant-Slug` header (or route param), falling back to the authenticated user's `tenant_id`.

    TenantMiddleware

  2. 02

    Fail-safe scope

    A global scope that returns zero rows when no tenant resolves. Failure closes by default rather than opening.

    TenantScope

  3. 03

    Queues & realtime

    Laravel Horizon on Redis for background processing, and Laravel Reverb to push live orders and alerts to front of house and kitchen.

    Horizon · Reverb

  4. 04

    Restaurant dashboard

    Next.js 16, the teams' day-to-day app: stock, orders, finances, alerts.

    Next.js 16

  5. 05

    Platform superadmin

    Separate app for supervising every tenant, with cross-tenant analytics explicitly unscoped.

    Next.js 16

Trade-offs

The scope closes rather than opens

The naive implementation of a multi-tenant scope filters nothing when the tenant is missing — and therefore serves everything. I inverted the default: no tenant, no rows. The single exception is the user model, which must stay findable so authentication can work before any tenant is known.

Highlights
  • Tenant isolation applied automatically by global scope, failing closed by default
  • Stock, orders, finances and realtime reporting in a single application
  • Distinct interfaces for front-of-house and kitchen teams
  • Background processing under Horizon, realtime over Reverb WebSockets
  • One API serving both the tenant dashboard and the platform superadmin