SaaS platform development
Multi-Tenant SaaS Development in Laravel
The isolation decision is made once and decides everything after it - what a noisy customer costs their neighbours, and whether an enterprise buyer's security review ends the conversation.
Every multi-tenant product makes one decision early that it cannot easily unmake: where the wall between customers sits. Everything else - what an outage costs, how long a restore takes, whether an enterprise security review goes well - follows from it.
The decision is usually made implicitly, by whoever adds the first
company_id column, and discovered two years later when it starts to hurt.
The three models, and who they suit
One schema, a tenant column on every table. The cheapest to build and to run. One database, one migration, one connection pool. It suits products with many small accounts and no customer large enough to demand otherwise.
Its weakness is that isolation is a property of your code rather than of the infrastructure. A missing filter is a data breach, a restore for one customer means extracting their rows from a backup of everybody's, and a noisy tenant's heavy query is felt by everyone.
A database per tenant. Isolation you can point at during a security review. Backup and restore are per customer, a deletion request is a dropped database, and residency requirements are satisfiable per account.
The cost is operational and it is real: migrations run N times and can fail on the ninety-fourth, connections multiply, and "how many tenants do we have" becomes a question about infrastructure rather than a count.
Both, by tier. Shared by default, dedicated for customers who pay for it. This is what most successful products end up running, and building for it early costs little if the tenant boundary is explicit - the application asks for a connection rather than assuming one.
What we build either way
A tenant resolved once, at the edge. From the domain, the subdomain or the authenticated user - in one place, early in the request, so nothing further downstream has to work it out again. Jobs carry it explicitly, because a queued job has no request to read it from and this is where cross-tenant bugs are actually born.
Isolation enforced by the model, not by discipline. A global scope means a
forgotten where clause returns nothing rather than everything. Combined with
a tenant-aware factory and tests that assert another tenant's record is
invisible, the guarantee survives the developer who joins next year.
Queues that cannot starve each other. One customer importing two million rows must not delay everybody else's password resets. That is queue and worker layout - covered in queue engineering - and it is the most common complaint we are called about on SaaS platforms that scale past their first few hundred accounts.
Caches and storage keyed by tenant. Cache keys, uploaded files and generated documents all need the boundary too. A cache key that omits the tenant is the fastest possible data leak and the hardest to notice.
Onboarding and offboarding as code. Creating a tenant - schema, seed data, domain, billing - should be one operation rather than a runbook. So should removing one, including what the contract says about retention.
Billing, which is where the schema leaks
Subscription billing looks like an integration and behaves like a modelling problem. Plans change, customers upgrade mid-period, trials extend, a payment fails three days after the feature was used.
Two decisions save most of the pain. Entitlements are their own concept rather than being read from the payment provider - the application asks whether this tenant may do this thing, not what their Stripe status is. And billing history is immutable: an upgrade writes a new record rather than editing the old one, because the invoice you issued last March has to keep saying what it said.
Where this fits
This is application development with the tenancy decisions made deliberately. If a platform already exists and the trouble is that one customer's load is everyone's problem, that is performance and scaling - and if the trouble is that nobody is sure whether the isolation actually holds, the audit answers that specifically, with the queries to prove it either way.
How an engagement runs
It starts with one question answered on paper: which of the three tenancy models this product needs, and what a contract with your largest future customer is likely to demand.
The scope that follows sets out the model, the isolation guarantees, the billing behaviour and the phases, with a price. It is signed before any of it is built, because tenancy is the one decision on this page that cannot be changed later without a migration project of its own.
Then the build, in phases that each end deployed, with the isolation tests written alongside the features.
What you receive
The tenancy model written down before anything is built, then the application with isolation enforced at the data layer, and a test suite whose negatives are the point. A tenant that cannot read another tenant's rows, asserted rather than assumed.
With it, the billing reconciliation, the migration path between the three models in case you picked the wrong one, and a written answer to the question every enterprise buyer eventually asks about where their data sits and who can reach it.
