sickn33/agentic-awesome-skills/saas-multi-tenant
Claude Code and agent skill by sickn33/agentic-awesome-skills
- community
Checking that the SKILL.md file is still in the repository.
Design and implement multi-tenant SaaS architectures with row-level security, tenant-scoped queries, shared-schema isolation, and safe cross-tenant admin patterns in PostgreSQL and TypeScript.
Install
npx vskill@latest install sickn33/agentic-awesome-skills/saas-multi-tenantSKILL.md
SaaS Multi-Tenant Architecture
When to Use This Skill
- The user is building a SaaS application where multiple customers share the same database
- The user asks about tenant isolation, row-level security, or data leakage prevention
- The user needs to scope every database query to a specific tenant without manual WHERE clauses
- The user asks about shared-schema vs schema-per-tenant vs database-per-tenant tradeoffs
- The user is implementing admin endpoints that must access data across tenants
- The user needs to add
tenant_idcolumns to an existing single-tenant application - The user asks about PostgreSQL RLS policies for tenant isolation
- The user is building tenant-aware middleware in Express, Fastify, or Next.js API routes
Do NOT use this skill when:
- The user is building a single-user application with no shared infrastructure
- The user asks about authentication only without tenant scoping (use an auth skill instead)
- The user needs general database schema design without multi-tenancy requirements
Core Workflow
- Determine the tenancy model. Ask the user about their scale expectations and isolation requirements. For most SaaS apps under 1000 tenants, shared-schema with a
tenant_idcolumn on every table is the correct default. Schema-per-tenant adds operational overhead (migrations run N times). Database-per-tenant can also be justified by isolation, noisy-neighbor, recovery, customer-managed keys or operational needs; compare actual requirements.
- Add
tenant_idto every tenant-scoped table. The column must beNOT NULL, typeUUIDorTEXT, and included in every composite index. Never allow a tenant-scoped table to exist without this column — a missingtenant_idis a data leak waiting to happen.
- Set up PostgreSQL Row-Level Security (RLS). Create a policy on each tenant-scoped table that filters rows by
current_setting('app.current_tenant_id'). This acts as a database-level safety net — even if application code forgets a WHERE clause, RLS blocks cross-tenant reads.
- Build tenant-aware middleware. At the start of every request, extract the
tenant_idfrom the authenticated session or JWT claims. Set it on the database connection usingSET LOCAL app.current_tenant_id = '...'inside a transaction. Every subsequent query in that request inherits the tenant scope automatically.
- Scope all ORM queries by tenant. If using Prisma, apply a global middleware that injects
where: { tenantId }into everyfindMany,findFirst,update, anddeletecall. If using Drizzle, create a base query builder that includes the tenant filter. Never rely on developers remembering to add the filter manually.
- Handle tenant-aware migrations. Every new table migration must include
tenant_idas a column. Write a linting rule or CI check that rejects any migration creating a table withouttenant_idunless the table is explicitly marked as global (e.g.,plans,feature_flags).
- Build cross-tenant admin routes separately. Admin endpoints that aggregate data across tenants must bypass RLS explicitly using
SET LOCAL role = 'admin_bypass'or a dedicated database role. These routes must be protected by a separate admin authentication flow — never reuse tenant user sessions for admin access.
- Implement tenant provisioning. When a new customer signs up, create their tenant record, seed default data (roles, settings, onboarding state), and assign the founding user. Wrap this in a database transaction so partial provisioning never leaves orphan records.
Examples
Example 1: PostgreSQL RLS Policy for Tenant Isolation
Example 2: Explicit Transaction Boundary
Do not commit on the HTTP response finish event: the handler may fail, the client may disconnect, or the response may already report success before COMMIT fails. Use an awaited transaction wrapper and send the response only after commit:
Add request timeout/cancellation and target-specific integration tests before use. Validate the user's current tenant membership server-side before calling it; never trust an unverified tenant claim or routing header.
Example 3: Historical Prisma Middleware Sketch
This incomplete legacy $use example is not an isolation layer. It omits findUnique, upsert, nested operations, relation loads and raw queries, and does not bind a transaction-local RLS context. Current Prisma versions may require extensions instead. Do not deploy it as automatic protection; enumerate every operation, forbid client changes to tenant ownership and execute under a non-owner, non-BYPASSRLS database role with tested policies.
Never Do This
- Never query a tenant-scoped table without a `tenant_id` filter. Even if your ORM middleware handles it, raw SQL queries bypass middleware entirely. Every raw query must include
WHERE tenant_id = $1or rely on RLS. A single unscopedSELECT * FROM invoicesleaks every customer's billing data.
- Never store `tenant_id` only in the application session without enforcing it at the database level. Application-layer filtering is a suggestion. RLS is enforcement. If a bug in your middleware skips the tenant filter, only RLS prevents the data leak. Run both layers.
- Never use auto-incrementing integer IDs for tenant-scoped resources. Sequential IDs (
invoice #1042) let attackers enumerate other tenants' resources by incrementing the ID. Use UUIDs for all tenant-scoped primary keys. Reserve integer IDs for internal-only tables.
- Never let tenant users access admin aggregation endpoints. A route like
GET /admin/metricsthat queries across all tenants must never be reachable with a regular tenant JWT. Use a separate authentication mechanism (API key, admin role claim with a different issuer) for cross-tenant routes.
- Use a separate least-privilege migration role. RLS governs row access, not DDL visibility; ALTER TABLE does not silently alter only one tenant. Test data migrations against every intended tenant and grant elevated privileges only for the required operation, never default to superuser.
- Share pools only with reliable transaction cleanup. If you use
SET LOCAL app.current_tenant_idinside a transaction, that setting is scoped to the transaction. But if a previous request's transaction was not properly committed or rolled back, the connection returns to the pool with stale tenant context. Commit/rollback before release, destroying the connection if rollback fails. SET LOCAL expires at transaction end; RESET does not repair an open or failed transaction.
Edge Cases
- Tenant deletion and data retention. When a tenant cancels their subscription, you cannot simply
DELETE FROM tenants WHERE id = $1. Foreign key cascades may time out on large datasets. Instead, soft-delete the tenant (setdeleted_at), revoke all user sessions, then run a background job that deletes tenant data in batches over hours or days.
- Tenant data export for GDPR/compliance. When a tenant requests a full data export, you need to query every tenant-scoped table for that
tenant_idand package it. Build a registry of all tenant-scoped tables (parse your migration files or maintain a manifest) so the export job doesn't miss tables added after the export feature was built.
- Shared resources between tenants. Some features require shared state — e.g., a marketplace where Tenant A's products are visible to Tenant B's users. These tables need a different RLS policy: read access is public (no tenant filter), but write access is still scoped to the owning tenant. Model these as
owner_tenant_idinstead oftenant_id.
- Tenant-aware background jobs. When a cron job or queue worker processes tasks, there is no HTTP request to extract
tenant_idfrom. The job payload must includetenant_id, and the worker must set the database session variable before processing. Never run background jobs without tenant context — they will either fail on RLS or bypass it entirely.
- Connection pool exhaustion with schema-per-tenant. If you use one PostgreSQL schema per tenant and each schema requires its own connection pool, 500 tenants means 500 pools. This exhausts
max_connectionsfast. Use a connection pooler like PgBouncer in transaction mode, or switch to shared-schema before hitting this wall.
Best Practices
- Create a `tenants` table as the single source of truth. Every
tenant_idforeign key in every table points back totenants.id. Include columns forname,slug(for subdomain routing),plan_id,created_at, anddeleted_at. This table is the root of your entire data model.
- Index `tenant_id` as the first column in every composite index. PostgreSQL uses leftmost prefix matching for composite indexes. An index on
(tenant_id, created_at)serves both "all items for tenant X" and "items for tenant X sorted by date." An index on(created_at, tenant_id)only helps date-range queries across all tenants.
- Use subdomains or path prefixes for tenant routing.
acme.yourapp.comoryourapp.com/org/acme— both work. Map the subdomain or path to atenant_idlookup at the edge (middleware or reverse proxy). This lookup should be cached (Redis or in-memory with 60s TTL) since it runs on every single request.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/saas-multi-tenant/SKILL.md
Popularity
- Repository stars
- 47,054
- Repository forks
- 6,853
- 7-day trend score
- 54
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/sickn33/agentic-awesome-skills/saas-multi-tenant)