Oksa Satya.

July 11, 2026 · 7 min read

Building Multi-Tenant SaaS: Data Isolation Without the Drama

SaaSMulti-tenantPostgreSQLGo

Multi-tenancy is a simple promise with long consequences: many companies use one application, and their data must never be visible to each other — not sometimes, but always. This is the pattern I use in two production systems (Dexova and Helixio), plus the traps I ran into.

Choose the isolation model deliberately

There are three common options: a separate database per tenant, a separate schema per tenant, or one shared schema with a tenant column. For a SaaS with many small-to-mid tenants, a shared schema is almost always the right starting point: cheapest to operate, one migration run, and onboarding a new tenant is practically free.

The price: isolation becomes entirely the responsibility of application discipline. Every query must be scoped to a tenant — and 'every' here really does mean every.

Tenant context you cannot forget

The classic mistake: relying on every developer to remember to add the tenant filter to a query. Forget once, and data leaks. Tenant context must flow from authentication through the entire execution path — in Go, via a context.Context populated by middleware after the token is validated, then read by the data layer.

The principle: the safe path must be the easiest path. A query helper that takes the tenant from context explicitly turns 'forgot the filter' from a silent bug into code that looks obviously wrong at review time.

RBAC on top of isolation, not instead of it

Tenant isolation answers 'which company's data' — it does not yet answer 'who can do what'. In Helixio I use four roles (Owner, Admin, Member, Viewer) per workspace; in Dexova, per-module roles (e.g. cashier vs manager in POS, with access revocation from the dashboard).

They are different layers, and should not be mixed: the tenant check happens in the data layer, the permission check in the use-case layer. Mixing them makes both hard to test.

Traps that only bite later

  • Background jobs: an async job has no request — tenant context must be carried explicitly in the job payload, not pulled from 'the currently logged-in user'.
  • Cache keys: a key without a tenant prefix is a data leak waiting for a schedule. Namespace every key per tenant.
  • Global vs tenant data: reference tables (e.g. bank lists, public holidays) are intentionally global — mark clearly which ones are global so nobody 'fixes' them by adding a tenant column.
  • Exports and reports: the path that most often joins many queries — and the most common place for a single filter to be missed. Isolation testing matters most exactly here.

Summary

Multi-tenancy is not a feature but a property the architecture has to protect: tenant context that flows automatically, a safe path that is the easiest to use, and healthy suspicion toward every path that does not go through a request — jobs, caches, and reports.

Building something similar?

I take on projects and technical consulting around business systems.