Multi-tenancy in saas-kit
How saas-kit isolates organization data via row-level filtering + AsyncLocalStorage context, with an ESLint rule that catches accidental cross-tenant reads at lint time.
Every SaaS hits the multi-tenant wall eventually. saas-kit picks shared DB, row-level isolation by organization_id — the standard for 95% of SaaS — and enforces the boundary at four layers.
Layer 1 — URL routing
Path-based /[orgSlug]/... routing keeps local dev simple (no wildcard subdomains). The Next 16 proxy resolves orgSlug → organizationId once per request and forwards it as a header.
Layer 2 — AsyncLocalStorage
@saas-kit/context wraps each request in withRequestContext. Repos read orgId from this context — no caller ever passes it explicitly.
Layer 3 — withOrg repo helper
Every repo function in @saas-kit/db/repos calls withOrg() which reads the ALS context and injects WHERE organization_id = $orgId into the Drizzle query.
Layer 4 — ESLint rule
@saas-kit/eslint-rules/no-raw-tenant-query is red on any direct db.query(...) outside whitelisted files. Cross-tenant code lives in *.cross-tenant.ts files (admin actions, webhook handlers).
The cross-tenant leak test in M2's CI suite proves it: as user A in org X, every repo function returns TenancyError or 404 when reading org Y's rows.