When building QeakFlow — a privacy-first, GDPR-compliant email marketing SaaS — one architectural decision became surprisingly difficult: how much tenant isolation does a SaaS actually need?
The conventional answer is straightforward: one application, one database, and a tenant_id on every relevant table.
I chose a different approach. Each QeakFlow workspace runs in its own container with its own PostgreSQL database.
This gives us a much stronger isolation boundary, but it also introduces infrastructure costs and operational complexity that a conventional multi-tenant architecture avoids.
This is why I chose it, how the architecture works, and where the approach starts to hurt.
Why hard tenant isolation?
QeakFlow is an email marketing platform, which means every tenant can potentially store subscriber PII and its own SMTP credentials.
That made data isolation more important than it would be for many other SaaS products.
The architecture looks roughly like this:
There is no shared application database containing all subscriber data with a tenant_id column. Each tenant has its own isolated application instance and database.
- A smaller blast radius for data-access bugs In a traditional multi-tenant database, every query needs to respect tenant boundaries:
SELECT *
FROM subscribers
WHERE tenant_id = :tenant_id;
That seems simple until you have complex joins, background jobs, exports, reporting queries, migrations, and administrative tooling. One missing tenant filter can potentially expose another customer's data.
Separate databases remove this particular class of application-level isolation bug. They don't eliminate every possible security problem, but they make accidental cross-tenant queries much harder.
Simpler tenant deletion
With separate databases, deleting a tenant's application data has a very clear boundary. Instead of finding every table containing that tenant's records, the tenant's database can eventually be removed entirely. That makes the data lifecycle easier to reason about.Resource isolation
Email marketing can create surprisingly uneven workloads. One customer might import 100,000 contacts while another sends a small newsletter once a week.
With isolated containers, CPU and memory limits can be applied independently. For example:
Tenant A -> CPU: 0.5 | RAM: 256 MB
Tenant B -> CPU: 0.5 | RAM: 256 MB
Tenant C -> CPU: 0.5 | RAM: 256 MB
A large import from Tenant A doesn't automatically consume all resources available to Tenant B.
- SMTP isolation QeakFlow uses a BYO-SMTP model. Each customer connects their own email delivery provider.
That means background workers can maintain SMTP connections, process bounces, send campaigns, and handle provider-specific events. If something goes wrong with one tenant's SMTP connection or worker, the failure is contained within that tenant's environment rather than taking down a shared worker responsible for everyone.
The infrastructure
The current architecture is deliberately simple.
The control plane manages tenants, billing, provisioning, and configuration. Redis is used as the orchestration backbone for things such as distributed locks, container state, and background coordination.
The actual tenant workloads run separately. The infrastructure currently uses:
Docker & cgroups (for resource boundaries)
PostgreSQL (isolated databases)
Redis (orchestration state)
Prometheus & cAdvisor (visibility into container health)
Ubuntu 24.04 LTS (bare-metal host environment)
The goal isn't to build a Kubernetes cluster for a handful of customers. The goal is to keep the isolation boundary while keeping the infrastructure understandable enough to operate as a small team.
The downside: memory
This is where the architecture becomes expensive.
Every isolated application instance has a baseline memory footprint. In our testing, approximately:
10 idle tenants → ~1.5 GB RAM
100 idle tenants → ~15 GB RAM
Those numbers are only a rough measurement from our current setup, not a universal Docker or Listmonk requirement. But the underlying problem is real: the cost of isolation scales with the number of tenants.
With a shared application instance, adding the 100th tenant doesn't require another application process. With dedicated containers, it does. At small scale this is acceptable. At larger scale, it becomes one of the main architectural constraints.
Database migrations become more complicated
A shared database makes schema migrations relatively straightforward:
1 Migration ---> 1 Shared Database
With isolated databases:
1 Migration ---> Tenant A DB
---> Tenant B DB
---> Tenant C DB
---> Tenant D DB
Every tenant database needs to be upgraded. That means the provisioning and migration system needs to understand database versions and handle failures safely.
A migration that succeeds for 99 tenants but fails for tenant 100 isn't just a technical inconvenience. It becomes an operational state that needs to be tracked and recovered. This is one of the biggest trade-offs of the architecture.
Orchestration becomes part of your application
Once every tenant has its own container, you need to answer questions that don't exist in a simple shared application:
When should a container start?
What happens when it crashes?
How do you detect an unhealthy instance?
How do you update all instances?
Can an idle tenant be stopped?
How do you provision a new tenant safely?
What happens if provisioning succeeds halfway?
How do you recover from a failed migration?
At that point, you're no longer just building a SaaS application — you're also building a small orchestration layer.
For QeakFlow, Redis became particularly useful here. It provides coordination between provisioning workers and helps prevent two workers from modifying the same tenant at the same time.
Does separate infrastructure make the system more secure?
Not automatically. This distinction is important.
Dedicated databases eliminate some risks associated with shared multi-tenant databases, but they introduce other attack surfaces. You still have to secure:
The control plane API
Container networking
Database credentials
Provisioning logic
Secrets management
Reverse proxy configuration
Host-level access
A separate database doesn't magically make a SaaS GDPR-compliant. It simply gives you a stronger technical isolation boundary to build on.
Why not just use a shared database?
For many SaaS products, I think shared multi-tenancy is the right answer. It's cheaper, easier to operate, easier to migrate, and easier to monitor. Mature multi-tenant applications can provide strong isolation with the right testing, authorization layers, and database constraints.
If I were building a generic project-management SaaS, I probably wouldn't choose a dedicated database and container for every tenant.
Email marketing is a somewhat different case. The data includes subscriber information, campaigns, and potentially SMTP credentials. The workloads can also be unpredictable. That makes stronger isolation more valuable.
What I would do differently at larger scale
I don't think this architecture is the final answer for thousands of tenants. At some point, the operational overhead would probably outweigh the benefits.
A more scalable architecture could use different isolation levels depending on the tenant tier:
Small / Free tenants → Shared application infrastructure
Larger / Sensitive tenants → Dedicated database
Enterprise tenants → Dedicated application + database
In other words, tenant isolation doesn't have to be binary. You can choose the isolation boundary based on workload, sensitivity, and business requirements.
So, was it worth it?
At the current stage of QeakFlow, I think so. We're trading infrastructure efficiency for a simpler security boundary and stronger workload isolation.
The cost is real: more memory, more containers, more databases, more migrations, and more orchestration.
But the architecture also makes an important property of the system explicit: one tenant's data and workload should not casually become another tenant's problem.
For a few tenants, dedicated infrastructure is probably overkill. For highly sensitive workloads, it can be a reasonable trade. For thousands of small tenants, I'd expect a hybrid approach to win.
How are you handling tenant isolation in your SaaS? Did you stick with shared multi-tenancy, separate databases, dedicated containers, or some hybrid approach?
Andrii is building QeakFlow — a privacy-first email marketing platform with per-tenant isolation and BYO-SMTP. Early access at
qeakflow.com

Top comments (5)
Per-user databases with bring-your-own SMTP is a refreshingly honest architecture — congrats on shipping.
Quick public check of qeakflow.com (headers + public config only):
TLS 1.3, canonical tag and response time (365ms) are all in good shape. Happy to re-run the scan free once the headers land. Good luck with the launch!
Thanks a lot for taking the time to inspect qeakflow.com and for the precise feedback! I'm working on adding those security headers and fixing the robots.txt / sitemap.xml redirects right now. Will push the update as soon as possible.
Appreciate the support on the launch!
Happy to hear it — glad the check was useful.
One tip while you're in there: on Vercel you can set all five headers in a single headers() block in next.config.js (or a headers array in vercel.json) so they apply to every route, credential pages included — no per-route wiring.
And for CSP specifically: ship it as Content-Security-Policy-Report-Only first, watch the violation reports for a day, then flip to enforcing. Saves you from white-screening your own login page over a missed domain.
Ping me here once it's deployed and I'll re-run the check — the before/after deltas are the satisfying part.
Thanks again for the pro-tip! I've applied the headers globally in next.config.ts and fixed robots.txt / sitemap.xml to return direct 200 OK responses.
Everything is live now - feel free to re-run the scan whenever you have a moment. Appreciate the help!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.