You are about to build a SaaS product on Laravel, and one early decision shapes everything after it: how will each customer's data be kept apart? Get Laravel multi-tenancy right and onboarding, billing and scaling stay simple. Get it wrong and you face a data leak between customers or a painful rewrite two years in.
The short answer: most B2B SaaS products should start with a single shared database and a tenant_id on every tenant-owned table, enforced in code and (on PostgreSQL) by row-level security. Separate databases per tenant are worth their extra cost only when customers, contracts or regulations demand hard isolation. The rest of this guide explains how to decide, and what a safe implementation looks like in Laravel 13.
What multi-tenancy means in a Laravel SaaS
A tenant is the customer account that pays you: a company, a team or a workspace. Each tenant has its own users, settings and data, but every tenant runs on the same application code and servers. Multi-tenancy is the set of decisions that keeps those tenants apart:
- Identification: which tenant is this request for? (subdomain, custom domain, path or the logged-in user's account)
- Data isolation: how do you make sure tenant A never reads or writes tenant B's rows?
- Shared services: queues, cache, file storage, search indexes and logs must respect the same boundary.
- Operations: migrations, backups, reporting and support across all tenants.
The data isolation choice drives the cost and complexity of the other three, so start there.
The three multi-tenancy architectures compared
Single database, tenant_id |
Schema per tenant (PostgreSQL) | Database per tenant | |
|---|---|---|---|
| Isolation | Logical (code + optional RLS) | Separate schemas, one server | Separate databases |
| Cost per extra tenant | Lowest | Low | Highest (connections, backups, monitoring) |
| Migrations | Run once | Once per schema | Once per database |
| Cross-tenant reporting | Simple SQL | Harder | Needs a data warehouse or fan-out queries |
| Noisy neighbours | Shared load | Shared load | Can be moved to their own server |
| Per-tenant backup/restore | Hard | Possible | Easy |
| Good fit | Most B2B SaaS, many small tenants | Mid-size tenant counts on PostgreSQL | Enterprise, regulated or very large tenants |
Single database with a tenant_id column
Every tenant-owned table gets a tenant_id foreign key, and every query filters by it. It is the cheapest to run, migrations happen once, and analytics across customers is plain SQL. The risk is human: one query without the filter leaks data. That is why the filter must be automatic (a global scope) and why a database-level safety net is worth adding.
Schema per tenant (PostgreSQL)
PostgreSQL schemas let each tenant have its own set of tables inside one database. Isolation is stronger than a column filter and you still manage one server. But every migration runs once per schema, and with thousands of tenants the number of tables becomes an operational problem of its own. It is a middle ground that few teams need.
Database per tenant
Each tenant gets its own database, sometimes on its own server. Isolation is as strong as it gets, a single customer can be backed up, restored, moved or deleted on its own, and a heavy tenant can't slow down the others. In exchange you pay for more connections, more backups, slower migrations across hundreds of databases, and reporting that needs a separate pipeline.
How to choose: a short checklist
Pick database per tenant if any of these is true:
- Contracts or regulators require a customer's data to be physically separate, or deletable and exportable as a unit.
- Customers need their data in a specific region or on their own infrastructure.
- You will have few, large tenants (tens or hundreds, not thousands), each paying enough to cover the extra infrastructure.
Otherwise, pick a single database with tenant_id, especially if:
- You expect many small tenants, a free tier or self-service sign-up.
- You need reporting and analytics across all customers.
- Your team is small and wants one migration path and one backup to watch.
Hybrid setups are common later: keep most tenants in the shared database and move an enterprise customer to a dedicated database when their contract requires it. Design for that early by keeping all tenant resolution in one place.
Building shared-database tenancy in Laravel 13
Here is the core of a safe single-database setup. It is deliberately small: identify the tenant once per request, scope every query automatically, carry the tenant into queued jobs, and let the database refuse anything that slips through.
Identify the tenant
A middleware resolves the tenant (here from the domain) and stores its id in Laravel's Context, as hidden data so it isn't written to every log line:
namespace App\Http\Middleware;
use App\Models\Tenant;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Context;
use Symfony\Component\HttpFoundation\Response;
class IdentifyTenant
{
public function handle(Request $request, Closure $next): Response
{
$tenant = Tenant::where('domain', $request->getHost())->firstOrFail();
Context::addHidden('tenant_id', $tenant->id);
return $next($request);
}
}
Scope every query (and fail closed)
A trait on each tenant-owned model adds a global scope and fills tenant_id on create. If no tenant is set, it throws instead of quietly returning every row:
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Context;
use RuntimeException;
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope('tenant', function (Builder $query) {
$query->where($query->qualifyColumn('tenant_id'), static::currentTenantId());
});
static::creating(function (Model $model) {
$model->tenant_id ??= static::currentTenantId();
});
}
protected static function currentTenantId(): int
{
return Context::getHidden('tenant_id')
?? throw new RuntimeException('No tenant is set for this query.');
}
}
Admin screens that really need all tenants call withoutGlobalScope('tenant') explicitly, which makes those places easy to find in code review.
Keep the tenant in queued jobs
Background jobs are where tenancy bugs usually hide: an invoice job runs minutes later, with no request and no tenant. Laravel's Context solves most of this, because information in the context is captured when a job is dispatched and hydrated back while the job runs. The scope above therefore works inside jobs dispatched during a tenant's request. Jobs started by the scheduler have no request, so they loop over tenants and set the context for each one before doing any work.
Add PostgreSQL row-level security as a safety net
On PostgreSQL, row-level security (RLS) lets the database itself refuse rows from other tenants, even if a raw query forgets the filter:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::bigint);
The middleware sets the value for the connection, for example with DB::statement("select set_config('app.tenant_id', ?, false)", [(string) $tenant->id]);. Two details matter. First, table owners bypass RLS unless the table uses FORCE ROW LEVEL SECURITY, and superusers always bypass it, so the app should connect as a restricted role. Second, the setting lives on the database session: with long-running workers (Octane, queue workers) or a connection pooler, set it at the start of every request and job so a previous tenant's value is never reused.
RLS doesn't replace the Eloquent scope; it backs it up. Also remember that unique indexes are checked across all rows, so make them composite (tenant_id, email) rather than global.
Billing per tenant with Laravel Cashier
In a B2B SaaS the customer who pays is the tenant, not the individual user. Laravel Cashier assumes the User model by default, but you can register a different billable model with Cashier::useCustomerModel() and adjust Cashier's migrations to that table. Put the Billable trait on Tenant, and subscriptions, trials, invoices and plan limits all hang off the account that owns the data. That keeps billing and access checks in one place: "can this tenant use feature X" becomes one question instead of one per user.
Package or custom code?
You don't have to build all of this yourself:
- stancl/tenancy (Tenancy for Laravel) supports single- and multi-database tenancy with automatic identification. Its latest stable release, v3.10.1 (August 2026), supports Laravel 10 to 13. Version 4 is documented, including PostgreSQL RLS support, but had no stable tag on Packagist when this was written, so check before you depend on it.
- spatie/laravel-multitenancy v4 (PHP 8.2+, Laravel 11+) is smaller and more explicit, with guides for both a single database and multiple databases.
A package saves weeks on database-per-tenant setups, where creating databases, running migrations per tenant and switching connections is real work. For a single shared database, the code above is often all you need, and owning it makes the behaviour easier to audit. Our older walkthrough of stancl/tenancy shows the package approach step by step.
Mistakes that cause data leaks or rewrites
- Filtering by hand. Writing
where('tenant_id', …)in controllers works until someone forgets. Use a global scope that fails closed. - Forgetting the side channels. Cache keys, uploaded file paths, search indexes, broadcast channels and exports all need the tenant in them, for example
tenant:{id}:dashboardas a cache key. - Global unique constraints. A unique
emailacross all tenants blocks the same person from joining two customers and reveals that the address exists elsewhere. - Billing on the user. Moving subscriptions from users to tenants later means migrating live payment data. Decide on day one.
- Choosing database-per-tenant "to be safe". It is the right answer for some products, but it multiplies infrastructure and migration work. Choose it for a concrete requirement, not by default.
- No tests for isolation. Add a test that creates two tenants and asserts that tenant A's user gets a 404 for tenant B's records on every important route.
Planning a SaaS on Laravel?
Multi-tenancy is easiest to get right before the first customer signs up. We have built SaaS platforms with Laravel, including an AI-ready transportation management system and a quote builder and billing system, and we plan tenancy, billing and scale from the start. Read about why Laravel suits SaaS products, see our SaaS development service, or launch your project and tell us what you are building.