Skip to content
Back to skills

Design Multi Tenant Saas

ASecurity

Design and review tenant isolation for a SaaS application where all tenants share one PostgreSQL database. Use when adding tenant-owned tables, resolving which tenant a request belongs to, adding Row-Level Security, or checking for cross-tenant data access.

  • 6 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 4, 2026
developmentgosqlfastapiapidatabasesecurity

Works with

  • api

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add aishajv/claude-everything --skill design-multi-tenant-saas --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Design Multi Tenant Saas?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Design Multi Tenant Saas
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aishajv-design-multi-tenant-saas/badge)](https://www.skillsdirectory.com/skills/aishajv-design-multi-tenant-saas)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: design-multi-tenant-saas
description: Design and review tenant isolation for a SaaS application where all tenants share one PostgreSQL database. Use when adding tenant-owned tables, resolving which tenant a request belongs to, adding Row-Level Security, or checking for cross-tenant data access.
---

# Design Multi-Tenant SaaS

Goal: a tenant can never read or change another tenant's rows, even when a query forgets a filter.

## 1. Resolve the Tenant Once per Request

1. Authenticate the user.
2. Read which tenant the request is for, from the subdomain, the URL path, or a header.
3. Check that the user is a member of that tenant. If not, reject the request.
4. Keep the verified `tenant_id` for the rest of the request.

Example: a user who works for `acme` sends `X-Tenant-ID: globex`. Reject the request. The header says which tenant the user wants; it does not prove they belong to it.

## 2. Let PostgreSQL Enforce Isolation

Do not rely on every query remembering `WHERE tenant_id = ...`; one forgotten filter leaks data. Use **Row-Level Security (RLS)**: a rule attached to a table that PostgreSQL applies to every read and write on that table.

**Step A: add the rule to every tenant-owned table, in a migration.**

```sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id')::uuid);
```

- `ENABLE ROW LEVEL SECURITY` turns rules on for the table.
- The policy lets a query see or write a row only when the row's `tenant_id` equals the current request's tenant.
- `current_setting('app.tenant_id')` reads the tenant that the application sets in step B.

**Step B: tell PostgreSQL the tenant at the start of each request's transaction.**

```sql
SELECT set_config('app.tenant_id', :tenant_id, true);
```

`set_config` is a built-in PostgreSQL function that stores a named setting. The final `true` keeps it only until the transaction ends, so a reused (pooled) connection cannot carry one request's tenant into the next. Pass the verified `tenant_id` as a bind parameter. In a FastAPI app, run this in the database session dependency right after the transaction starts.

**Result:** with `app.tenant_id` set to `acme`, `SELECT * FROM orders` returns only `acme` orders, and inserting an order with `globex`'s `tenant_id` fails. If the application forgets step B, PostgreSQL raises an error rather than returning another tenant's rows.

Never connect the application as a superuser or a role with `BYPASSRLS`: both skip RLS.

## 3. Keep References Inside One Tenant

Load a referenced row through a normal query before linking to it. RLS hides other tenants' rows, so a foreign ID fails as "not found".

Optional, for extra safety: include `tenant_id` in foreign keys, such as `FOREIGN KEY (tenant_id, customer_id) REFERENCES customers (tenant_id, id)`, so the database itself rejects links between tenants.

## 4. Test Isolation

Prove that:

- A user who is not a member of a tenant is rejected, even with a forged tenant header.
- With `acme` set, queries return no `globex` rows, and writes with `globex`'s `tenant_id` fail.
- Without a tenant set, queries on tenant-owned tables fail.
- An `acme` row cannot reference a `globex` row.
- A reused database connection never carries the previous request's tenant.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.