In SaaS and multi-client enterprise systems, there is one incident that must never happen: "Company B's data appearing on Company A's login screen"—a cross-tenant data leak. It instantly becomes a critical information security incident, yet the cause is rarely elaborate: a single line where someone forgot to add tenant_id to the query's WHERE clause. That alone exposes data on the screen that should never have been visible.
An article that tackles this problem head-on is Ensuring Tenant Isolation with Defense in Depth across Application and DB Layers (Zenn / counterworks). The conclusion is straightforward: protect data across both layers, rather than relying solely on the application layer or the database layer alone. When undertaking multi-tenant systems in custom development, we treat this not merely as a "best practice," but as an essential prerequisite where a single failure spells disaster.
Why application-layer filters eventually fail
The foundation of multi-tenant isolation is attaching tenant boundary conditions to every query. Most systems implement this at the application layer using ORM scopes or shared WHERE clauses. Under normal conditions, this works. The problem is that there are countless paths where developers can forget to include them.
- Writing raw SQL for a newly added admin dashboard aggregation without routing through the shared scope
- Batch processing or CSV exports pulling data through a different path than standard APIs
- An ORM
JOINwhere the condition applied to only one of the joined tables - Temporarily removing the boundary condition when adding an "all-company report" feature and forgetting to restore it
Relying on humans to consistently append tenant_id across every single query path becomes unrealistic as a system grows. Application-layer defense protects you as long as it is written correctly, but breaks the moment a single line is omitted. This asymmetry is why cross-tenant leaks never disappear. The structure is very similar to incidents where omitted soft-delete flags expose data that was supposed to be deleted; we previously covered this state model design philosophy in Database Design Moving Away from "Soft Deletes" (GH Media).
Defense in depth — placing the last line of defense in the DB layer
This is where adding an extra layer of defense at the database tier proves effective. In PostgreSQL, Row Level Security (RLS) can be used to ensure that the database itself never returns rows belonging to any tenant other than the current one. Even if the application accidentally omits WHERE, the database blocks rows outside the boundary. This serves as the ultimate last line of defense.
| Layer | Role | Strengths | Weaknesses |
|---|---|---|---|
| Application layer | Appends tenant conditions to standard queries | Flexible, fast, integrated with business logic | Vulnerable to omissions (breaks with a single missing line) |
| DB layer (RLS) | Database blocks rows outside the tenant | Last line of defense that prevents leaks even if omitted in code | Requires connection-level context management and guard against misconfigurations |
The mindset is not "either/or" but "both." The application layer handles everyday correctness and performance, while the database layer serves as an insurance policy that absorbs human mistakes. Think of this as applying defense in depth—a fundamental security principle—to tenant isolation. For broader foundations of web security, Introduction to Website Security Measures (GH Media) is also a useful companion reference.
Implementation in custom development — preventing leaks even when bugs occur
In our custom development work, we design multi-tenant systems not to a standard of "safe if coded correctly," but to a standard where "data does not leak even if mistakes happen." Specifically, we proceed in the following order.
Reliably pass tenant context per connection
Whether RLS functions properly depends on whether the database connection context accurately reflects which tenant is connected on each request. In our custom development, we establish an infrastructure where the tenant context is never lost between authentication and the database session, solidifying this as a shared foundation. In a cloud management system project, we established the tenant once at the request entry point and unified all subsequent database access to run under that context, ensuring that out-of-boundary data would never be returned even if a query condition was omitted.
Make boundary violations detectable via automated tests
Even with defense in depth, you cannot know if configurations are correct without verification. In custom development, we write automated tests verifying that "querying Company B's data under Company A's authentication always returns empty results (or is denied)" and integrate this into CI pass criteria. This mechanically prevents regressions whenever new features or raw SQL queries are added. This philosophy of not stopping at merely running tests aligns directly with Guaranteeing Deliverable Test Quality with Mutation Testing in Custom Development (GH Media).
Architect exception paths, such as company-wide access, into the design
In real-world operations, situations inevitably arise where operational administrators need cross-tenant reports. Removing boundary conditions in an ad hoc manner creates breeding grounds for incidents. Instead, we incorporate exception paths into the architecture from the start, explicitly defining who can perform cross-tenant lookups and under what conditions. Delivering exceptions as legitimate administrative features rather than backdoors is the level of quality we provide in custom development.
Common pitfalls to avoid
First, becoming complacent simply because RLS is enabled. If per-connection tenant context management is flawed, RLS may not work as intended, or it could mistakenly block legitimate access. Configuration verification tests are an indispensable part of the implementation.
Second, removing DB-layer defenses in the name of performance. When paired with proper index design, RLS operates with practical, robust performance. Tearing down the last line of defense for performance reasons is an unfavorable trade-off against the probability of a catastrophic leak. For defining and monitoring baseline normal operations after handover, please refer to Define "Normal" First in Monitoring (GH Media).
Third, misunderstanding this as permission to slack off on application-layer checks. The DB layer is strictly insurance; everyday correctness and performance remain the responsibility of the application layer. Defense in depth assumes that both layers are executed thoroughly.
Summary — moving to an architecture immune to a single omitted line
Cross-tenant data leaks do not arise from sophisticated hacking; they occur from an omitted line in a WHERE clause. Application-layer filters protect you when written correctly, but fail the moment a single mistake is made. That is why you need defense in depth: everyday correctness in the application layer, backed by the database layer (RLS) as an ultimate safeguard that prevents leaks even when bugs occur. When delivering custom development, establishing connection-level tenant context, catching violations with automated tests, and designing exception paths as official features—starting with these three steps is the realistic way to ensure leaks never happen.
If you would like to evaluate whether your multi-tenant SaaS architecture is secure, or if you want to rebuild your system to eliminate cross-tenant leak risks, please reach out via our contact form. We can start by reviewing the isolation mechanisms of your existing system.
Sources
- Ensuring Tenant Isolation with Defense in Depth across Application and DB Layers (Zenn / counterworks)
- Row Security Policies (Official PostgreSQL Documentation)
- Database Design Moving Away from "Soft Deletes" (GH Media)
- Introduction to Website Security Measures (GH Media)
- Guaranteeing Deliverable Test Quality with Mutation Testing in Custom Development (GH Media)
- Define "Normal" First in Monitoring (GH Media)








