SkillPilotAI

Multi-tenancy and Data Isolation

Run many customers (tenants) on the same software/infra, while keeping each tenant's data and performance separate from and safe from the others.

63.1 Why is it used?

  • In SaaS (like Shopify, Zoho, Freshworks) thousands of businesses use one platform. A separate setup for each would be expensive.
  • Data leak is the biggest risk: if Tenant A sees Tenant B's orders, the business is finished.
  • Noisy neighbour: if one big tenant runs a heavy query, everyone else should not slow down.
  • Big enterprise clients ask for a separate DB/region for compliance. The architecture has to be flexible.
  • The right model lowers cost per tenant and makes onboarding happen in minutes.

63.2 How it works

Three models: Silo vs Pool vs Bridge
  1. 1.Silo (DB per tenant): separate DBs for tenants A, B, C. Strongest isolation, highest cost. Best: enterprise, bank, health
  2. 2.Pool (shared + tenant_id): one shared table, tenant_id = A / B / C on each row. Isolation depends on RLS, lowest cost. Best: small SMBs, free tier
  3. 3.Bridge (schema per tenant): 1 DB server, a separate schema per tenant (a, b, c). Medium isolation, cost in between. Best: a mixed customer base

A tiered approach is common in the real world: free/basic plans in the pool, premium plans in silo

One request in the pool model (B2B billing SaaS, like Zoho/Shopify)
  1. 1.Shopkeeper t42 logs in: tenant_id = t42 in the JWT
  2. 2.API Gateway: per-tenant rate limit. t99's 100x sale traffic -> 429 throttle, other tenants normal
  3. 3.App service: SET app.tenant_id = 't42' in the DB session
  4. 4.PostgreSQL / Citus: RLS ON, non-owner role. From the orders table, after the RLS filter only t42's rows are visible (t17's and t99's rows are hidden)
  5. 5.tenant_id everywhere: logs, metrics, billing, cache key (t42:order:9)

Even if you forget WHERE in SELECT * FROM orders, the policy USING (tenant_id = current_setting('app.tenant_id')) returns only t42's rows

Hybrid: pool and silo together
  1. 1.50,000 small tenants -> Pool: a Citus cluster, tenant_id as shard key + RLS
  2. 2.20 enterprise / bank -> Silo: a separate DB + K8s namespace (ResourceQuota) or a separate AWS account
  • Silo model: a separate DB/stack per tenant (or a separate account). Best isolation, but higher cost and ops.
  • Pool model: all tenants in the same DB/tables, with a tenant_id column in every row. Cheap, but isolation depends on app logic.
  • Bridge model: the middle path. A shared DB server with a separate schema per tenant; or small tenants in the pool and big ones in silo.
  • Row-Level Security (RLS): the DB itself adds the tenant_id filter to every query. Even if a developer forgets WHERE, there is no leak.
  • Tenant-aware sharding: make tenant_id the shard key. One tenant's data sits on one shard, and a big tenant gets a dedicated shard.
  • Noisy neighbour control: per-tenant rate limits, quotas, resource limits and moving a big tenant to a separate pool.
  • Tenant context: tenant_id in the login token (JWT), a tenant tag on every request/log/metric. For both debugging and billing.
  • Kubernetes namespaces: compute isolation with a namespace per tenant + ResourceQuota + NetworkPolicy.

63.3 Tools you can use

ToolTypeHow it is used
PostgreSQL Row-Level SecurityDB featureCREATE POLICY tenant_iso ON orders USING (tenant_id = current_setting('app.tenant_id')::uuid); set the tenant in every session.
Separate DB per tenant (RDS / Azure SQL elastic pools)Silo storageEach tenant has its own database; in an Azure SQL elastic pool many DBs share one resource pool.
Citus (distributed PostgreSQL)Tenant-aware shardingDistribute tables on tenant_id; one tenant's queries go to a single node.
Kubernetes NamespacesCompute isolationCPU/memory and network are separated with a per-tenant namespace, ResourceQuota, LimitRange and NetworkPolicy.
AWS IAM + separate accounts (AWS Organizations)Account-level siloPremium tenants get a separate AWS account. Blast radius and billing are both separate.

63.4 Comparison: Silo vs Pool vs Bridge model

PointSilo (DB per tenant)Pool (shared tables + tenant_id)Bridge (schema per tenant / mixed)
IsolationStrongestWeakest, depends on RLS/app logicMedium
Cost per tenantHighestLowestIn between
Noisy neighbourAlmost noneHighest riskLow, but possible on a shared server
Schema migrationRun separately on every DB, slowOnce, easyRun on every schema
Number of tenantsPractical up to hundredsUp to lakhsUp to thousands
Per-tenant backup/restoreEasyHard, rows have to be filteredPossible at schema level
Best forEnterprise, regulated (bank, health)Small SMB tenants, free tierA mixed customer base

63.5 When to choose which

  • Silo: big enterprise customers, strict compliance, or those who ask for a separate region/data residency.
  • Pool + PostgreSQL RLS: thousands of small tenants, low cost, fast onboarding.
  • Bridge/tiered: free/basic plans in the pool, premium plans in silo. The most common real-world approach.
  • Citus/sharding by tenant_id: when the pool model's data does not fit in a single DB.
  • Kubernetes namespace per tenant: when you also want to isolate compute but do not want the cost of a separate cluster.

63.6 Real-world example

Scenario: a B2B SaaS billing platform (like Zoho/Shopify).

  1. The platform has 50,000 small shopkeepers and 20 big enterprise clients.
  2. Small tenants are in the pool model: one PostgreSQL (Citus) cluster, tenant_id in every table, RLS policy enabled.
  3. A request comes in, tenant_id is taken from the JWT, and SET app.tenant_id = '...' is run on the DB session.
  4. Even if a developer forgets to add WHERE tenant_id, RLS returns only that tenant's rows.
  5. One tenant sends 100x traffic in a sale; the per-tenant rate limit throttles it, and the other tenants stay normal.
  6. A big bank client takes a separate DB and a separate Kubernetes namespace (or a separate account) for compliance. This is silo.
  7. For billing, every metric/log is tagged with tenant_id: you know exactly how much resource each tenant used.

63.7 Interview tips

  • Trusting only WHERE tenant_id in app code: one bug and there is a data leak. Add DB-level RLS.
  • Not putting tenant_id in cache keys: Redis ends up serving another tenant's data.
  • Connecting the app as a superuser/table owner: RLS gets bypassed in PostgreSQL (use FORCE ROW LEVEL SECURITY or a non-owner role).
  • Interview tip: be sure to explain the silo vs pool vs bridge trade-off (cost vs isolation) and the noisy neighbour solution.