Laravel Multi-Tenancy Migration: How We Moved a Single-Tenant App with Zero Downtime
Last Updated on August 18, 2026
Quick Summary
Laravel multi-tenancy migration is a critical step for growing applications facing rising infrastructure and maintenance costs, and doing it without downtime requires careful planning and execution. This article breaks down how Bacancy Technology migrated a live Laravel application to a multi-tenant architecture, covers the strategies used to maintain zero downtime, highlights the key challenges involved, and shares the results, including 60% lower infrastructure costs and provisioning reduced from nearly 3 days to under 15 minutes.
Table of Contents
Introduction
Single-tenant architecture is where most Laravel products begin, and it works well for the first few years. The strain shows later, once every customer account carries its own infrastructure and its own release cycle. Our client reached that stage during a period of steady growth.
The engagement carried one condition we could not negotiate around. Users were active around the clock, and the contract included an uptime commitment backed by financial penalties. ITIC’s Hourly Cost of Downtime research reports that one hour of downtime costs over $300,000 for more than 90 percent of mid-size and large enterprises.
This post records how Bacancy Technology delivered a Laravel multi-tenancy migration on a live production system without a maintenance window. It covers the architectural decisions, the migration sequence, the problems we hit, and the measured results.
The Single-Tenant Bottleneck That Forced a Migration
The application started as a single-tenant build, with one deployment serving one customer. Every account had its own database, environment file, queue workers, and cron entries. That arrangement stayed manageable at four customers and became an operational problem at twenty-two.
Provisioning was the first cost to surface. Onboarding one account required a fresh server, a database script, a manual environment setup, and a full migration run. The process consumed close to three engineering days and produced no new product capability.
Deployment was the second cost, and it grew faster. A single bug fix meant twenty-two separate releases, each carrying its own risk of partial failure. Configuration drift followed, with feature flags enabled for some customers and forgotten for others. Support then became unpredictable, because no two environments behind a defect report were identical.
The decision came down to a cost curve rather than an architectural argument. Infrastructure spend rose with every account added while revenue per account stayed flat, and maintaining duplicated stacks eventually cost more than consolidating them.
Multi-Tenancy in Laravel: Why and How We Adopted It
Multi-tenancy means one running application instance serving many customers, with each customer’s data kept separate at the application layer. The codebase, deployment, and infrastructure are shared. Only the data and configuration stay tenant-specific. Laravel carries a large share of production PHP work, with 9.3% of professional developers reporting extensive work in it over the past year in the 2025 Stack Overflow Developer Survey, so the pattern below applies well beyond this one client.
Alternatives We Evaluated Before Committing to a Tenancy Model
Consolidation was not the only option, and we tested three others first. Better deployment automation was the cheapest fix, but a pipeline running twenty-two releases still leaves twenty-two databases and twenty-two failure points. Containerising each customer removed the manual provisioning work while keeping the infrastructure bill on the same curve. One shared database with no tenancy layer was faster to build and unacceptable for data isolation.
Core Building Blocks of a Multi-Tenant Laravel Application
Laravel suits this model because its service container resolves dependencies per request, which allows tenant-specific values to be swapped before business logic runs. Five components carry that work:
Tenant Identification: Middleware reads the incoming request, usually the subdomain or a custom domain, and resolves which tenant it belongs to.
Tenant Context: The resolved tenant is bound into the service container, so every service, job, and event handler downstream can read it.
Connection Switching: The default database connection is rebound at runtime, pointing queries at the correct tenant database or applying the correct scope.
Bootstrappers: Cache prefixes, filesystem disks, queue payloads, and mail configuration are re-registered per tenant, which prevents one account from reading another account’s stored data.
Tenant Lifecycle Events: Creation, migration, seeding, and deletion run through events, so provisioning becomes a single command rather than a manual checklist.
One distinction matters before any of this is wired up. Some data belongs to the platform rather than to a customer, including the tenant registry, billing records, and internal admin accounts. That data lives in a central context and must never pass through tenant resolution.
Why Do We Choose a Package Over a Custom Implementation?
Our team at Bacancy Technology adopted stancl/tenancy rather than writing the tenancy layer ourselves. Building identification, connection switching, and bootstrappers in-house would have added months to the timeline. It would also have left the client’s own team maintaining infrastructure code after the engagement closed. Adoption then ran in a deliberate order, with the package going in first while tenancy stayed disabled. Central routes and models were marked next, which drew a clear line around platform data.
The trade-off is worth stating plainly. Data isolation shifts from a physical guarantee to an application-level responsibility, and one missing scope becomes a data leak.
Choosing a Tenant Isolation Model: Database-per-Tenant vs Single-Database Scoping
Isolation is the decision that shapes every later step. It determines how migrations run, how backups work, and how every query gets written. Reversing it after the cutover means repeating most of the migration, so we settled it first.
How Database-per-Tenant Isolation Works in Practice
Each tenant receives a separate database, and the application switches the connection once the tenant is identified. Separation is physical, so a missing query scope cannot expose one customer’s records to another. The cost sits in operations, because every schema change runs once per tenant database. Cross-tenant reporting also needs a separate aggregation layer.
How Single-Database Scoping Works in Practice
All tenants share one database, and every tenant-owned table carries a tenant_id column. A global query scope applies the filter on every read and write, so application code stays close to its original shape. Isolation then depends entirely on that scope holding, which makes a forgotten scope a data leak rather than a bug.
Isolation Model Comparison at a Glance
Factor
Database-per-Tenant
Single-Database Scoping
Data isolation
Physical, survives application bugs
Logical, depends on the query scope
Schema migrations
Once per tenant database
Once for the entire customer base
Tenant provisioning
Database creation and full migration
Insert a record, ready in seconds
Cross-tenant reporting
Needs a separate aggregation layer
Standard query with the scope disabled
Infrastructure cost
Rises with each tenant added
Largely flat as tenants are added
Best suited to
Regulated data, fewer large customers
Growing counts, moderate data volume
Why We Chose Single-Database Scoping for This Migration
Three factors decided it. Per-tenant schema migrations would have recreated the operational load we were hired to remove. The client roadmap included cross-customer analytics, which single-database scoping supports directly. Data volume per customer was moderate, so table growth stayed within what indexed queries handle comfortably.
Compliance could have reversed this decision, and it did not apply here. Their contracts required logical separation and audit trails rather than physically separate databases. A client under stricter healthcare or financial rules would push us toward database-per-tenant.
How We Planned the Migration Without a Maintenance Window
A migration that cannot pause has no rollback moment, so every change had to work correctly against both the old and the new data shape.
The plan was built around expansion and contract, which splits a breaking change into 3 non-breaking stages. New columns and tables are added first, while the old structure keeps working untouched. Data is then backfilled, and the application begins writing to both shapes at once. Only after every read has moved across does the old structure get removed.
Four rules governed the order of work, and none were negotiable during delivery:
Additive changes only until cutover: no column drops, no renames, and no type changes on tables carrying live traffic.
Backfills run in batches: every backfill is processed in chunks with a delay between them, so no statement holds a lock long enough to affect latency.
Every stage sits behind a flag: tenant resolution, scoping, and the new write paths each had a toggle that could be switched off without a deployment.
Read paths changed last: writing to the new structure is safe while nothing depends on it, so reads moved only after backfills were verified.
Verification came from running the tenant-aware code path against production data in shadow mode for 2 weeks. Requests executed both the old query and the scoped query, compared the results, and logged any mismatch without changing the response. When mismatches reached zero and held there, the scoped path became authoritative.
Our 6 Steps Laravel Multi-Tenancy Migration Process
The work ran as six sequential steps, each deployable on its own. No step depended on a change that had not already been verified in production.
Step 1: Auditing the Single-Tenant Schema and Shared State
The audit inventoried everything that assumed one customer. We classified all sixty tables as central or tenant-owned, then catalogued every raw query, cache key, and storage path referencing a single account. Shared state was the harder half, because singletons and cached lookups carried customer data between requests.
Step 2: Introducing the Tenant Model and Tenant Identification
We created the tenants table and a domains table linking each customer to their hostname. Identification middleware resolved the tenant from the request and bound it into the service container. Central routes stayed outside that middleware, so billing and admin continued working without tenant context.
Step 3: Adding tenant_id and Backfilling Existing Data
The column was added as nullable, with an index, on every tenant-owned table. Nullable matters here, because a non-nullable column would have blocked writes from the running application immediately. Backfills ran in batches of a few thousand rows with a pause between them, keeping lock duration short enough to avoid latency spikes.
Step 4: Wiring Tenant-Aware Connections with stancl/tenancy
The package supports single-database tenancy through the same identification layer it uses for separate databases. We enabled its bootstrappers to re-register cache prefixes, filesystem disks, and queue payloads per tenant. That prevented one customer from reading another customer’s cached values or uploaded files, which a query scope alone does not cover.
Step 5: Enforcing Row-Level Scoping with Global Query Scopes
A trait applied a global scope to every tenant-owned model and populated tenant_id automatically on creation. Raw queries were the exposure point, since global scopes do not apply to them. We rewrote each one found in the audit and added a static analysis rule blocking new raw queries against tenant-owned tables.
Step 6: The Zero-Downtime Cutover
Customers moved onto the consolidated database one at a time rather than together. Each ran in dual-write mode first, with records written to both the original database and the shared one. Shadow reads compared both paths and logged mismatches without affecting responses, with feature testing in Laravel covering the same scenarios in CI before each customer moved. When a customer showed zero mismatches for a full week, their read path switched over through a feature flag. Rollback stayed available throughout, because the original database kept receiving writes until that customer was stable.
Need a Laravel multi-tenancy migration delivered without a maintenance window?
Hire Laravel developers from Bacancy Technology who have run tenant isolation, backfills, and cutovers on live production systems, so your customers never see a maintenance page.
What Breaks in a Multi-Tenant Laravel App That Nobody Warns You About
Query scoping gets most of the attention in multi-tenancy guides, and it gives us the least trouble. The failures came from everything outside the request lifecycle, where tenant context does not exist unless code sets it.
Queued jobs: A job dispatched inside a tenant request runs later on a worker with no request behind it. We serialised the tenant identifier into every payload and restored context before the handler ran.
Cache keys: Two tenants requesting the same dashboard produce the same key, and the second receives the first tenant’s data. Nothing errors and nothing reaches the logs, so every key needs a tenant prefix.
Storage paths: Uploads written to a fixed directory let one customer overwrite another customer’s documents. Each tenant needed its own disk root, resolved at runtime.
Scheduled commands: The scheduler runs in a central context with no tenant attached, so a nightly report produces nothing or produces it for the wrong customer. Every task had to loop over tenants and initialise context per iteration.
Inbound webhooks: Third-party callbacks hit central routes carrying nothing that identifies the customer. We mapped external account identifiers to tenants and set context before processing the payload.
Zero-Downtime Deployment Tactics That Kept the App Live
Tactic
How We Applied It
What It Prevented
Atomic releases
Each build went to a fresh directory, with a symlink switched once complete
Requests hitting a partially updated codebase
Graceful worker restarts
Workers finished the current job before exiting
Jobs stopping mid-execution and leaving records half written
Backward-compatible releases
Every deployment ran correctly against both the old and new schema
Rollbacks blocked by a migration already applied
Batched migrations
Schema changes ran in batches at low traffic, with short lock timeouts
Long locks queueing behind live customer queries
Feature flags on read paths
Switching a customer to scoped reads was a flag change, not a deployment
Cutover errors needing a full build to reverse
Migration Results: Faster Tenant Provisioning and Lower Infrastructure Cost
The migration completed across eleven weeks, with every customer moved individually and no scheduled outage. Total unplanned downtime across the project was zero minutes.
Measure
Before Migration
After Migration
Tenant provisioning time
Close to three engineering days
Under fifteen minutes, fully automated
Deployments per release
Twenty-two
One
Database instances
Twenty-two
One, plus a central database
Schema migration effort
Twenty-two separate runs
A single command for all tenants
Monthly infrastructure cost
Baseline
Reduced by roughly 60 percent
Sprint capacity on maintenance
Around 40 percent
Under 15 percent
Defect reproduction time
Several hours per report
Under thirty minutes
Provisioning was the change the client felt first. Onboarding moved from a multi-day server build to a single artisan command creating a tenant record and its domain mapping. Infrastructure cost fell because twenty-two servers and databases collapsed into one environment, and each new customer now adds a database row rather than a server.
The engineering gain is harder to put in a table. Bug fixes reach every customer in one release, and configuration drift no longer exists as a category of defect. Roadmap work absorbed the sprint capacity that maintenance had been consuming.
Bacancy Technology's Take on Laravel Multi-Tenancy Migration
The technical work was not the difficult part of this project. Scoping queries and switching connections is well documented, and stancl/tenancy handles most of it. What decided the outcome was sequencing, and the discipline to keep every change reversible.
Four lessons carried the most weight:
Audit shared state before touching the schema. Singletons, cached lookups, and static properties caused more defects than any table change, and none appear in a database diagram.
Choose the isolation model against the roadmap rather than the current data volume. Cross-customer analytics and growth rate decided this migration, while compliance requirements would have reversed it.
Assume anything running outside a request has no tenant. Queues, scheduled commands, and webhooks each failed quietly, and quiet failures are the ones that reach customers.
Verify by comparison rather than by absence of errors. Shadow reads gave us evidence that the scoped path was correct, while waiting for exceptions would have told us nothing.
A single-tenant application that has outgrown its model rarely fails suddenly. It gets slower to deploy, more expensive to run, and harder to support, until consolidation becomes the cheaper option. Recognising that point early gives a team room to migrate carefully rather than under pressure. If your team is weighing that decision now, the Laravel development services team at Bacancy Technology can review your architecture and map a migration path that fits your uptime commitments.
Timelines depend on schema size and customer count rather than lines of code. A sixty-table application with around twenty customers took eleven weeks end to end, with roughly three of those weeks spent on audit and planning before any schema change landed. Applications carrying heavy raw SQL, custom reporting, or years of shared state take longer, because each of those has to be found and rewritten before scoping can be trusted.
Yes, provided every change stays additive until the final cutover. The approach that makes it work is expand and contract, where new columns are added first, data is backfilled in batches, and reads move across only after the writes are verified. Nullable columns, batched backfills, short lock timeouts, and feature-flagged read paths are what keep the application serving traffic throughout.
Decide it against your roadmap and your compliance obligations rather than your current data volume. Database-per-tenant suits regulated data and a smaller number of large customers, since isolation survives application bugs. Single-database scoping suits a growing customer count and moderate data per account, because schema changes run once instead of once per tenant. Cross-customer reporting usually tips the decision toward scoping.