Multi-Tenant Architecture
In short: A software architecture in which a single application serves several independent customers (“tenants”) at the same time — each with its own data, often separated by their own subdomains.
In more detail: Typical pattern: customer1.my-platform.com, customer2.my-platform.com etc. all run on the same codebase, but with separate data per tenant. This usually needs a wildcard DNS record and a payment solution that can distribute money to several recipients (e.g. Stripe Connect).
Our context: A long-term goal for Emzett — the shop platform is later meant to be “rented out” to others, each customer on their own subdomain. According to the roadmap, the last and biggest expansion step.
In Depth
Three degrees of isolation
In multi-tenant systems, the central design decision is HOW the data of the different tenants is kept apart — from “loose” to “strict” there are roughly three common approaches:
- Shared tables with a
tenant_idcolumn — all tenants live in the same tables, every row carries a tenant identifier, and every query additionally filters by it. The easiest to operate (one database, one schema, one connection pool), but a forgottenWHERE tenant_id = ...filter can mix data between customers — a serious security risk that in practice is often guarded against with row-level security policies at the database level (Postgres supports this natively: a policy ensures that even a forgotten WHERE clause in the application code returns no foreign rows, because the database filters by itself). - One schema per tenant — the same database, but each tenant gets its own PostgreSQL schema (namespace, e.g.
tenant_customer1.ordersinstead ofpublic.orders). Better isolation than approach 1 (a badly written query can’t accidentally access another schema without explicitly addressing it), but migrations have to run for each schema separately — a noticeable operational overhead with hundreds of tenants. - One database per tenant — maximum isolation (a database error or a performance spike at tenant A doesn’t affect tenant B at all), but the most effort to operate, especially with many small tenants: every database needs its own backups, its own monitoring, its own migrations, and the number of open connections grows linearly with the number of tenants.
Many larger SaaS platforms (e.g. Slack in its early architecture) start with approach 1 and move towards approach 2 or 3 as customers grow and compliance requirements increase (some enterprise customers contractually require physical data separation), often only for the largest customers, while smaller ones keep sharing approach 1 (a “hybrid” model).
Subdomain routing
The subdomain detection itself usually happens in the middleware/proxy layer: the request’s Host header is parsed, the tenant is derived from it, and this context is then passed on to all downstream database queries (e.g. via a request context or a variable set by the middleware, which is then “passed through” the entire request handling via AsyncLocalStorage in Node.js, for example, without having to hand it explicitly to every function).
// Simplified pattern in the middleware/proxy layer
const host = request.headers.get("host"); // e.g. "customer1.my-platform.com"
const tenant = host?.split(".")[0];For wildcard subdomains, the DNS zone needs a matching record (*.my-platform.com → the same server IP), and the TLS certificate also has to be a wildcard certificate (or automatic certificate provisioning per subdomain, as Vercel offers for custom domains), otherwise visitors get a certificate warning.
Payment distribution
For a platform that itself takes money from end customers and has to pass it on to the respective tenants (marketplace model), a normal Stripe integration isn’t enough — that’s what Stripe Connect is for: each tenant gets its own (linked) Stripe account, and payments can be routed directly or via the platform with a commission split (“application fee”), without the platform itself having to act as the legal payee for all tenant revenue.
See also: DNS records