# Saas Multi Tenancy

> Tenant isolation and lifecycle for multi-tenant SaaS — data scoping that fails closed, tenant context propagation, noisy neighbors, per-tenant operations. Use when building B2B/B2C SaaS with multiple customers on shared infrastructure, adding tenant-scoped tables or features, debugging cross-tenant data leaks, or when the user says "multi-tenant", "tenant isolation", "SaaS", "workspace", "organization data", or "row-level security".

- Skill: `05-deepak-patidar/saas-multi-tenancy` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 05-deepak-patidar/saas-multi-tenancy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/05-deepak-patidar/saas-multi-tenancy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: 05-deepak-patidar (https://skillmd.com/u/05-deepak-patidar)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/05-deepak-patidar/saas-multi-tenancy

---


# SaaS Multi-Tenancy

In a multi-tenant system, the worst bug class isn't downtime — it's **tenant A seeing tenant B's data**. One leaked invoice ends the customer relationship and possibly the company. Design so that isolation failures are structurally impossible, not just carefully avoided, because "carefully" doesn't survive the 400th endpoint.

## Choosing the isolation model (decide once, explicitly)

- **Shared schema + `tenant_id` column** — the default. Cheapest to operate, easiest to migrate, scales to thousands of tenants. Requires the discipline below.
- **Schema-per-tenant / database-per-tenant** — buy isolation with operational pain (N× migrations, connection management, cost). Justified by: hard compliance walls, wildly divergent tenant sizes, or contractual "your data is physically separate". Don't drift here for comfort.
- Hybrid (big tenants carved out) is legitimate — but only after the shared model measurably strains.

The rest of this skill assumes shared-schema, where the discipline matters most.

## Gate 1: Scoping that fails CLOSED

The core principle: **a forgotten WHERE clause must return nothing, not everything.**

- Two layers, always: application-level scoping (every query goes through a tenant-scoped session/repository that injects `tenant_id` — never hand-written per query) AND database-level enforcement (Postgres RLS with `FORCE`, policies keyed off a per-request setting, app connecting as a role that **cannot bypass RLS** — superusers silently bypass it; verify the role, and boot-guard against misconfiguration).
- `tenant_id NOT NULL` on every tenant table + FK to the tenants table + composite indexes leading with it (database-design). New tenant-scoped table = added to the RLS/policy list in the same migration, enforced by convention *and* a test that scans the schema for unpoliced tables.
- The tenant comes from the **authenticated principal, never from the request** — no `?account_id=` params, no tenant in the POST body, no trusting a JWT claim you didn't issue (threat-model-security Gate 1).

## Gate 2: Context propagation — where leaks actually happen

Request handlers are the easy part. Cross-tenant leaks live in the places that don't have a request:

- **Background jobs**: every enqueued job carries its `tenant_id` explicitly, and the worker re-establishes tenant context before touching data. A worker processing a loop of tenants must reset context per iteration — context bleeding between loop iterations is the classic leak.
- **Caches**: every cache key of tenant-scoped data includes the tenant (system-design). Query caches cleared on login/logout — stale cross-account data in an SPA is this bug wearing a frontend costume.
- **Scheduled/cron jobs, exports, report generators, search indexes, LLM/RAG retrieval** (ai-engineering): each is a query path; each gets the same scoping audit as an endpoint.
- **Logs and error trackers**: tag entries with tenant (great for debugging) but treat aggregated views as cross-tenant data needing admin-level access.

## Gate 3: The two-tenant test — the cheapest breach prevention that exists

In your test suite, permanently: seed tenant A and tenant B, then as A's user attempt to read, update, delete, and *list* B's objects on **every resource type** — expecting 404/empty, never data. Add the list-endpoint case explicitly (unscoped lists leak in bulk). Run it in CI; every new resource joins it. Before any launch, run it live against production-like data (release-readiness Gate 2). This one fixture catches the bug class that code review misses.

## Gate 4: Tenant lifecycle is a feature set, not an afterthought

Design these before the first real customer, because retrofitting them is surgery:
- **Provisioning**: idempotent tenant creation (seed data, defaults, numbering sequences) — one function, tested.
- **Suspension** (unpaid/abusive): a `blocked`/`suspended` flag checked at auth time, gating the whole app — data preserved, access stopped, reversible.
- **Export**: a tenant can get their data out (their invoices, parties, ledgers) in a usable format. Contractually expected; also your data-privacy-compliance obligation.
- **Purge**: hard deletion of a departed tenant — everywhere: rows, files/objects, search indexes, caches, backups-policy documented. An orphaned tenant's data is pure liability. Purge is destructive: confirm, audit, and stage it (soft-delete window → hard purge).

## Gate 5: Tenants are not the same size — plan for the whale

- **Noisy neighbor**: per-tenant rate limits on expensive endpoints, caps/pagination on unbounded queries (that one tenant with 500k products will find your missing LIMIT), and background-job queues that can't be monopolized by one tenant's bulk import (fair scheduling or per-tenant concurrency caps).
- **Observe per-tenant**: your metrics need a tenant dimension on the hot paths — "the API is slow" vs "tenant 4 is running 80% of load" are different incidents (observability-readiness).
- Per-tenant configuration (features, limits, branding) lives in a tenants/settings table read at runtime — never in code branches naming specific customers.

## Cross-tenant admin operations

Your own admin/support panel is deliberately cross-tenant — which makes it the highest-privilege attack surface you own: separate principal type and stronger auth, every cross-tenant read/write written to an append-only audit log (*who viewed which tenant's what, when*), and impersonation (log-in-as-tenant), if built, is time-boxed, visibly flagged in the UI, and audited. Support convenience is how breaches get built with good intentions.

