Skip to content

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.

73features compared
26Lasagna advantage
40parity
7stancl advantage
Feature
Lasagna
stancl/tenancy
Area
Subdomain resolver
Tenant identification
Custom domain
CustomDomainMiddleware maps Host header → tenant id
Tenant identification
Combined domain + subdomain resolver
domain-or-subdomain resolver
Tenant identification
Path resolver
Tenant identification
HTTP header resolver
via custom resolver
Tenant identification
Request data (body / query) resolver
Tenant identification
Resolver chain with fallback
user code
Tenant identification
Imperative API (tenancy.run / initialize)
Tenant identification
Cached tenant resolution
BentoCache
Tenant identification
Schema isolation (PostgreSQL)
default driver
Data isolation
Database-per-tenant
database-pg driver
Data isolation
Single-database row scoping
rowscope-pg + withTenantScope mixin
Data isolation
MySQL / MariaDB support
PostgreSQL is the strategic focus
Data isolation
SQLite (in-memory testing)
sqlite-memory driver
Data isolation
Per-tenant connection pool
Data isolation
Read replicas (round-robin / random / sticky)
Data isolation
Deterministic connection naming
Data isolation
Lazy connection provisioning
Data isolation
Pluggable IsolationDriver contract
Data isolation
Database adapter switching
Context switching
Cache prefix per tenant
BentoCache namespace
Context switching
Filesystem (Drive) prefix
Context switching
Queue context propagation (jobs see the tenant)
BullMQ + AsyncLocalStorage
Context switching
Mail driver context (per-tenant from / SMTP)
Context switching
Session scoping per tenant
Context switching
Broadcasting / Transmit per tenant
Context switching
Contextual logger (tenantId on every log line)
AsyncLocalStorage; no helper threading
Context switching
BootstrapperRegistry with LIFO lifecycle
Context switching
Typed lifecycle events
14 events
Lifecycle & events
Declarative hooks (config-driven)
Lifecycle & events
Per-phase before / after hooks
Lifecycle & events
Soft delete + retention with purge command
tenant:purge-expired
Lifecycle & events
Pipeline of jobs per event
5 jobs: install, uninstall, clone, backup, restore
Lifecycle & events
Provisioning commands
Commands / CLI
tenants:migrate / tenants:run
Commands / CLI
tenant:doctor with --fix / --json / --watch
8 built-in checks + plugin API
Commands / CLI
tenant:backup + retention tiers
Commands / CLI
tenant:restore
Commands / CLI
tenant:clone
Commands / CLI
tenant:import (load external SQL)
Commands / CLI
tenant:repl (REPL inside a tenant)
Commands / CLI
Total command count
32 commands
7 commands
Commands / CLI
Per-tenant maintenance mode + bypass token
Operations & SaaS
User impersonation with HMAC tokens
Operations & SaaS
Plans + quotas as middleware (returns 429)
TenantQuotaExceeded event
Operations & SaaS
Encrypted tenant attributes
Operations & SaaS
Audit log table + service
third-party (Spatie)
Satellites
Feature flags per tenant
Satellites
Webhooks (HMAC signed + retries + state machine)
Satellites
Branding per tenant
Satellites
SSO / OIDC with JWKS verification
iss/aud/exp + nonce
Satellites
Operational metrics (cursor-based SCAN)
Satellites
Circuit breaker per tenant (Opossum)
Reliability & observability
OpenTelemetry spans
Reliability & observability
Prometheus /metrics (no peer dep)
Reliability & observability
Health probes /livez /readyz /healthz
Reliability & observability
REST admin API + OpenAPI 3.1 + Swagger UI
31 endpoints
Reliability & observability
Universal routes (Route.tenant / central / universal)
Routing & app structure
Auto-loaded tenant.routes.ts
Routing & app structure
Cross-domain redirect helper
Routing & app structure
buildTestTenant factory
Testing helpers
MockTenantRepository
Testing helpers
setRequestTenant helper
Testing helpers
In-memory adapter for fast tests
sqlite-memory driver
Testing helpers
Auth integration (Adonis Auth / Bouncer / Laravel Auth)
host-app responsibility, auth bridge package planned
Ecosystem integrations
Billing integration (Stripe / Cashier)
billing package planned
Ecosystem integrations
Production deploy artefacts (Dockerfile, docker-compose, Helm chart)
Ecosystem integrations
Subpath exports / tree-shake friendly
Ecosystem integrations
Dedicated documentation site
this site
Product surface
Discord community
Product surface
Admin dashboard UI (out of the box)
OpenAPI + dashboard package planned
Product surface
SaaS starter kit / boilerplate
create-lasagna-saas planned
Product surface
Book / paid course
Product surface

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:doctor with ten built-in checks, a --fix flag for auto-recovery, --json for CI gates, and --watch for 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 against limit=10 produce exactly ten successes and forty QuotaExceededException. No race window.
  • SSO replay protection. OIDC state is consumed via atomic GETDEL; two concurrent callbacks with the same state can never both succeed.
  • Audit log immutability. tenant_audit_logs carries Postgres triggers that block UPDATE, DELETE, and TRUNCATE from inside the tenant's own schema.
  • Header-vs-domain hijack. customDomain({ strict: true }) rejects a request whose x-tenant-id disagrees with the custom-domain match — 400 E_TENANT_HEADER_DOMAIN_MISMATCH, not a silent override.
  • Rate-limit fails closed. Redis down means 503, never silent fail-open. Opt into failOpen: true only if your threat model accepts it.
  • Identifier injection. Every user-supplied identifier that reaches DDL passes assertSafeIdentifier. An architectural test fails CI if any future rawQuery interpolates a template variable without going through that helper.
  • Doctor checks against real state. long_running_queries, replica_lag, queue_stuck run 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.

Was this page helpful?

Released under the MIT License.