# Multi-tenancy patterns

There are three ways to hold many customers in one product — everyone in the same tables, one schema each, or one database each — and the choice trades isolation against operational cost. None of them is a security control on its own; enforcement is. Connect uses shared tables with a workspace key, and enforces the boundary three separate times.

- **Status:** Reference
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/technology/multi-tenancy/

## Three patterns on one axis

**Shared schema** — One set of tables for everyone, with a customer key on every row. Cheapest to run and to migrate; entirely dependent on the key being applied to every query.
**Schema per customer** — One database, many schemas, identical tables in each. A stronger blast radius, and every schema change now happens as many times as you have customers.
**Database per customer** — Full separation, per-customer restore, and per-customer placement. The strongest boundary and the heaviest operation — connections, upgrades, monitoring and cost all multiply.
**Hybrid** — Most customers pooled, a few separated because a contract or a regulation demands it. Two operational models to maintain rather than one.

The axis is the same in all four: how much do you spend, per customer, to make a mistake impossible rather than merely unlikely?

## What each pattern actually costs

| Concern | Shared schema | Schema each | Database each |
|---|---|---|---|
| Cost per additional customer | Near zero | Small but real | Substantial |
| A schema change | Once | Once per customer | Once per customer, across instances |
| Blast radius of a query bug | Everyone, unless enforced below the application | One schema, if the search path is right | One customer |
| Restoring one customer to yesterday | Hard — the rows are interleaved | Feasible | Straightforward |
| Placing one customer's data in a region | Not possible without splitting | Not possible on one instance | Straightforward |
| Noisy neighbour | Shared resources | Shared resources | Isolated |
| Answering a question across all customers | One query | A union | A pipeline |

Two rows in that table decide most real choices, and neither is about security. Per-customer restore and per-customer placement are the requirements that force separation; everything else can usually be bought more cheaply with enforcement.

## The pattern is not the control

A separate schema per customer with an application that sets the wrong search path is less safe than shared tables with the boundary enforced in the database. Separation makes a leak require a mistake in the *right* place; it does not make one impossible, and it tends to encourage the belief that the hard part is done.

What actually decides safety is whether the boundary is enforced somewhere a forgetful query cannot get past, and whether it is enforced more than once by mechanisms that fail differently. See [row-level security](/docs/technology/row-level-security/) for the layer that does the work in a shared schema.

## Does Connect use multi-tenancy, and which pattern?

**Shared schema, keyed on `workspaces`, with the boundary enforced three times.** Nearly every table carries a workspace reference and is filtered automatically; a short, enumerated list of control-plane tables — platform identity, sessions, billing, and enquiries addressed to the operator itself — is the deliberate exception.

The three enforcement points are an allowlist in the middleware, the ORM kernel that filters every query, and row-level security in the database. The customer-facing API is a separate facade: a customer session reaches `/api/customer/...` paths, and a request the rewriting layer does not recognise becomes a blocked path answered 403 rather than a path that happens to work.

The operator's own workspace is a workspace like any other, which is a useful property rather than a detail: there is one feature set over one implementation, and the difference between the operator and a customer is commercial — a customer's use is bounded by plan allowances. Parity is not asserted, it is enumerated: a test reads the route table out of the running application and compares every operator capability against the customer surface, with a list of known gaps that may shrink and must never grow.

## What a shared schema makes genuinely harder

- **Restoring one customer.** Their rows are interleaved with everyone's, so a point-in-time restore of one workspace is a data operation rather than a file operation. See [backup and restore](/docs/technology/backup-restore/).
- **Regional placement of one customer's data.** Where data lives is a property of the deployment, not of the row. See [data residency](/docs/technology/data-residency/).
- **Resource isolation.** One workspace running an unusually heavy job competes with everyone else, so query cost is an operational discipline rather than a per-customer concern — which is why statement counts here were driven down to constants rather than left proportional to workspace size.
- **Deleting everything for one customer.** It has to be a deliberate, complete operation across every scoped table, rather than dropping a database.

Naming these is more useful than defending the choice. A shared schema is the right default for a product where every workspace wants the same features and none of them has contracted for its own database, and it is the wrong one the moment somebody has.

## Questions

### Is one database per customer safer?

It has a smaller blast radius and it is not automatically safer. Safety comes from enforcement that a forgetful query cannot bypass. A shared schema with forced row-level security and an independent query-building layer above it closes the common failure; separate databases mostly relocate it.

### Can one workspace ever see another's data?

Three independent mechanisms would have to fail together: the path allowlist, the query kernel and the database policies. They fail differently by design, which is the entire reason there are three rather than one.

### Does the operator's workspace have extra features?

No. It is a workspace like the others, over the same implementation. Three things are correctly operator-only because they are about running the platform rather than using it: verifying customer payments, the operator's own pricing, and enquiries addressed to the operator.

## Related

- [Multi-tenancy in Connect](https://connectbyjbrh.com/docs/security/multi-tenancy/)
- [Row-level security](https://connectbyjbrh.com/docs/technology/row-level-security/)
- [PostgreSQL](https://connectbyjbrh.com/docs/technology/postgresql/)
- [Workspace isolation](https://connectbyjbrh.com/docs/security/workspace-isolation/)
- [The customer facade](https://connectbyjbrh.com/docs/security/customer-facade/)
- [Data residency](https://connectbyjbrh.com/docs/technology/data-residency/)
- [Backups and restore](https://connectbyjbrh.com/docs/technology/backup-restore/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §1 to §3 — audiences, request flow, data model
- `docs-source/sources/GENERAL.md` §11 — measured query-cost improvements
- Connect capability registry (docs-source/facts.py)
