System Design
Intermediate 13 min read Reliability, security & operations

Auth, permissions and multi-tenancy

In one line

Authentication proves who is calling, authorisation decides what they may do, and multi-tenancy makes sure one customer can never see another's data; design all three at the gateway, the service and the database, not in one place.

What it is

Authentication (authN) answers 'who are you?'. Users log in with a password, OTP, passkey or a social or enterprise identity provider; services prove their identity with mTLS certificates or signed tokens. The result is usually a short-lived token, such as a JWT access token, or a session ID stored server-side.

Authorisation (authZ) answers 'what may you do?'. Common models: RBAC (roles like admin, editor, viewer), ABAC (rules on attributes: 'managers can approve expenses under Rs 50,000 in their own department'), and ReBAC (relationships: 'you can view a document if you are a member of a team that owns its folder'), popularised by Google's Zanzibar.

Multi-tenancy means one system serves many customer organisations (tenants). Isolation can be silo (separate database or stack per tenant), pool (shared tables with a tenant_id column) or bridge (shared app, separate schemas or databases for large tenants). Most SaaS products pool small tenants for cost and move large or regulated ones into silos. The core rule: tenant ID comes from the authenticated identity, never from a request parameter, and it is enforced at every layer.

Why interviewers ask

Almost every design has users and permissions, and B2B designs (Slack, Notion, a CRM, an AI assistant over company documents) are multi-tenant by nature. Interviewers check whether you separate authN from authZ, understand the JWT revocation trade-off, can model sharing permissions efficiently, and prevent cross-tenant leaks in caches, search indexes and vector stores. A data leak between tenants is the kind of bug that ends companies, so defence in depth impresses.

How it works

  1. 1Use a standard identity layer: OAuth 2.0 for delegated access and OpenID Connect (OIDC) for login, through an identity provider such as Keycloak, Auth0, Cognito or your own service. For enterprise tenants support SAML or OIDC single sign-on.
  2. 2Issue short-lived access tokens (5-15 minutes) and longer refresh tokens (days to weeks) that are rotated on each use and stored securely. Mobile apps use the authorisation code flow with PKCE.
  3. 3Verify tokens at the API gateway (signature, expiry, audience, issuer) using the provider's public keys (JWKS), and pass the verified identity (user_id, tenant_id, roles) to services. Services still check permissions for the specific resource.
  4. 4Understand the JWT trade-off: verification is stateless and fast, but a stolen or revoked token stays valid until it expires. Keep expiry short, keep a small deny-list for emergencies, or use opaque tokens checked against a session store when instant revocation is required.
  5. 5Authenticate service-to-service calls too: mTLS inside a service mesh (Istio, Linkerd, SPIFFE identities) or signed service tokens, so a compromised pod cannot call everything.
  6. 6Model authorisation for the product: RBAC for simple admin features; ReBAC or ABAC for sharing (documents, folders, projects). Centralise policy in a service or library (OpenFGA, SpiceDB, Open Policy Agent, Cedar) so rules are consistent and auditable.
  7. 7Carry tenant_id in every row, every cache key, every search and vector index filter, every object storage path and every log line. Partition or shard large tables by tenant_id.
  8. 8Enforce tenant isolation in the database as a second line of defence, for example Postgres row-level security with a per-transaction tenant setting, and an app role that is not the table owner.
  9. 9Limit noisy neighbours: per-tenant rate limits, quotas, connection limits and LLM token budgets; move very large tenants to dedicated shards or silos.
  10. 10Audit: log every permission change and sensitive access with who, what, when and tenant; enterprise customers will ask for these logs.

Worked example · Request path in a multi-tenant document AI assistant

  1. Scale: 8,000 tenants, 2 million users, 40 million documents, 3,000 API requests per second at peak.
  2. Login: an employee of tenant 'acme' signs in through Acme's Azure AD with OIDC. The identity service issues a 10-minute JWT with sub=u_91, tid=acme, roles=[member] and a refresh token.
  3. The app calls GET /docs/d_555. The gateway verifies the JWT signature with cached JWKS keys (under 1 ms, no network call) and forwards user u_91 and tenant acme.
  4. The document service asks the authorisation service: can u_91 view doc d_555? The ReBAC store finds d_555 is in folder f_12, which is shared with team t_3, of which u_91 is a member: allowed. With caching, p95 check time is about 5 ms.
  5. The database query runs inside a transaction with SET LOCAL app.tenant_id = 'acme'. Even if a bug drops the WHERE tenant_id clause, row-level security returns zero rows from other tenants.
  6. The user asks the AI assistant a question. The retrieval step filters the vector search by tenant_id = acme and by the document IDs the user can read, so the LLM never sees text from another tenant or from documents this user cannot open.
  7. An admin removes u_91 from team t_3. The relationship is deleted; the next permission check fails. The JWT is still valid for up to 10 minutes, but it carries only identity, not document permissions, so access to d_555 ends immediately.

Picture it

An office building. Authentication is the security desk checking your ID and giving you a visitor badge that expires tonight. Authorisation is the access rules on each door: your badge opens your floor and meeting rooms booked by your team, not the server room. Multi-tenancy is a building shared by several companies: even if a door lock fails, each company keeps its files in its own locked cabinets, so one company's visitor never reads another's papers.

Reference code

-- Every tenant table carries tenant_id and has RLS enforced.
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY;  -- applies to the table owner too

CREATE POLICY tenant_isolation ON documents
  USING (tenant_id = current_setting('app.tenant_id')::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

-- Per request, inside the transaction (app connects as a non-owner role):
BEGIN;
SET LOCAL app.tenant_id = '6f1c2a9e-0000-4000-8000-000000000001';
SELECT id, title FROM documents WHERE folder_id = $1;  -- only this tenant's rows
COMMIT;

Key patterns

Gateway authN, service authZ

When: Microservice systems

How: Verify tokens once at the edge and pass a trusted identity; each service checks resource-level permissions with a shared policy service or library.

Defence-in-depth tenant isolation

When: Pooled multi-tenant data

How: Tenant from the token, tenant filter in queries, database row-level security, tenant-scoped caches and indexes, and automated cross-tenant tests.

Tiered tenancy

When: Customers of very different sizes

How: Pool small tenants on shared shards; move large or regulated tenants to dedicated shards, schemas or stacks with the same code.

Short-lived tokens with refresh rotation

When: Mobile and web clients

How: 10-minute access tokens, rotating refresh tokens detected on reuse, and a revocation path for logout and compromised accounts.

Real-world use

Google Zanzibar

Zanzibar is Google's global authorisation system for Drive, YouTube, Photos and more. It stores relationship tuples like 'doc:readme#viewer@group:eng#member' and answers check requests at very large scale with low latency. Open-source systems such as SpiceDB and OpenFGA follow its model.

Slack and enterprise SaaS

Business SaaS products such as Slack, Atlassian and Salesforce support SSO via SAML or OIDC, user provisioning via SCIM, and per-workspace or per-org data boundaries, because enterprise buyers require them.

Postgres row-level security

Supabase and many SaaS backends use Postgres RLS policies so every query is filtered by the current user or tenant at the database level, as a safety net under application checks.

AWS IAM and Cedar

AWS IAM evaluates attribute-based policies on every API call. AWS open-sourced Cedar, a policy language used by Amazon Verified Permissions, for application-level authorisation.

OTP login in India

Many Indian consumer apps use phone number plus SMS or WhatsApp OTP as the main login. It is convenient, but SIM-swap and OTP-phishing risks mean high-value actions (payments, changing the phone number) need extra checks such as device binding, UPI PIN or step-up verification.

In a project

Your B2B HR platform pooled all 3,000 customer companies in one Postgres database. A support engineer found that an export endpoint took company_id from the query string, and changing it returned another company's employee salaries.

  1. Fix the bug and remove every place where tenant ID comes from client input; derive it only from the verified token.
  2. Add Postgres row-level security on all tenant tables, connect the app with a non-owner role, and set the tenant with SET LOCAL inside each transaction so pooled connections do not leak it.
  3. Write an automated cross-tenant test: log in as tenant A and call every endpoint with tenant B's resource IDs; expect 403 or 404. Run it in CI forever.
  4. Prefix every cache key and object storage path with the tenant ID; scope search and vector indexes by tenant.
  5. Introduce a central authorisation check for HR roles (employee, manager, HR admin) and field-level rules for salary data.
  6. Give the three largest tenants dedicated database shards and per-tenant rate limits, and offer SSO and SCIM for enterprise customers.
  7. Add audit logs for exports and permission changes and notify affected customers as legally required.

Impact: A single missing WHERE clause can no longer leak data, because the database itself enforces tenant boundaries. The CI test catches regressions before release, and enterprise customers get the audit trail and SSO they need to pass their security reviews.

Trade-offs

Use it when

  • Pooled multi-tenancy for many small and medium customers, to share infrastructure cost
  • Silo tenancy for large, regulated or data-residency customers who pay for isolation
  • JWTs for stateless, high-QPS verification across many services
  • ReBAC (Zanzibar-style) when users share resources through folders, teams and links
  • RBAC alone for products with a few fixed roles

Avoid it when

  • Long-lived JWTs (days) when you need fast revocation; use short expiry or opaque tokens
  • Putting fine-grained permissions inside the JWT: they go stale and tokens grow large
  • One database per tenant for thousands of tiny tenants: migrations and connections become unmanageable
  • Enforcing tenant isolation only in application code with no database or index-level safety net
  • Building your own password, OTP and token handling when a proven identity provider fits

Common mistakes

  • Taking tenant_id or user_id from the request body or query string instead of the verified token.
  • Confusing authentication with authorisation, for example 'the user is logged in, so they can open this order'.
  • Caching responses or embeddings without the tenant in the key, so one tenant gets another's data.
  • Storing JWTs in localStorage in web apps exposed to XSS, or never rotating refresh tokens.
  • Checking permissions only in the UI and not on the API.
  • Ignoring noisy neighbours: one tenant's bulk import slows everyone because there are no per-tenant limits.
  • Using plain SET instead of SET LOCAL with RLS and a connection pool.

Interview questions

Quick revision

  • AuthN = who you are; AuthZ = what you may do.
  • OIDC for login, OAuth 2.0 for delegated access, PKCE for mobile and SPA clients.
  • Access tokens 5-15 min, rotating refresh tokens; JWT revocation needs a plan.
  • Verify at the gateway, authorise per resource in services.
  • RBAC for roles, ABAC for attribute rules, ReBAC (Zanzibar) for sharing.
  • Tenant ID only from the token; in every row, key, index, path and log.
  • Postgres RLS with SET LOCAL as a safety net.
  • Pool small tenants, silo big or regulated ones.
  • Per-tenant rate limits and quotas stop noisy neighbours.
  • mTLS or signed tokens for service-to-service.
  • Automated cross-tenant tests in CI.

Check yourself

1. What is the main drawback of long-lived self-contained JWT access tokens?

2. Where should a service get the tenant ID for a request?

3. Which authorisation model fits 'a user can edit a file if they are in a team that owns the parent folder'?

4. Why use SET LOCAL instead of SET for the tenant setting with Postgres RLS and a connection pool?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free