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.

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

FactorDatabase-per-TenantSingle-Database Scoping
Data isolationPhysical, survives application bugsLogical, depends on the query scope
Schema migrationsOnce per tenant databaseOnce for the entire customer base
Tenant provisioningDatabase creation and full migrationInsert a record, ready in seconds
Cross-tenant reportingNeeds a separate aggregation layerStandard query with the scope disabled
Infrastructure costRises with each tenant addedLargely flat as tenants are added
Best suited toRegulated data, fewer large customersGrowing 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

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

TacticHow We Applied ItWhat It Prevented
Atomic releasesEach build went to a fresh directory, with a symlink switched once completeRequests hitting a partially updated codebase
Graceful worker restartsWorkers finished the current job before exitingJobs stopping mid-execution and leaving records half written
Backward-compatible releasesEvery deployment ran correctly against both the old and new schemaRollbacks blocked by a migration already applied
Batched migrationsSchema changes ran in batches at low traffic, with short lock timeoutsLong locks queueing behind live customer queries
Feature flags on read pathsSwitching a customer to scoped reads was a flag change, not a deploymentCutover 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.

MeasureBefore MigrationAfter Migration
Tenant provisioning timeClose to three engineering daysUnder fifteen minutes, fully automated
Deployments per releaseTwenty-twoOne
Database instancesTwenty-twoOne, plus a central database
Schema migration effortTwenty-two separate runsA single command for all tenants
Monthly infrastructure costBaselineReduced by roughly 60 percent
Sprint capacity on maintenanceAround 40 percentUnder 15 percent
Defect reproduction timeSeveral hours per reportUnder 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.

Frequently Asked Questions (FAQs)

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.

Pratik Panchal

Pratik Panchal

Director of Engineering at Bacancy

CONNECT WITH THE AUTHOR
SUBSCRIBE NEWSLETTER