Laravel · SaaS
Building a Multi-Tenant SaaS in Laravel: Architecture Decisions That Matter
April 23, 2026
Multi-tenancy is one of the few architectural decisions in SaaS development that is genuinely difficult to change after the fact. Get it right early and it becomes invisible infrastructure. Get it wrong and you will spend months refactoring core data models while trying not to break production for existing customers.
This is a guide to the decisions that matter on day one: the ones that constrain everything else. And the decisions that can safely wait.
Decision 1: database strategy
There are three standard approaches to isolating tenant data, each with a different trade-off between isolation and operational complexity.
Shared database, shared tables (row-level tenancy)
Every table has a tenant_id foreign key. All tenants share the same schema. This is the simplest approach to implement and the easiest to operate: one database, one migration run, standard Laravel tooling throughout.
The risk is data leakage. A missing where tenant_id = ?clause in a query returns every tenant's data. In Laravel, you mitigate this with a global scope applied to every Eloquent model, which automatically appends the tenant filter. The scope must be registered before any query runs, typically in middleware.
Shared database, separate schemas
Each tenant gets their own database schema (in PostgreSQL) or database prefix (in MySQL). Migrations run per-tenant. The application switches the active schema on each request. This provides stronger logical isolation than row-level tenancy without the operational overhead of separate database servers.
Database per tenant
Each tenant has a completely separate database. This provides the strongest isolation and makes it trivial to migrate, backup, or delete a single tenant's data. The cost is operational: running migrations across hundreds of databases requires tooling, and connection pooling becomes a concern at scale.
Our default recommendation for early-stage SaaS is row-level tenancy with a global Eloquent scope. It is the fastest to build, easiest to operate, and straightforward to migrate away from if you later need stronger isolation.
Decision 2: tenant identification
You need to know which tenant is making a request before any application logic runs. The two common approaches are subdomain routing and path-based routing.
Subdomain routing (acme.yourapp.com) is the more common SaaS pattern. It requires wildcard DNS and a wildcard SSL certificate, but it provides clean URL separation and makes tenant context obvious. Resolve the tenant in a middleware that reads $request->getHost(), strips the subdomain, and looks up the tenant record.
Path-based routing (yourapp.com/acme) is simpler to set up but less conventional for SaaS. It works well for internal tools or admin panels where custom domains are not required.
Whichever approach you choose, the tenant resolution must happen in middleware and the resolved tenant must be bound into the service container so it is available everywhere in the application without passing it through every method call.
Decision 3: queue isolation
When a job is dispatched to a queue, the HTTP request context (including the resolved tenant) is gone. If your jobs touch tenant-scoped data, you must serialize the tenant identifier with the job and re-resolve the tenant at the start of the job'shandle() method.
The cleanest pattern is a TenantAware interface and a corresponding job middleware that reads the tenant ID from the job, looks up the tenant, and registers it in the container before handle() is called. This keeps tenant resolution logic in one place rather than duplicated across every job class.
Decision 4: migrations
With row-level tenancy, migrations are straightforward: you run them once against the shared database. With schema-per-tenant or database-per-tenant, you need a migration runner that iterates over tenant records and applies pending migrations to each tenant's schema or database.
Build this tooling before you have more than a handful of tenants. Running migrations manually across 200 databases is not a viable deployment process.
What can wait
Not everything needs to be solved on day one. Billing integration (Stripe customer per tenant), per-tenant feature flags, tenant-specific configuration, and usage metering can all be added incrementally once the core isolation model is working. Start with the simplest thing that correctly separates tenant data, and build from there.
The goal in the early stages is to make the wrong thing hard to do accidentally. A global scope that enforces tenant filtering on every query is the right tool for that, not infrastructure built for a scale you have not yet reached.