Every agency I’ve seen drifts into products the same way. You build something for a client, it’s good. A second client wants something close to it. By the fourth build you realise you’ve been rebuilding the same app with a different logo each time.
So you decide to do it properly — one product, many customers. That’s the day you walk into multi-tenancy, whether anyone said the word or not.
I’ll skip the textbook definition. The one thing worth taking seriously before you write a line of code is this: multi-tenancy is the decision that is genuinely painful to reverse.
You can swap frameworks, rewrite the frontend, or change hosting providers. What you cannot easily do is go back and re-thread the concept of “which customer does this row belong to” through a database that already contains live client data.
I spent years in payments. A row leaking from one party to another isn’t a bug report — it’s a regulator’s afternoon. That instinct is the one thing I carry into even the smallest agency product: decide tenancy first. Almost everything else stays cheap to change.
A tenant is simply one customer of your product and everything that belongs to them — their users, data, settings, and configuration. Multi-tenancy is the art of letting many of them share the same system while each feels like it’s theirs alone.
Shared, or Separate?
Strip away the jargon and every architectural choice comes down to the same question, asked repeatedly at the database, application servers, cache, and search layers: Do tenants share this resource, or get their own copy?
| Model | Isolation | Cost & Complexity | Best For | Risk Level | Reversibility |
|---|---|---|---|---|---|
| Silo | Everything separate | High | Large/regulated enterprise clients | Very low | Easy |
| Pool | Shared + logic | Low | Many small clients | High (if done poorly) | Very hard |
| Hybrid | Mix of both | Medium | Most growing agencies | Medium | Medium |
Most products that survive long enough end up in the hybrid model: share the cheap layers aggressively, isolate the expensive-to-leak ones, and let different clients sit in different places depending on what they pay.
The mistake is thinking you have to pick one model globally. You don’t.
Where It Actually Gets Decided: The Data
Sharing servers is easy. Data is where it bites.
Database per tenant (Silo)
The cleanest option. One bad migration only affects one client. Backups and data export when a client leaves are trivial. Compliance conversations are straightforward.
The cost is operational: every schema change must fan out across every database. At a few hundred tenants you need automation, or you’ll spend your life running migrations.
Shared tables with tenant_id (Pool)
The cheap option — and the right default for an agency selling to lots of small businesses.
One set of tables. A tenant_id column on everything. Every query filters on it.
Here’s the part I’d tattoo on a junior developer: the day someone forgets that filter, you’ve handed one client’s data to another.
That’s not a glitch you patch quietly. Under UK GDPR (and similar data protection laws in most jurisdictions), it can become a notifiable breach.
flowchart TD
A[Incoming Request] --> B[Tenant Resolver<br/>subdomain / custom domain / API key]
B --> C[Application Data Layer<br/>Automatically injects tenant filter]
C --> D[Postgres with Row-Level Security]
D --> E[Only returns rows<br/>belonging to this tenant]
style C fill:#e0f2fe,stroke:#0369a1
style D fill:#fef3c7,stroke:#b45309
The reliable way is to make forgetting the filter impossible:
- Build a data layer that automatically injects the tenant filter.
- Back it with Postgres Row-Level Security (RLS) so the database itself refuses to return another tenant’s rows even if your code forgets.
Belt and braces.
The leaks that actually happen
The dangerous leaks almost never come from an obvious SELECT *. They come from:
- A cache that forgot to include the tenant in its key
- A nightly export job that ran without tenant scoping
- A search index that was never designed with customer isolation in mind
Scope those too, or they will find you.
A near-miss story: Early in my payments career we once caught (in staging) a reporting job that was about to email every merchant a CSV containing all transactions from the previous month — not just their own. The tenant_id filter had been accidentally dropped during a refactor. It never reached production, but it was close enough to make the point stick: you must make the safe path the only easy path.
The Bit That’s Specific to Agencies
A generic SaaS company can get away with yourapp.com/acme. You usually can’t — because your clients hired an agency precisely so the product would feel like theirs.
That makes custom domains (e.g. book.theirshop.co.uk pointing at your platform with automatic certificates) less of a nice-to-have and more of the reason they’re paying you a premium.
Build support for it early. Retrofitting domain handling later is fiddly and expensive.
The same applies to branding. Logos, colours, and custom wording should be treated as tenant data, not code you redeploy. When done right, onboarding a new client’s look and feel becomes someone filling in a form instead of you doing a release.
Onboarding and Commercial Reality
How does client number 50 actually come into existence?
If the answer is “I run a checklist by hand,” you’ve quietly capped your growth at the limit of your own attention. Even a rough automated “create tenant” flow — config, subdomain, API keys, defaults — turns onboarding from a small project into a non-event.
The commercial trick, once you have a mix of clients, is to let pricing and isolation match each other:
- Keep your many small clients in the pool (this protects your margin).
- Offer a dedicated setup (own database, sometimes own deployment) as the premium tier for clients who are big enough or regulated enough to pay for it.
The cleanest way to grow into this is to start everyone in the pool and build the ability to lift a single heavy client out into their own silo when they’re worth it. Starting fully siloed and trying to consolidate later is much harder.
flowchart LR
subgraph Small Clients
direction TB
P1[Client A] --> Pool[Shared Pool<br/>tenant_id + RLS]
P2[Client B] --> Pool
P3[Client C] --> Pool
end
subgraph Enterprise Clients
direction TB
S1[Client X] --> Silo1[Own Database]
S2[Client Y] --> Silo2[Own Database]
end
Pool -.->|Lift when ready| Silo1
What I’d Actually Do
If you’re an agency turning client work into a product for small businesses, here’s the practical path:
- Start pooled — shared tables with
tenant_idon everything. Nothing else makes financial sense at those price points. - Enforce scoping in the data layer and again in the database (Postgres RLS). A forgotten filter should be impossible.
- Resolve the tenant in one place at the edge — whether that’s a subdomain, custom domain, or an API key embedded in a widget on the client’s own site.
- Treat all branding and limits as tenant data so you’re not redeploying to change a colour.
- Design the system so you can move a single big client onto their own database later without rewriting the application. That escape hatch is your future premium tier.
The thing I would avoid above all is putting the decision off.
Tenancy is the one piece you cannot comfortably bolt on once real customers and their data are already in the system. “We’ll sort it properly after launch” is how an afternoon’s design choice becomes a year of careful, risky migrations.
Spend the thinking now.
Future you — the one who isn’t explaining a data leak to a nervous client — will be grateful.