Blog

Key takeaways:
Most writing on multi tenant architecture opens with three database diagrams: a database per tenant, a schema per tenant, and one shared table with a tenant column. Each arrives with a list of pros and cons, and you are left to pick one.
That framing skips the two decisions that actually cause trouble. It never asks what a tenant is in your product, and it never says how the isolation gets enforced once you have chosen a diagram.
The tenant definition and the enforcement mechanism are two of the four decisions this guide works through, in the order you have to make them and numbered in the headings below. The cost arithmetic is written out, and so are the specific places tenant data leaks.
The examples lean on PostgreSQL because it is the database we reach for most at VAULT, and the reasoning carries to any relational database. Most of it applies to any multi tenant SaaS architecture, whatever the stack underneath.
Multi-tenant architecture is a design where one running instance of an application serves many separate customers, each seeing only their own data and configuration. The alternative, a separate deployment per customer, is called single-tenant. A multi tenant application architecture therefore has to answer a question a single-tenant one never asks: which customer is this request for?
The hard part arrives immediately. Before you can isolate tenants, you have to say what a tenant is, and Microsoft's architecture guidance is unusually direct about how much that varies.
In a business-to-business product you will usually map a tenant to a customer organization. Then the exceptions show up. A customer with divisions in three countries may need three tenants for data residency reasons. A customer who wants a sandbox separate from production needs two. A single tenant normally holds many users, so tenant and user are different concepts that a surprising amount of code conflates.
Three mappings, and only the last one is about users.

Consumer products are messier still, because a household, a club or a shared account may be the real unit of data ownership rather than the individual.
A marketplace makes the point sharply, since both sides of the transaction are tenants with different visibility over the same order. We built Leaf Trade as a two-sided wholesale marketplace, which puts buyers and sellers in one data model with asymmetric rights over the same records.
That is a structural choice, and it is far cheaper to settle before the first table exists than after. Asymmetric visibility is one of the things that makes building a marketplace harder than building either side of it alone.
Write your definition down in one sentence. If you cannot, the schema argument that follows will go in circles.
Most articles describe three database layouts. Microsoft's Azure Architecture Center describes four deployment models instead, and the distinction is more useful because it separates which tenants share from which components are shared.
| Model | What is shared | Isolation | Cost efficiency | Fits when |
|---|---|---|---|---|
| Automated single-tenant | Nothing, each tenant gets its own deployment | Highest | Lowest | Heavy regulatory requirements, few large tenants |
| Fully multitenant | Everything, one deployment for all tenants | Lowest | Highest | Many small tenants, uniform requirements |
| Vertically partitioned | Some tenants share, others get dedicated deployments | Mixed by tenant | Medium | A mostly shared product with a few exceptional customers |
| Horizontally partitioned | Shared application, dedicated databases per tenant | Data isolated, compute shared | Medium to high | Shared compute is fine but one tenant's data volume dominates |
Horizontal partitioning is the one teams rediscover late. If you find that a single component carries most of the load or most of the sensitivity, you can split just that component per tenant and keep sharing the rest.
Vertical partitioning is the pressure valve. It lets you run one shared product for most customers while giving a dedicated deployment to the one bank that will not sign otherwise, which beats rebuilding for everyone.
Both are available regardless of what you chose on day one, which is the argument against over-engineering the first version. The choice between serverless versus Kubernetes turns on the same question one layer down: buy the isolation a customer is paying for, not the isolation that sounds safest.
Isolation is a purchase, and the price is rarely stated. Microsoft's guidance states it: if a single tenant requires a specific infrastructure cost, 100 tenants probably require 100 times that cost.
That number is the whole argument. A dedicated deployment per tenant gives you clean data isolation, no cross-tenant performance interference, and the option to roll out changes one tenant at a time. It also multiplies your infrastructure bill by your customer count and multiplies your maintenance work by the same factor.
Shared infrastructure inverts both. One deployment costs roughly one deployment even as tenants arrive, and higher tiers to absorb the extra load still come in far below per-tenant provisioning.
That holds until it does not. Microsoft is explicit that there is a limit to how far a single deployment scales and that the costs of scaling can rise nonlinearly, giving the example of a shared database whose throughput you exhaust and then pay increasingly more to raise. The shared model is cheaper per tenant across the range most products live in, not at every volume.
Set the two multiples next to each other and the trade is plain.

So the question is not which model is better in the abstract. It is how much isolation your customers are actually paying for. A product at $200 a month per customer cannot fund a dedicated database cluster each, and a product at $200,000 a year per customer probably can, which is the same unit-economics reasoning that belongs in any honest software development cost estimate.
Database-per-tenant reads as the safe default, and the objection to it is operational rather than architectural. It does not fail on day one. It fails on the day you have to change something everywhere at once.
Microsoft names the cost directly in its guidance on the fully isolated model: ongoing maintenance such as applying new configuration or software updates can be time consuming, and you have to plan separately for how you query and manipulate data across every deployment at once. Reporting and analytics across the fleet stop being a query and become a project.
None of that is impossible. Fan-out migrations and infrastructure as code handle it, provided you have the people and the tooling to run them. The trade is the durable part: per-tenant databases buy a one-time design simplification with permanent operational overhead.
There is also a structural catch. You still need somewhere to keep the data that is not tenant-scoped, including your own user directory, tenant configuration, billing records and feature flags. So a database-per-tenant design is really a database-per-tenant plus one shared database, and that shared database is where cross-tenant bugs concentrate.
Choosing a model tells you where tenant data sits. It does not stop a query reading the wrong row. That takes an enforcement mechanism, and where you put it decides whether it survives contact with a growing team.
The simplest multi tenant database architecture is a shared schema where every tenant-scoped table carries a tenant identifier, and every query filters on it. This works and it is fragile, because correctness now depends on every developer remembering a WHERE clause forever.
Filtering in application code means one forgotten predicate is one data leak. That is not a hypothetical failure mode, and it is the one we find most often in multi-tenant code we inherit, because the query looks entirely normal in review.
PostgreSQL can enforce the filter in the database instead. Enable row security on a table and, per the PostgreSQL documentation, all normal access for selecting or modifying rows must be allowed by a policy. If no policy exists, a default-deny applies and no rows are visible at all.
Default-deny is exactly the behavior you want, since a new table is closed until someone opens it deliberately. Then read the next sentence in the same documentation, which is where teams get caught.
Table owners normally bypass row security, and superusers or roles holding the BYPASSRLS attribute always bypass it. Applications are commonly wired up to connect to PostgreSQL as the user that owns the tables, which means a team can enable row-level security, see no errors, and have no enforcement whatsoever.
The same table, three ways, and only the role at the bottom changes anything.

The fix is one statement. ALTER TABLE ... FORCE ROW LEVEL SECURITY makes the owner subject to its own policies, and the more durable arrangement is to have the application connect as a role that neither owns the tables nor carries BYPASSRLS. Either way, verify it by trying to read another tenant's row as your application's real database user, not as yourself in a terminal.
Database policies need to know which tenant is asking. The usual approach is to set a session variable at the start of each request from the authenticated session, then write policies that compare the row's tenant against it.
Two rules make this hold. The tenant must come from the server-side session or token rather than anything the client can set, and the variable must be set in the same place connections are handed out, so no code path can skip it. Connection pooling is the thing to watch, because a pooled connection carrying the previous request's tenant is a leak with no bug in your query at all.
Tenant isolation failures cluster away from the main request path. The list below is where we look first when reviewing a multi-tenant codebase, and each one is a route where the tenant context quietly goes missing.
TRUNCATE and REFERENCES from row security entirely.The last one overlaps with ordinary object-ownership checks rather than tenancy as such, and both belong in the same review, which is why broken access control tops the current OWASP ranking and why we treat it as the first thing to test in any web application security pass.
Every one of those routes runs outside the place the tenant filter lives.

Test for these by writing a case that authenticates as tenant A and asserts it cannot reach a record belonging to tenant B, then run it against every one of those paths rather than only the API.
Shared infrastructure means one tenant's bad afternoon can become everybody's. Microsoft's guidance names this the noisy neighbor problem and treats avoiding it as a benefit of dedicated deployments rather than something you fix later.
In a shared model you manage it instead of eliminating it. Per-tenant rate limits on your API keep one integration from consuming the whole budget. Query timeouts stop a pathological report from holding connections. Moving expensive asynchronous work onto separate queues per tenant class keeps a bulk import from delaying everyone's notifications.
Horizontal partitioning is the structural answer when management is not enough. If one tenant's data volume dominates, give that component its own database and leave the shared application in place.
Microsoft's own closing instruction on tenancy models is the part to take seriously: whichever model you choose, test that one tenant's data is not leaked to another and that any noisy neighbor outcomes are acceptable. Both of those are testable claims, so treat them as acceptance criteria rather than intentions.
You cannot price a product whose per-customer cost you cannot see. In a fully shared deployment, the infrastructure bill arrives as one number, and working out which tenants caused it takes deliberate instrumentation.
Tag resources by tenant where the platform allows it, and record per-tenant counters for the things that actually drive cost: storage consumed, rows written, API calls, compute minutes of background work, and outbound data. Store those alongside your billing data rather than in a monitoring tool that rolls old detail off after a few weeks.
This matters earlier than most teams expect, because the moment you move from one flat price to tiers or usage-based pricing, every tier boundary is an argument about cost you now need evidence for.
Charging money is what promotes these from engineering preferences to commercial constraints. A no-code SaaS reaches that day with less instrumentation available to answer the question than a custom build has.
Schema changes and new tenants are the two routine operations that multi-tenancy complicates, and both reward being automated early. Onboarding especially, because a manual process that works at ten customers is a bottleneck at fifty.
Step six is the one that gets skipped, and it is the one a customer contract or a privacy request will eventually demand. If any of this is further than your current team has gone, that is a normal place to bring in help, and you can contact us to talk it through.
Treat your first choice as reversible, because the usual trigger for change is commercial rather than technical. A large regulated customer arrives, asks for a dedicated database, and your fully shared deployment has no answer.
Vertical partitioning is that answer, and it is cheaper than it sounds. You keep the shared deployment for everyone else and stand up a second deployment for the one tenant, which is why building the tenant identifier into every table from the start matters even if you never plan to split.
One cheap decision today is what leaves both of those moves available.

The practical requirements are the same in both directions. Tenant data has to be extractable as a complete set, your configuration has to be reproducible rather than hand-tuned, and your application has to be able to route a given tenant to a given deployment without a code change.
Architecture decisions that get revisited when a sales conversation changes are really roadmap decisions, so whoever owns product strategy belongs in the room when this one is made.
Leaf Trade is a useful case because the tenancy question was not optional. It is a wholesale marketplace in cannabis supply chain, so a cultivator and a dispensary both hold accounts, both see parts of the same order, and neither may see the other's wider book of business.
We built the storefront, cart, bulk ordering and a REST API on Django and PostgreSQL with Redis caching, using a forked Saleor ecommerce framework with a React frontend, deployed with Docker on AWS Elastic Container Service. The case study records the result as a product that was able to raise $4.5 million in funding.
The tenancy work on a marketplace is the asymmetry rather than the separation. A seller's catalog and pricing are visible to the buyers they sell to and to nobody else, while a single order is a record both sides hold a different view of, so neither party can simply be filtered out of the other's queries.
The detail most relevant here is the data migration. Cutting over took only a few minutes of downtime, and the case study credits that to the work done before the switch: a set of data transfer scripts, procedures for onboarding customers onto the new platform, and a series of test migrations that let the client and their own customers verify their data had transferred correctly.
A tenancy model is what makes that kind of cutover plannable at all, because it determines how cleanly one customer's data can be moved without touching anyone else's. The rest of the architecture it ran on, including the forked ecommerce framework and the caching layer, is in the Leaf Trade build.
Start with the sentence defining your tenant, because it determines everything downstream. Then default to the most shared model your customers' requirements allow, add the tenant identifier to every table whether or not you need it yet, and enforce isolation in the database rather than in application code.
Reach for a dedicated deployment when a specific customer's compliance requirements or data volume demands it, not as a general posture. Vertical and horizontal partitioning exist so that one demanding customer does not set the architecture for all the others.
In practice the choice is driven by one or two concrete pressures rather than a general preference, and it is worth naming which pressure you are responding to.
| What is pushing the decision | Points to | Why |
|---|---|---|
| Many small tenants with similar needs | Fully multitenant | Cost per tenant stays close to flat as customers arrive |
| One customer's contract demands isolation | Vertically partitioned | A dedicated deployment for that tenant, shared for everyone else |
| One tenant's data volume dominates | Horizontally partitioned | Split the database carrying the load, keep compute shared |
| Every customer is large and regulated | Automated single-tenant | Isolation is the product requirement, so the cost is the price of sale |
| You genuinely cannot tell yet | Fully multitenant, tenant key on every table | Keeps both partitioning routes open at no extra cost today |
The last row is the useful default. Putting a tenant identifier on every table costs you almost nothing now and is the single thing that makes the other four rows reachable later.
We spend a lot of time building multi tenant SaaS architectures for startups and growing companies, and the recurring theme is that the schema question is the easy half. The expensive mistakes are a tenant definition nobody wrote down and an isolation rule enforced only by habit, which is the kind of thing a team building their first SaaS product finds out at the worst moment.
One deployment of your application, many customers inside it, and each tenant able to see only its own data and configuration. The opposite arrangement gives every customer a deployment of their own and is called single-tenant.
Most commercial products are built as a multi tenant SaaS architecture because the economics of the alternative do not work below enterprise price points. Microsoft's architecture guidance describes four variants, from fully shared through to automated single-tenant deployments, with partitioned models in between.
Single-tenant gives each customer dedicated infrastructure, which maximizes isolation and removes cross-tenant performance interference. Multi-tenant shares infrastructure, which is far cheaper to run and maintain. The cost difference is close to linear, since nothing is shared, which is why Microsoft's guidance warns that scaling a dedicated-deployment model scales the bill by your customer count.
Default to a shared database with a tenant identifier on every table unless a specific requirement rules it out, because it is the cheapest to operate and the easiest to migrate schemas on. Move to a database per tenant when data volume, compliance or a contractual isolation requirement justifies the operational overhead, and consider doing so for only the tenants that need it rather than all of them.
Filtering in application code works until someone forgets a clause, so enforce it in the database. PostgreSQL's row-level security applies a default-deny policy to any table with row security enabled and no policy defined. The step teams miss is that table owners and roles with the BYPASSRLS attribute bypass these policies entirely, so use FORCE ROW LEVEL SECURITY or connect your application as a non-owning role.
The noisy neighbor problem is one tenant's resource consumption degrading performance for others sharing the same infrastructure. Per-tenant rate limits, query timeouts and separate queues for heavy asynchronous work manage it in a shared model. Where one tenant's load genuinely dominates, horizontally partitioning that component gives it dedicated resources while the rest of the application stays shared.
A tenancy model is one of the few decisions that gets more expensive to change every month you leave it, and it is also one of the easier ones to get right with a second opinion early.
If you are deciding how to structure a product for many customers, or you have inherited one and want to know where it leaks, contact us.