Why not just install a copy per customer?
Installing a separate copy per customer looks safest at first: the data is physically separated and nobody can see anyone else's. The problem starts with scale. With five installations a security patch is applied five times; with fifty, fifty — and each starts drifting to a different version. Before long the answer to 'which customer is on which version' becomes a spreadsheet, and that spreadsheet is always out of date.
The second cost is in support. When a bug report arrives you first have to find which installation it came from, which version that installation runs, and whether the bug is fixed in that version. With a single installation none of those three questions exists. The real justification for moving to multi-tenancy is operational cost, not technical elegance.
There are still cases where separate installations are right: contracts where data cannot leave the organisation, sectors requiring network-level isolation, and enormous single customers. That is why we offer our products both as multi-tenant cloud and as a single on-premises installation — but as the same core, not a separate fork.
Tenant resolution: which customer does this request belong to?
The first decision in a multi-tenant system is how you determine which tenant a request belongs to. There are three common routes: from the domain, from the URL path, or from the user's session. All three work, and the choice depends on the product's nature — if the customer wants to appear under their own brand, the domain; for an internal-facing panel, the session.
In our content platform we chose the domain: the incoming address is matched against registered domains and that site's theme, languages and content are loaded. Because even the theme is chosen at runtime, opening a new customer requires no new deployment. In our business product the tenant comes from the user's session, with a hierarchy on top: organisation, brand, branch.
Whichever route you choose, the critical point is preserving that information along the chain. If an intermediate layer replaces the address with its own name, the system opens the wrong tenant — and it does not look like an error: the page loads, only the content is wrong. So the 'unknown tenant' case must be logged rather than falling silently to a default.
Tenant hierarchy: one level is usually not enough
At the start 'tenant' means a single customer; in real life the customer has layers inside. A restaurant chain has brands, each brand has branches; a holding has companies, and companies have divisions. If you build the data model on a single tenant level, when the second branch arrives you either open a new tenant — and cannot combine reports — or change the model.
In our business product we use a three-level hierarchy: organisation, brand, branch. Records attach to the lowest level, permissions can be granted at any level, and reports roll upwards. For a single-location business this looks like extra complexity, but it stays invisible: the user sees only their branch while the hierarchy sits in the background.
The hierarchy decision is one of the most expensive to change later, because it touches every table and every query. So even in the first release you must ask 'what happens if this customer opens a second branch'. If the answer is 'they will not', one level is enough; if it is 'they might', build the hierarchy from the start.
Isolation cannot rest on a single layer
The most critical security property of a multi-tenant system is that one tenant's data is never visible to another. Trying to solve that in one place — say, only a filter in the application layer — is the most common mistake, because a single forgotten query invalidates all of it. Isolation must be layered: row-level security in the database, a mandatory tenant constraint in the application, and an automatic filter in list components.
The third of those layers was added later in our case, and for a concrete reason: an audit found that a list component did not apply the tenant filter automatically and that on a particular endpoint another organisation's data could be seen. The code was not written wrongly; a filter had simply been forgotten in one place. That was the systemic hole: protection depended on a developer remembering.
The principle that follows: security must not be something to remember. Once the tenant filter is the default behaviour, forgetting becomes impossible — going the other way requires deliberate effort. Row-level security in the database is a second net with the same logic: even if the application errs, the database does not return the wrong row.
How do you test isolation?
Isolation testing is negative-scenario testing: you check not that the right user sees the right data but that the wrong user cannot see the wrong data. The practical method is to set up test data with two tenants and try, with one's session, to reach the other's records — both through the interface and directly against the endpoints. The interface is usually clean; leaks happen at the endpoint level.
Doing that test once is not enough; it must be repeated for every new endpoint. Rather than leaving it to human discipline, the healthiest route is to bind it to automated tests: a test per endpoint of the form 'call it with another tenant's id, it must return nothing' is written once and runs on every change. The cheapest form of security is the automated one.
Tenant-specific behaviour: how far do you go?
In a multi-tenant product every customer wants something to be different, and those requests are reasonable. Without a boundary the product fills with 'if customer X then' checks in the code, and before long nobody knows what applies to whom. The right approach is moving differences out of code and into configuration: modules that switch on and off, per-tenant settings, overridable texts and theming.
In our products this takes three forms. In the business product modules switch on per branch — those who do not use them neither pay for nor see them. In the content platform each tenant can change its theme, its languages and more than four hundred interface strings into its own words. In both cases the core is single; only configuration differs.
When a genuine difference cannot be solved by configuration, the decision becomes clear: either the need is generalised into a feature available to everyone, or a separate module is written for that customer. The third option — burying a customer-specific exception in the core — is the easiest in the short term and the most expensive in the long one.
Shared database or a schema per tenant?
There are three ways to separate data in a multi-tenant system: everything in one database with rows distinguished by a tenant id; a separate schema per tenant; a separate database per tenant. All three are real options, and the choice sits at the intersection of customer count and isolation needs. With hundreds of small customers, shared tables are the only sensible route; with ten large institutions, a schema each can make sense.
The advantage of shared tables is operations: one schema, one migration, one backup, one monitoring setup. The disadvantage is that isolation depends entirely on software — which is why row-level security in the database is not a luxury but a second net. With a schema per tenant, isolation is stronger, but every schema change repeats as many times as you have tenants and migration days grow long.
We chose a shared database and strengthened isolation with layers. The deciding point was this: we expect customer numbers to grow and need the operating cost of each new customer to be near zero. In return, for customers whose data cannot leave the organisation we offer a fully separate installation — keeping the third route as a product option.
Restoring a single tenant: the hard part of backups
In a shared database, backing up is easy and restoring is hard. Rolling the whole system back to yesterday is technically simple, but you cannot do it because of one customer's mistake — you would erase a day of every other tenant's data. So in a multi-tenant product, 'restore a single tenant' has to be designed as its own capability.
The practical solution has two layers. First, deletion that is not really deletion: records are marked inactive and physically cleaned after a period. That resolves the vast majority of user mistakes without touching a backup. Second, a tool that can extract and re-insert only that tenant's rows from a backup — writing it when the need arises means writing it at the worst possible time.
When a tenant leaves: exporting and deleting data
What happens when a customer leaves is a flow that must be designed on the product's first day. Three questions need answers: in what format will the data be exported, how long will it be kept, and how will it then be deleted? Writing it into the contract is not enough; there must be a mechanism inside the product, otherwise every departure becomes a manual intervention.
On the deletion side the most common mistake is deleting the record and leaving the traces: the same data lives on in backups, logs, caches and reporting tables. A real deletion policy must cover all of those and have defined periods. When a customer says 'delete my data', the answer you give must be precise.
This flow has a commercial benefit too: a customer who knows leaving is easy decides more comfortably on the way in. That is why we treat data export as a basic capability rather than a feature — what keeps a customer should be satisfaction, not being locked in.
Operations: the responsibility a single installation brings
A single installation lowers maintenance cost while concentrating risk: a bug now affects not one customer but all of them. So in a multi-tenant product the release process must be more disciplined than in a single-customer one — staged rollout, a rollback plan and post-release monitoring are not optional here.
The same concentration applies to performance. One tenant's heavy report can slow down others sharing the same resources. To prevent that you move heavy work into a separate queue, bound queries per tenant and monitor slow endpoints. Behind the complaint 'the system is slow today' there is frequently a single tenant running something unusual.
Conclusion
Multi-tenant architecture is the most economical way to deliver one product to many customers, but its price is discipline: preserving tenant information along the chain, getting the hierarchy right from the start, achieving isolation with three layers rather than one, automating negative-scenario tests, and moving differences into configuration rather than code.
We apply all of these principles in our own products, and we learned one of them the hard way: as long as protection depends on a developer remembering, it is incomplete. If you are planning a product that will open to many customers, discussing the isolation design on day one is the cheapest route.