Database Per Tenant vs Shared Database for CRM SaaS
Database per tenant vs shared database, argued on the four bills you actually pay: single-tenant restores, backup storage, migrations and GDPR erasure.
A customer emails on Tuesday morning. An admin on their side deleted a pipeline of 900 deals on Friday and nobody noticed until the weekly report came out wrong. They want Thursday’s data back, and only theirs. What happens next was decided months earlier, before you had a paying tenant, in a choice most people make in ten minutes: database per tenant vs shared database. In one model this is a twenty-minute job with a blast radius of one customer. In the other it is an afternoon of extraction SQL against a restored copy, while you work out how much of that tenant’s Monday you will destroy to get Thursday back.
We build and sell multi-tenancy modules for Perfex CRM and Concord CRM, plus a multi-tenant CRM platform of our own, so weigh the recommendations accordingly. The argument is not that one model wins. It is that this comparison gets fought on abstract purity - isolation good, sharing cheap - when the decision is settled by four operational bills that arrive whether you budgeted for them or not.
The two models, and the one in between
Shared database. One schema, every tenant’s rows in the same tables, separated by a tenant_id column that every query must carry. The application enforces isolation at query level, usually through a global scope in the ORM. Our own Laravel platforms, including LeadHub, work this way.
Database per tenant. Every tenant gets its own schema, credentials and connection, so isolation is a connection boundary rather than a WHERE clause. The SaaS module for Perfex CRM offers this as one of three options, alongside a shared database and a hybrid mix decided at plan level.
One database, per-tenant table prefixes. The variant nobody lists, and the one shared hosting tends to force on you. The SaaS module for Concord CRM draws exactly that line: single-database with unique table prefixes on shared hosting, isolated database per tenant on dedicated servers. It inherits full isolation’s table-count problems while fitting one database quota.
The four bills you actually pay
One: restoring a single tenant to yesterday
With an isolated database the nightly dump is already per tenant. Restore tenant_417 into a scratch schema, verify it, copy back the affected tables. Nobody else is touched, and you can offer the customer a genuine choice between full rollback and surgical extraction.
With a shared database your dump is the whole platform, so recovering one tenant starts with restoring the entire thing to a scratch instance. A 30 GB dump loading at a realistic 30 MB/s is roughly seventeen minutes before you write a line of SQL, and that is the easy part. Then you pull that tenant’s rows from every table referencing them, in foreign-key order, and merge them into a live database where the same tenant has been working since 09:00. Restoring over production is not available: it rolls back everyone else too.
The mitigation is scheduled per-tenant logical exports. A GDPR Article 20 export producing a full ZIP including uploaded files doubles as a recovery artifact - but it is data, not a one-command restore.
Two: backup storage and the per-database floor
With innodb_file_per_table on, every table costs at least one tablespace, typically 96 to 128 KB even when empty. A CRM schema of around 150 tables is therefore roughly 15 to 20 MB before a single record exists, so 1,000 tenants means 15 to 20 GB of pure structure on disk. The same arithmetic applies to the prefixed-table variant.
The storage number is less alarming than the orchestration number: a thousand databases across thirty days of retention is 30,000 backup objects to name, verify and expire. A shared database gives you one dump, one retention policy and one thing to test - and testing is the point, because a backup you have never restored is a hypothesis. Verifying a file is not truncated with an SQL-statement count is easy once and tedious a thousand times.
The counterpoint is real: monolithic dumps stop being practical past a few hundred gigabytes, and the physical snapshots or binlog recovery you move to are per-server, not per-tenant, so bill one gets worse.
Three: migration runtime across N schemas
A shared database means one ALTER TABLE against the biggest table you own. One operation to plan, one lock window, one outcome.
Isolated schemas turn the same change into N migrations. Eight hundred schemas at 1.5 seconds each is twenty minutes: nothing from a CLI, impossible from a browser request under any sane max_execution_time. That matters for self-hosted PHP products, because browser-driven updaters that run migrations without SSH are what makes shared hosting viable for a one-person SaaS. They cope with a single schema; across hundreds they need a queued, resumable runner that somebody has to write.
Then the failure mode nobody plans for. Tenant 417 fails on a duplicate index left from a manual fix in March, so your fleet sits on two schema versions and application code must tolerate both until you clear it. Every operator running isolated tenancy eventually builds a per-tenant schema-version table and a repair command. Budget the week.
Four: what a GDPR erasure request costs
This is where isolation looks strongest and is partly bluffing.
With an isolated database: DROP DATABASE, remove the file directory, done, provable to a regulator in one line. With a shared database you delete across every table carrying tenant_id, plus the ones people forget - audit logs, login attempts, webhook delivery logs, queued email, uploaded files, any search index outside MySQL.
The catch is that total erasure is the wrong target in both models. Tax law requires invoices and receipts to survive for seven to ten years, so they must not live inside the thing you drop. Our own approach in LeadHub is to have tax receipts deliberately survive tenant erasure through ON DELETE SET NULL. Run isolated databases and you must make that same call deliberately, keeping billing in the platform database rather than the tenant one, or an erasure request becomes an accounting problem.
Backups undercut both models equally, since neither lets you surgically edit last week’s dump. The workable practice is documented retention windows that age data out, disclosed in your data processing agreement: 180 days for audit logs, 30 for login attempts, 90 for webhook events, and a cool-off period before deletion is final.
| Bill | Shared database | Database per tenant |
|---|---|---|
| Restore one tenant | Full restore, then extraction SQL | Restore one small schema |
| Backup storage | One dump, one policy | ~15-20 MB floor per tenant, N jobs to verify |
| Migrations | One ALTER, one outcome | N runs, partial-failure states, resumable runner |
| Erasure | Delete across every tenant-scoped table, log and file | DROP DATABASE, if billing lives elsewhere |
Provisioning is the constraint nobody prices
Isolated tenancy is only self-service if something creates a database at signup: automatic subdomain and database provisioning on cPanel and WHM, or a Plesk REST API integration creating domains, subdomains and databases when a tenant registers, with cron deploying the database behind it. Without one of those hooks, “database per tenant” quietly means a human on every signup, which kills free trials.
Shared hosting adds a hard ceiling: cPanel plans cap databases and MySQL users, often in the tens rather than thousands. That ceiling is the entire reason the prefixed-table variant exists.
What neither model fixes
- Resource isolation. Separate databases on one MySQL server still share the buffer pool, IOPS and CPU, so a tenant importing four million rows degrades everyone either way. Data isolation is not resource isolation until you split servers.
- Missing indexes. Shared tables get blamed for slowness that is actually an absent composite index on
(tenant_id, created_at). Splitting the data hides that for a year, then returns it once per tenant. - Application bugs. A tenant resolver returning the wrong connection leaks as thoroughly as a forgotten
WHERE tenant_id. Isolation moves the boundary; it does not test it for you. - Credential sprawl. N databases means N credential sets and a provisioning account holding
CREATE DATABASE- now the most valuable thing on the server.
The hybrid model: isolation as a plan feature
The version worth arguing for is decided per plan, not per platform. Starter tenants live in shared tables. The tier that pays for it gets its own database. Enterprise brings its own, which is what BYODB support in the Concord module is for: the tenant supplies database credentials at creation and keeps full data custody.
Both of our CRM tenancy modules treat the scheme as an attribute of the tenant rather than the platform - Perfex through a hybrid mix decided at plan level, Concord by making the database scheme a choice at tenant creation and listing it beside subdomain, package and status.
Commercially that is the point. An isolated database stops being a cost absorbed silently across every trial account and becomes a line item you can charge for, plus a one-line answer in a procurement questionnaire. The price is running two models and two backup paths, so if the platform is one person, keep one until revenue justifies the second.
Database per tenant vs shared database: how to decide
- Write the restore SLA first. “One tenant, to any point in the last seven days, within four hours.” Every other decision falls out of that sentence.
- Name your provisioning hook. If you cannot point at a cPanel, WHM or Plesk API call that creates a database on signup, isolated tenancy is not self-service yet.
- Do the migration arithmetic at your twelve-month tenant count, not today’s, then check whether your updater survives it.
- Find out where the invoices live. Inside the tenant database, erasure and statutory retention are in direct conflict, and moving them is only cheap now.
- Price the isolated tier. If nobody pays a premium for it, you are buying operational load and giving it away.
- Default to shared with query-level isolation unless steps 2 and 3 both came back clean.
Start with step one today. Write the restore SLA down, in hours, and put it where support can read it. Almost everyone picks a tenancy model first and discovers afterwards what SLA they can actually meet, which is how a Tuesday morning email turns into an afternoon of extraction SQL.