- Programming Language: Go 1.26.0
- Container: Docker Compose
- Database: PostgreSQL 18 (single shared database)
In shared database, shared schema multi-tenancy, every tenant's data lives in
the same tables, distinguished by a tenant_id column. It's the cheapest,
densest model — one schema, one connection pool, thousands of tenants — but it
lives or dies by isolation: one missing WHERE tenant_id = ... and a tenant
sees another's data.
This project enforces isolation in the database with PostgreSQL Row-Level
Security (RLS) instead of trusting every query. A policy ties each row to the
session's app.current_tenant setting, so even a query with no tenant filter only
ever returns the current tenant's rows.
- Tenant discriminator — a
tenant_idcolumn on shared tables. - Row-Level Security —
USING(reads) andWITH CHECK(writes) policies. - Session context —
set_config('app.current_tenant', ...)per transaction. - Non-superuser role — the app connects as
app_userso RLS actually applies. - Fail-closed — no tenant set ⇒ the query errors, it does not leak everything.
app sets app.current_tenant = 2 (per transaction, via app_user role)
│
▼
┌──────────── saas_db (one schema) ────────────┐
│ documents(id, tenant_id, title, body, ...) │
│ │
│ RLS policy tenant_isolation: │
│ USING tenant_id = current tenant │
│ WITH CHECK tenant_id = current tenant │
└──────────────────────────────────────────────┘
every query is auto-filtered to tenant 2's rows only
docker compose up -d
docker compose ps # wait until healthy
go mod tidy
go run main.goEach tenant sees only its own documents. A cross-tenant read returns 0 rows
(RLS hides it), a forged cross-tenant insert is rejected by WITH CHECK, and a
query run with no tenant set errors out instead of leaking all rows.
| Model | Isolation | Density | Ops cost | This project |
|---|---|---|---|---|
| Shared DB, shared schema | Logical (RLS) | Highest | Lowest | ✅ here |
| Shared DB, separate schema | Medium | Medium | Medium | — |
| Separate DB per tenant | Strongest | Lowest | Highest | — |
Shared schema gives the best density and lowest cost; RLS buys back much of the isolation you'd otherwise get from physical separation.
docker compose down -v