Why Lasagna
Multi-tenancy is one of those problems that looks easy from far away. A tenant_id column. A few middlewares. Done.
Then production happens.
A backfill job touches the wrong rows because someone forgot the WHERE. A customer asks for their data export and you spend a weekend writing JOIN-and-pray scripts. A tenant's connection pool exhausts and takes down the others. A migration ships fine in dev but stalls in production because three tenants have a stale schema. You start writing per-tenant audit code, then per-tenant feature flags, then per-tenant backups, then per-tenant rate limits; and six months later you have an operational nightmare none of which is your product.
Lasagna is the package I wish existed when I hit that wall the first time. It assumes you'll need every one of those things eventually, so it ships them on day one; but as opt-in satellites, not a god-class.
The metaphor that earned the name
A lasagna is the only dish I know that improves as you add layers, provided each layer keeps to itself. Sauce, pasta, béchamel, ragù, cheese; every layer has a job, none of them seep into the next one unless you want them to. That is how this package thinks about multi-tenancy:
Tenant schemas
Each tenant lives in its own PostgreSQL schema (tenant_<uuid>). Provisioned by the package, named by the package, routed by the package. Your queries call TenantBaseModel.query() and the schema switch happens before the SQL leaves your process.
Bootstrappers
Cache, drive, mail, session, queue, transmit. Each bootstrapper scopes its service to the active tenant; automatically, via AsyncLocalStorage. No threading tenantId through helpers, no manual prefix.
Operational services
Circuit breaker, read replicas, OpenTelemetry, Prometheus, health probes, audit logs, signed webhooks, SSO/OIDC, feature flags, branding, quotas. The plumbing your SaaS will eventually want, in a consistent contract.
AdonisJS 7
The framework everything stands on. We don't fight it; providers, middleware, ace commands, container bindings, config. If you know AdonisJS, you already know how to use Lasagna.
PostgreSQL 14+
The foundation. Schemas are a Postgres-native concept and we lean into that. MySQL and MariaDB are not on the roadmap.
The Zellige principle
Each tile in a Moroccan Zellige mosaic is shaped by hand. It has its own colour, its own cut, its own place. From far away you see the star; from up close you see thousands of clean, precise edges that never overlap.
That is the second principle of Lasagna: clean edges. The package never imports your Tenant model; it asks the IoC container for a TenantRepositoryContract and lets you bind it. The package never hardcodes tenant_id; it routes through IsolationDriver so you can swap schema, database, or row-scoping. The package never assumes a queue, a cache, a mailer; every bootstrapper is opt-in and auto-detected.
You write your app. Lasagna handles the seams.
How we compare to stancl/tenancy
stancl/tenancy is the gold standard in the Laravel world; stable since 2019, with a dedicated site, a book, a course, and a Discord. We owe a real debt to that project: it set the bar for what a serious multi-tenancy package should look like.
Lasagna covers the same ground (4 isolation drivers, 6 bootstrappers, 5 resolvers, full lifecycle hooks, imperative API) and adds the operational surface stancl leaves to the user: a doctor command, integrated read replicas, OpenTelemetry, Prometheus, scheduled backups with retention tiers, the impersonation flow, quotas-as-middleware, the REST admin API + OpenAPI 3.1 spec, and 33 ace commands vs stancl's 7.
It also has gaps that stancl has filled: an admin dashboard UI, a starter kit, an active Discord. Those are still on the roadmap. (Billing was on this list too — it shipped in v0.2 as a Stripe satellite.)
The table below is the same data we use internally to track our position. Filter by category, by who-wins, or search a feature.
Honest about the gaps
We're not pretending to be a drop-in replacement on every axis. If your team needs MySQL or a Nova-equivalent admin UI today, stancl is the right call. If you need every operational lever a SaaS will eventually want; and you're on AdonisJS 7 + PostgreSQL; Lasagna ships more of them in one box.
What you get that you can't easily build yourself
These are the calls that take weeks of engineering when you do them in-house, and that we already debugged:
- Circuit breaker per tenant. Opossum-backed, scoped to each tenant's database access. One bad schema can't take the others down with it.
- Read replica routing. Round-robin, random, or sticky-by-tenant. Connection naming is deterministic, lazy provisioning is built in.
- Doctor command.
tenant:doctorwith ten built-in checks, a--fixflag for auto-recovery,--jsonfor CI gates, and--watchfor a live TUI. The plugin API lets your app contribute checks. - Backups with retention tiers.
pg_dump, S3 mirror, JSON sidecar with checksums, tier-based intervals (standard,premium, …), per-tenant resolution. - REST admin API. 36 endpoints, OpenAPI 3.1 spec, Swagger UI. You bring the auth middleware; we bring the wiring.
- Audit, webhooks, quotas, feature flags, branding, SSO, metrics, impersonation, Stripe billing. Nine satellites, opt-in via the configure command.
Hardened against the failures that bite you in production
Multi-tenancy is the kind of code where a mistake surfaces months later, in production, on a Friday. So before tagging v2 we audited every guarantee in the list below against real Postgres, real Redis, real BullMQ; no mocks, no in-memory shortcuts.
- Cross-tenant isolation under HTTP concurrency. Interleaved requests across N tenants writing and reading their own rows — zero cross-reads, verified end-to-end.
- Quota atomicity.
consume()runs inside a single Redis Lua script. 50 parallel callers againstlimit=10produce exactly ten successes and fortyQuotaExceededException. No race window. - SSO replay protection. OIDC
stateis consumed via atomicGETDEL; two concurrent callbacks with the same state can never both succeed. - Audit log immutability.
tenant_audit_logscarries Postgres triggers that blockUPDATE,DELETE, andTRUNCATEfrom inside the tenant's own schema. - Header-vs-domain hijack.
customDomain({ strict: true })rejects a request whosex-tenant-iddisagrees with the custom-domain match — 400E_TENANT_HEADER_DOMAIN_MISMATCH, not a silent override. - Rate-limit fails closed. Redis down means 503, never silent fail-open. Opt into
failOpen: trueonly if your threat model accepts it. - Identifier injection. Every user-supplied identifier that reaches DDL passes
assertSafeIdentifier. An architectural test fails CI if any futurerawQueryinterpolates a template variable without going through that helper. - Doctor checks against real state.
long_running_queries,replica_lag,queue_stuckrun in CI against a live Postgres / BullMQ — not mocked clocks.
The list above is the current audit surface; every item has a spec under tests/integration/. If you spot a tenancy guarantee that should be on it and isn't, open an issue — the audit is iterative.
What's coming next
Phase 4 of the roadmap: a public Discord, the @adonisjs-lasagna/dashboard package (Inertia + Vue admin UI consuming the OpenAPI spec), create-lasagna-saas (a starter kit that wires Lasagna + Auth + Stripe), and the v2.0.0 stable cut.
If you want to follow along or contribute, the GitHub repo is the place. Issues are open for feature requests.