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.
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
Multi-tenancy and tenant isolation
Most business apps serve many customer companies from one system. So every permission check needs one more question: whose data is this? Your goal: ask "which tenant?" on every request, so Maya never reads a rival company's invoices.
- A tenant is one customer company in a shared product.
- Take the tenant from the verified token, never from the URL or body.
- Put the tenant filter on every query. Add row-level security as a backstop.
- A tenant admin has power only inside their own tenant.
- Test with two tenants on every release.
What is a tenant?
A tenant is one customer company inside your shared product. It has its own people, roles and data. When many tenants share one system, we call it multi-tenancy. The wall between tenants is made of your code.
one apartment building with many flats. All families share the roof. Each family has a locked door. A permission check is that lock.
Where the tenant comes from
The tenant must come from something the caller cannot change. That is the verified token or the server-side session. The sign-in system puts a tenant claim in the token, such as tenant: "acme". Your API checks the signature first, then reads the tenant.
Never trust a tenant from a URL or request body. The caller types those. Maya could write ?tenant=globex in one second. If they disagree with the token, reject the call.
The filter on every query
In a shared database, all tenants' rows sit in one table. A tenant_id column tells them apart. Every read and every write must use it. This is the tenant filter. The same holds for permission data (see Go deeper).
Cross-tenant BOLA
You will meet BOLA (broken object level authorization) in the OWASP tour. Here it crosses the tenant line, so it is cross-tenant access. The code looks up an invoice by id only, with no tenant filter.
Maya works at Acme. Her invoice link ends in 101. She tries /invoices/205. Her token is valid, so the code returns the invoice. It belongs to Globex. No check asked whose invoice it was.
Row-level security: the database backstop
Row-level security (RLS) lets the database apply the filter itself. The app sets the current tenant once per request, from the token. Then a query that forgets the filter still returns only Acme's rows. RLS is a second layer. If you set the tenant from the URL, RLS protects nothing. The PostgreSQL lines are in Go deeper.
Two kinds of admin
A tenant admin manages the people and settings of one tenant. The role counts only inside that tenant. Check the role together with the tenant, as in B2B organizations. A platform admin works for you, the product company, and sits outside every tenant. That power is dangerous. Support staff should reach a tenant's data only through a short grant that the tenant approves, with a log. Never give a permanent "see all" role.
If a token has no tenant claim, do not fall back to "all tenants". Refuse the call. This is fail-closed.
Test for leaks
Write a test with two tenants. Sign in as an Acme user. Ask for Globex objects by id, by list and by write. Every call must fail. Run it on every release.
One missing "which tenant?" can expose every customer at once. Use layers, called defense in depth. The token gives the tenant, the filter applies it, the database enforces it and tests prove it. The gateway cannot do this alone. It does not know who owns an invoice.
Go deeper: RLS in PostgreSQL, pitfalls, and per-tenant keys and login
Here is row-level security in PostgreSQL (version 18 documentation):
| PostgreSQL line | What it says |
|---|---|
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; | From now on, every query on this table must pass a policy. |
CREATE POLICY tenant_only ON invoices USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid); | Show only rows of the current tenant. If no tenant is set, the value is NULL and no rows match. |
SELECT set_config('app.tenant_id', 'a1b2…', true); | The app sets the tenant for this transaction only. |
PostgreSQL's documentation says superusers and roles with the BYPASSRLS attribute always skip row security. Table owners usually skip it too, unless you run ALTER TABLE … FORCE ROW LEVEL SECURITY. So the app should connect as a plain role that owns nothing. If a table has RLS on and no policy, nobody sees any rows. Set the tenant with set_config(…, true) inside the transaction. Then a pooled connection cannot carry one tenant's value into the next request. The NULLIF matters: on a reused connection the setting can be an empty string, and an empty string cannot become a uuid.
The tenant belongs in permission data too. In an OpenFGA style graph, put the organization in the tuples. For example, (org:acme, org, document:report) says the report belongs to Acme (see permissions as a graph). In a policy engine, put the tenant in the rule input (see policy as code).
Some tenants want their own login provider, their own encryption keys, or data in one country. A shared design makes this harder. See B2B organizations for per-tenant login, and multi-tenancy in architecture for silo, pool and bridge designs.
🧪 Interactive lab — enable JavaScript to play with this one.
Does your gateway policy carry the tenant claim? Check it with Gateway JWT Lint. Or talk to our team about a multi-tenant authorization review.