Free & open · no paywall, ever

IntegrAuth Academy

Identity, security and practical AI — free, from zero. Byte-sized lessons, real diagrams, hands-on labs, zero fluff. Two programs, each with its own exam and certificate.

20 tracks 241 lessons 242 hands-on labs 230+ diagrams 100% free

English is hard? Your browser can translate this page into Tamil, Telugu, Kannada, Malayalam or Hindi. Tech words stay in English so you learn the real names.

How?
  • Chrome on phone: ⋮ menu → Translate
  • Chrome on computer: right-click → Translate to …
  • Edge: the translate icon in the address bar
Identity & API Security Track 13 · Identity Architecture
0/166 lessons read
ARCHITECTURE · LESSON 3

Multi-tenancy — keeping customers' data apart

You sell clinic software called ClinicDesk. It keeps appointments, bills and patient notes for many clinics, all on the same servers, database and code. Maya works at one clinic. A rival clinic is a customer too. Your goal: choose a design that keeps each clinic's data apart, at a cost you can pay.

Key points
  • A tenant is one customer organization inside a shared system. Here, one clinic.
  • Silo, pool and bridge trade isolation against cost.
  • Most products mix them: small clinics share, a big hospital gets its own.
  • Stack three layers: tenant claim, automatic row scoping and isolation tests.

What is a tenant?

A tenant is one customer organization inside your shared system. It has its own users, data and settings (see the B2B identity lesson). Maya belongs to Tenant A, a small clinic. The rival belongs to Tenant B.

Multi-tenancy means many tenants share one system. This saves cost, because you patch one system. But the wall between tenants is now made of your code, not of separate machines. This lesson is about the design of that wall; the check on every request is in the multi-tenancy authorization lesson.

Three ways to build the wall

You trade isolation against cost. The three common models are the silo, the pool and the bridge.

🏠 Silo

Each clinic gets its own database, or its own whole stack. Tenant B's rows are not in Tenant A's database, so a leak is very unlikely. The price: with 40 clinics you run, patch and back up 40 copies.

🌊 Pool

All clinics share the same tables. A tenant_id column on every row keeps them apart. This is cheap and easy to grow. But the wall is one WHERE tenant_id = … in every query. Forget it once and data leaks.

🌉 Bridge

A mix. The app and some tables are shared, but sensitive data is split per clinic, in separate schemas or databases. It is stronger than pool and cheaper than silo.

Think of it like…

a cinema. A pool is one big hall. The seat number on your ticket is your tenant_id, and the usher must check it for every person. A silo is a private screening room for each group. A bridge is the big hall plus a locked private box.

Pick by customer, not by habit

You rarely choose one model for everyone. Small clinics share the pool: they pay little and ask little. A hospital chain may demand its own encryption keys, or its data kept in India. Give that customer a silo, and charge for it.

The wall in code

Whatever you pick, every token carries a tenant claim, often named org or tenant_id. Take the tenant from the token, never from a URL. The caller controls those, so Maya could simply write ?tenant=B.

Now the classic pool bug. A developer forgets the tenant filter. Maya asks for /invoice/999, and the code fetches it by id alone and returns a row from Tenant B. This is a cross-tenant BOLA (Broken Object Level Authorization), the top risk in the OWASP API Top 10. In a silo it cannot happen. In a pool, it is one missing clause away.

isolation: strong ← · · · → weaker cost: high ← · · · → lowSilo1 DB per tenant · B's rows don't exist herePoolshared tables + tenant_id · cheap, leakyBridgehybrid · shared app, split dataTenant A (Maya)Tenant B (a stranger)the tenant line — data must never crossGET /invoice/999SELECT * WHERE id=999 (no tenant filter!)Tenant B · invoice #999GET /invoice/999…WHERE id=999 AND tenant_id='A'∅ 0 rows · 404 ✓cross-tenant BOLA — the leak that ends companies
Figure — One forgotten clause crosses the line. The left query fetches by id alone and returns Tenant B's record. The right query adds the tenant filter and returns nothing.

Never rely on one wall. Stack three layers, called defense in depth:

  1. The tenant claim in the token says which organization the request is for.
  2. Row-level scoping adds tenant_id to every read and write automatically, so no human can forget it. A database row-level security rule can do this.
  3. Isolation tests run in your build pipeline. Maya's token asks for Tenant B's objects, and the test expects a 404.

If one layer slips, the next still holds. When a request names an object in another tenant, answer 404 (not found). A 403 (forbidden) would confirm that the object exists.

Go deeper: two things that are not isolation

Random ids such as UUIDs make it harder to guess another tenant's object. OWASP says this only adds to the permission check. It never replaces it.

Hiding things in the screen is not isolation either. If the screen hides Tenant B's bills but the API still returns them, nothing is protected. An attacker never uses your screen. Guard every object, list and write on the server.

Go deeper: row-level security in a database

Some databases can enforce the filter themselves. You set a rule on the table, such as "show only rows where tenant_id equals the current tenant". The app sets the current tenant once per request, from the token. Then even a query that forgets the filter returns only that tenant's rows. Test it: a connection that has no tenant set should return nothing, not everything. The exact rules and pitfalls are in the authorization lesson.

Go deeper: noisy neighbors, and moving a clinic from the pool to a silo

The pool has one more cost: the noisy neighbor. One clinic runs a huge report, and every other clinic slows down. Give each tenant its own limits (see the gateway lesson). In a silo, a noisy clinic only hurts itself.

Sooner or later a big customer asks for its own silo. Plan for it on day one. Keep tenant_id on every table, so one tenant's rows can be copied out cleanly. Route each request to a database by tenant, in one place in the code. Then the move is a copy, a switch and a test, not a rewrite.

Leaving is the same job in reverse. When a clinic cancels, you must be able to export and erase just that clinic's data. In a silo you drop one database. In a pool you delete rows, and backups need a plan too. India's data protection rules are in the rules lesson.

🧪 Interactive lab — enable JavaScript to play with this one.

🧪 Try it free

Cross-tenant object access is a Broken Object Level Authorization flaw. Check your own schema for it with GraphQL Check. Lint the gateway policy that should carry the tenant claim with Gateway JWT Lint.