Stop 22 · Build Backend Portfolio ProjectsProjects · 9/14

Multi-tenant SaaS API

Part of the Backend Developer Roadmap

In short

Every B2B SaaS, from Zoho to Freshworks, serves many companies from one codebase. The one bug that must never happen: Company A seeing Company B's data.

A backend serving multiple isolated customers (tenants) from one codebase and infrastructure — practice for data isolation strategies and tenant-aware authorization.

The purple blob watchman hands one key from a board of many to a resident.

Think of it like this

One apartment building, many flats

Everyone shares the lift, the water tank and the watchman. But each flat has its own lock, and the watchman never hands flat 302's key to someone from 405. Tenants share the infrastructure, never each other's stuff.

Watch it happen

Every request, tenant-aware

Find tenant
Auth
Membership
Scoped query
Respond

1/5Work out the tenant from the subdomain, like acme.myapp.com, or from the token's org claim.

The big ideas

What you need to know

Choose an isolation model

Shared tables with tenant_id, a schema per tenant, or a database per tenant. More isolation costs more to run.

Enforce it in one place

Middleware or Postgres row-level security, not a WHERE clause each developer must remember.

Roles per tenant

The same person can be admin in one company and viewer in another. Store roles on the membership.

Fair usage

Rate limits and quotas per tenant, so one big customer can't slow everyone else down.

See it in code

Walk through the code

sql
1ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;2 3CREATE POLICY tenant_isolation ON invoices4  USING (tenant_id = current_setting('app.tenant_id')::uuid);5 6-- In each request's transaction, the API sets the tenant first:7BEGIN;8SET LOCAL app.tenant_id = '6f1c2a9e-0b7d-4c1a-9f4e-2d8b3c5a7e10';9SELECT * FROM invoices;   -- only this tenant's rows come back10COMMIT;

1/3Turn on row-level security and add a policy: rows are visible only if tenant_id matches.

From app code, use SELECT set_config('app.tenant_id', $1, true) so the value is a safe query parameter. It lasts only for that transaction.

Save yourself the pain

Common mistakes

Oops

Relying on every developer to add WHERE tenant_id = ? to every query.

Do this instead

Someone will forget once. Enforce it centrally with RLS or a scoped repository layer, and test cross-tenant access.

Oops

Connecting as the table owner and assuming RLS still applies.

Do this instead

Table owners bypass RLS unless you add FORCE ROW LEVEL SECURITY. Use a separate, non-owner app role.

Your mission

Milestone 1: prove isolation

~45 min

Quick check

What's the main advantage of enforcing tenant isolation with Postgres row-level security?

Go deeper

  1. 1Choose and implement a tenant isolation strategy — shared DB with tenant ID, or separate schemas.
  2. 2Ensure every query is scoped to the authenticated user's tenant — no data leaks across tenants.
  3. 3Add tenant-level configuration, e.g. custom settings per customer.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge