orchestra/tenanti
Orchestra Tenanti brings multi-tenancy to Laravel: register tenants, switch database connections per tenant, and run tenant-aware migrations and Artisan commands. Includes tenant resolvers, configurable drivers, and a simple API for building SaaS apps with isolated data.
Architecture fit. orchestra/tenanti is a narrow, high-leverage piece of infrastructure: it specializes Laravel's Migrator and DatabaseManager to run the standard migrate/rollback/seed command family against a named tenant connection, selected at runtime. This only fits if the tenancy decision has already been made in favor of database-per-tenant or schema-per-tenant rather than a shared schema with a tenant_id discriminator. That is the pivotal architectural fork, and it is a product decision as much as an engineering one — it determines onboarding cost, backup/restore design, data-residency claims, and the shape of every future migration for the life of the platform. Tenanti does not provide tenant-aware cache, filesystem, queues, sessions, routing, or a tenant registry. It assumes you bring a tenant model and a resolver, and it covers the error-prone schema plumbing around them. Evaluate it as the migration half of a tenancy stack, not the whole stack.
Integration feasibility. The integration surface is unusually small, which is the package's main technical virtue. Publish the config, define one class extending Orchestra\Tenanti\Tenant implementing getConnectionName() and getConfig(), register it under a driver key, point the driver at a dedicated migration directory, and a working proof-of-concept is a ten-minute spike: two tenants, one trivial migration, tenanti:migrate user --tenants=1,2 --pretend. If --pretend emits SQL against two distinct database names, the wiring is correct and the remaining work is orchestration, not integration. There is an observer (Orchestra\Tenanti\Observer) that hooks provisioning into tenant row creation, which is the right affordance for automated onboarding.
Technical risk. The dominant risk is framework coupling, not package quality. Because Tenanti extends internals, a Laravel major upgrade can silently change connection resolution or migration batch semantics. Mitigations are cheap but non-optional: pin exact versions (6.3.*, not a caret), commit the lockfile, and stand up a CI harness of two ephemeral tenant databases running migrate then rollback on every build. Budget roughly half a day per Laravel major upgrade for validation. Secondary risks are operational rather than integration-shaped: the default connection can still be used by queue workers, scheduled commands, and anything holding a stale connection instance, producing wrong-database writes (a data-integrity incident, not an availability one); connection instances accumulate in long-running workers and can exhaust max_connections; cross-tenant operations are not transactional, so a fan-out wave leaves partial state on failure; and there is no built-in locking, so concurrent deploys can collide on the same tenant. All of these have known, cheap mitigations — middleware chokepoint plus an assertion on active connection name, DB::purge() at job end, cache locks per tenant, re-run-oriented tooling — but they are work you own.
Key questions.
composer.json before writing code.Stack fit. Native Laravel/PHP only. The package extends framework internals rather than reimplementing them, so it inherits Artisan ergonomics, the migration repository, and seeder behavior — meaning engineers already fluent in migrate are productive on tenanti:migrate immediately. Fit is excellent for a Laravel monolith moving to per-tenant isolation; poor for polyglot or non-Laravel services, and irrelevant if the isolation model is shared-schema.
Migration path. For greenfield tenants, the sequence is straightforward: define driver, create database, register connection, run tenanti:migrate. For converting an existing single-database install, do not replay historical migrations per tenant — one non-idempotent historical migration multiplied by N tenants is an outage. Instead baseline the current schema as "tenant v0," create tenant databases from that baseline, then insert the corresponding rows into each tenant's migrations table so Tenanti considers them current. Separately, keep control-plane migrations in database/migrations and tenant migrations in a dedicated per-driver directory; do not share files and filter with --path, because the first control-plane migration with a foreign key to users breaks tenant migrations. Note also that foreign keys cannot cross databases — relationships between tenant tables and the central users/tenants tables are enforced in application code, and require the consistency/cleanup job a real FK would have made unnecessary.
Compatibility. The critical compatibility axis is the Laravel major version, and the supplied material does not document the supported range. Treat support breadth as unverified until confirmed against the package's composer.json and CI matrix — that is the single most common early failure mode. A second axis is database engine: schema-per-tenant behavior differs between MySQL and PostgreSQL, so pick the target engine before committing. A third is organizational: any code path touching a tenant table without an explicit connection target is a latent bug, so compatibility is as much about team conventions as version numbers.
Sequencing.
--pretend verification. Timebox to a day.TenantSchema) exposing migrate, rollback, seed, schemaVersion. Application code talks to that, never to Orchestra\Tenanti\*. This is the single highest-value structural decision — it converts a fork contingency from a codebase-wide search into a one-class swap.Observer into an explicit onboarding action so provisioning is automated and emits a completion event rather than merely a creation.Maintenance. This is the primary cost. With zero dependents, no meaningful star traction, and a narrow ecosystem, the package should be treated as a potential maintenance liability rather than a strategic dependency. The source is roughly two thousand lines and readable in an afternoon; that investment is mandatory, because questions like "what exactly happens if a tenant migration fails midway" are answered only in code. Version pinning plus the CI upgrade harness converts the recurring cost into a predictable half-day per Laravel major. The wrapper class caps the blast radius if you must vendor or fork. There is no documented support matrix in the supplied material — assume you are your own first line of support.
Support. Upstream responsiveness is unproven from the available signals (score 0.3, no dependents). Plan for self-support: a named owner for the tenancy layer, an internal runbook covering fan-out, rollback, drift reconciliation, and canary promotion, and a written fork/swap contingency. The wrapper class is what makes that contingency credible.
Scaling. Costs scale linearly and in three distinct dimensions. Migration time grows with tenant count, which is why chunked fan-out in waves of ~25 with per-tenant success/failure logging is the operating norm rather than a nicety. Connection count grows with concurrent tenant resolution, and long-running queue workers are the sharp edge — each resolved tenant adds a database-manager entry and an open connection, so DB::purge($connectionName) at job end plus capped worker concurrency is required to avoid exhausting max_connections overnight. Backup and restore surface grows one-for-one with tenant databases, which is a real platform cost that should be reflected in pricing and support tiers. This profile is a strong fit for mid-market and enterprise segments where isolation is contractual, and a poor fit for high-volume low-ACV self-serve, where per-tenant databases multiply cost faster than they add value.
Failure modes.
migrations tables are your only visibility, so build the schema-version query into a health check from day one.--force / --no-interaction hanging a production pipeline on an invisible prompt.Ramp-up. Fast for the happy path, slower for the failure paths. A Laravel-fluent engineer reaches a working two-tenant spike in under an hour following the published config and one driver class. Realistic time-to-proficiency is two to three days to internalize the connection-isolation discipline, the fan-out workflow (canary with --pretend, canary for real, verify, chunk the remainder, re-run failures), and the drift baseline query; call it one to two weeks before an engineer can be trusted to run a production fan-out unsupervised. The published config is effectively the entire public surface area, so most of the ramp is operational judgment rather than API surface — which is a good sign for the package and a reminder that the risk sits in your orchestration layer, not in the dependency.
How can I help you explore Laravel packages today?