Multi-Tenant SaaS Architecture: How We Design Data Isolation That Scales
Vishvajeet Shukla · AI & Automation Architect · August 4, 2026
Cover design: Vishvajeet Shukla
FundFlick, our own lending operations SaaS, runs loan origination, collections, HRMS, and reporting for multiple lending businesses on one PostgreSQL-backed codebase. The architecture decision that shapes everything else in that product is a simple question: how does tenant A's data guarantee it never touches tenant B's?
The three real options
There are three standard approaches to multi-tenant data isolation, and each one trades operational complexity against isolation strength:
- Database-per-tenant — strongest isolation, but the operational overhead (migrations × N databases, connection pooling at scale) grows linearly with your customer count.
- Schema-per-tenant — a middle ground: one database, one Postgres schema per tenant. Better isolation than row-level, but migrations still have to run per-schema, and connection/schema-switching logic adds real complexity.
- Row-level isolation — one schema, every tenant-scoped table carries a
tenant_id, and every query is scoped to it. Migrations run once. The tradeoff is that isolation now depends entirely on discipline: every single query has to be scoped, with no exceptions.
Why we chose row-level for FundFlick
At the scale a lending operations SaaS actually runs at — dozens to low hundreds of tenant businesses, not thousands of consumer accounts — the operational cost of database-per-tenant or schema-per-tenant wasn't worth what it bought us. Row-level isolation, done correctly, gives strong-enough isolation with a single set of migrations and a single connection pool to reason about.
"Done correctly" is doing a lot of work in that sentence, so here's what it actually means in practice:
Making row-level isolation actually safe
- No raw queries bypass the scoping layer. Every data-access path goes through a query layer that injects the tenant filter — there's no code path where a developer can "forget" to scope a query, because the unscoped version simply isn't available to call.
- Postgres Row-Level Security as the backstop, not the primary control. Application-layer scoping is the main defense (it's where the business logic already lives), but RLS policies at the database layer mean that even a bug in the application layer can't leak cross-tenant rows — the database itself refuses.
- Every background job and report query gets the same scrutiny as a request handler. Cron jobs and batch reports are exactly where scoping bugs hide, because they don't go through the same request-level review a new endpoint gets.
The result
FundFlick runs loan origination, collection, bookkeeping, HRMS, task management, and reporting for multiple tenant businesses on this single-schema, row-level-plus-RLS design — one migration path, one connection pool, and isolation enforced at two independent layers instead of one.
Multi-tenancy isn't a database schema decision made once at project start. It's a discipline every query has to pass for the life of the product.
If you're scoping a new SaaS product and trying to decide between these three models, the honest framework is: pick based on your actual expected tenant count and team size, not on which one sounds more "enterprise." Row-level with RLS as a backstop has served us well at the scale FundFlick actually operates at.
