Connect by JBRH Open Connect

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 What this means
Audience
both, developer
Last verified
Product version
6.3.2

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#

ConcernShared schemaSchema eachDatabase each
Cost per additional customerNear zeroSmall but realSubstantial
A schema changeOnceOnce per customerOnce per customer, across instances
Blast radius of a query bugEveryone, unless enforced below the applicationOne schema, if the search path is rightOne customer
Restoring one customer to yesterdayHard — the rows are interleavedFeasibleStraightforward
Placing one customer's data in a regionNot possible without splittingNot possible on one instanceStraightforward
Noisy neighbourShared resourcesShared resourcesIsolated
Answering a question across all customersOne queryA unionA 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 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.
  • Regional placement of one customer's data. Where data lives is a property of the deployment, not of the row. See 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.