Structuring a Multi-Tenant SaaS Database: Patterns and Trade-offs
Your first ten customers won't expose bad database architecture. Your first hundred will. By the time a noisy tenant is hammering your shared database and slowing down everyone else's dashboards, the conversation about schema strategy is already overdue.
Multi-tenancy is not a feature — it is a foundational architectural decision, and getting it wrong is expensive to reverse. This post cuts through the theory and examines the three dominant strategies with the kind of specificity that actually helps you make a call: shared schema, schema-per-tenant, and database-per-tenant.
Why Multi-Tenancy Is Harder Than It Looks
At its core, multi-tenancy means one running instance of your application serves multiple customers (tenants), while keeping their data logically or physically separated. The challenge is that "separated" can mean very different things depending on your compliance requirements, customer expectations, and infrastructure budget.
African SaaS founders in particular face a constraint that most US-centric architecture guides quietly ignore: cloud costs are real and margin is thin. A strategy that works for a well-funded startup in San Francisco — spinning up a dedicated RDS instance per customer — can be economically catastrophic when you are serving SMEs across Lagos, Accra, or Nairobi on lean pricing.
Let's examine each strategy honestly.
Strategy 1: Shared Schema (Row-Level Tenancy)
This is the most common starting point. Every tenant's data lives in the same tables, distinguished by a tenant_id column.
-- Example: shared invoices table
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
amount NUMERIC(12, 2),
status TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_invoices_tenant ON invoices(tenant_id);
Advantages:
- Operationally simple. One schema to migrate, one schema to back up.
- Low infrastructure cost — a single PostgreSQL instance serves all tenants.
- Easy to build cross-tenant analytics if you ever need it.
Trade-offs:
- Every query must filter by
tenant_id. Miss that filter once in application code, and you have a data leak. This is a real security surface, not a hypothetical. - A single heavy tenant can degrade performance for everyone — the "noisy neighbour" problem.
- Row-level security (RLS) in PostgreSQL can enforce tenant isolation at the database level, which is a significant mitigation, but adds configuration complexity.
Best fit: Early-stage SaaS, B2SMB products, or situations where tenants are low-trust and relatively uniform in data volume. This is where most African SaaS products should start.
Strategy 2: Schema-Per-Tenant
PostgreSQL supports multiple schemas within a single database. In this model, each tenant gets their own schema — tenant_abc.invoices, tenant_xyz.invoices — while all schemas live in one PostgreSQL instance.
Advantages:
- Strong logical isolation. A misconfigured query is far less likely to cross tenant boundaries.
- Easier per-tenant migrations if tenants need customisation.
- No
tenant_idcolumn needed — the schema itself is the isolation boundary. - Still one database server, so infrastructure costs remain manageable.
Trade-offs:
- Schema migrations become complex fast. Rolling out a column change to 500 tenants means 500
ALTER TABLEstatements — ideally automated, but still a meaningful operational burden. - PostgreSQL connection pooling (via PgBouncer) gets more nuanced because
search_pathmust be set per connection. - PostgreSQL has soft limits around schema counts; at several thousand tenants, you will start to feel it.
Best fit: Mid-stage SaaS with a growing customer base that includes enterprise accounts requiring better isolation guarantees, but where you cannot yet justify the cost of dedicated databases. This is a strong sweet spot for many African B2B SaaS products serving fintechs, logistics companies, or healthcare providers who have basic compliance requirements.
Strategy 3: Database-Per-Tenant
Each tenant gets a fully dedicated PostgreSQL instance. Full stop.
Advantages:
- Maximum isolation — a tenant's database failure, corruption, or security incident is entirely contained.
- Simplest query logic; no tenant scoping needed.
- Tenants can be on different database versions, regions, or backup schedules.
- Easiest to meet strict data residency requirements.
Trade-offs:
- Cost scales linearly with tenant count. At 100 tenants, you are managing 100 database instances. Provisioning, monitoring, patching — everything multiplies.
- Operationally intense. You need strong automation (Terraform, Ansible, or a custom control plane) or this becomes unmanageable.
- Cross-tenant reporting requires a data warehouse or ETL pipeline, which is another system to maintain.
Best fit: Enterprise SaaS with large contracts that justify per-tenant infrastructure costs, or products in highly regulated industries (banking, healthcare, government) where data residency is a hard requirement. For most early and mid-stage African SaaS teams, this is a future destination, not a starting point.
Choosing Based on Your Stage and Budget
Here is a practical decision map:
| Factor | Shared Schema | Schema-Per-Tenant | DB-Per-Tenant |
|---|---|---|---|
| Infrastructure cost | Low | Low–Medium | High |
| Isolation strength | Low | Medium | High |
| Operational complexity | Low | Medium | High |
| Migration complexity | Low | High | Medium |
| Compliance suitability | Low | Medium | High |
A common and sensible evolution path is: start shared schema → migrate to schema-per-tenant as you approach enterprise sales → offer DB-per-tenant only to strategic accounts who pay for it.
That last point deserves emphasis. Database-per-tenant does not have to be your default — it can be a premium tier. Many successful SaaS companies charge enterprise customers a meaningful uplift specifically to offset the infrastructure cost of dedicated tenancy. That is not a workaround; it is correct product pricing.
PostgreSQL-Specific Considerations
If you are building on PostgreSQL — which you should be, for most use cases — a few things are worth knowing regardless of which strategy you choose:
- Row-Level Security (RLS): PostgreSQL's native RLS is underused. In a shared schema setup, enabling RLS and attaching a policy that enforces
tenant_id = current_setting('app.tenant_id')gives you a database-enforced safety net, not just an application-layer promise. - Connection pooling: PgBouncer in transaction mode is almost always necessary at scale. Plan for it early.
- Partitioning: In a shared schema with very high row counts, range or list partitioning by
tenant_idcan recover query performance without requiring a schema or database split.
Why This Matters for Your Project
The database strategy you choose in month three will still be with you in year three — migration is painful and risky. If you are building a SaaS product today, the right call is almost always to start simple (shared schema with RLS), instrument your performance data carefully, and design your application layer so that the abstraction between "tenant context" and "database query" is clean enough to swap strategies later. That architectural discipline — not the strategy itself — is what gives you room to scale.
At Code!nk Technologies, this is the kind of decision we work through with founders before a single line of application code is written. Getting the data layer right is not a backend detail; it is a business decision.




