CorperaHQ

Engineering

Multi-tenant the honest way: tenant isolation in Mongo

How CorperaHQ's data layer enforces tenant boundaries at the driver level — and why we throw a hard exception on cross-tenant queries instead of returning empty results.

CorperaHQ team·May 26, 2026
7 min read

Tenant isolation is one of those things that's easy to claim and hard to verify. Here's how we do it, with the actual mechanism, so you can audit the claim.

The plugin

Every Mongoose schema in the API installs a tenantScopePlugin. The plugin registers pre-hooks on every query verb — find, findOne, findOneAndUpdate, updateMany, deleteOne, deleteMany, countDocuments, aggregate, save.

On each query, the plugin:

1. Reads the current tenant from AsyncLocalStorage (set by authMiddleware on every request). 2. If the query doesn't include companyId, it injects the current tenant's value. 3. If the query DOES include a companyId, the plugin verifies it matches the current tenant. If not — it throws CROSS_TENANT_QUERY.

Throwing > returning empty

A naïve isolation approach returns empty results when tenants mismatch. That's worse than failing loudly: bugs that should throw at dev time silently return [] in production, and the dev assumes "no data."

We throw. Loudly. Stack trace. So that a controller that accidentally constructs a cross-tenant query gets noticed in the first 5 seconds of testing.

Escape hatch — but audited

Some legitimate flows (platform-staff admin tooling, internal migrations) need cross-tenant access. They use bypassTenantScope(query), which sets a special option on the query. Every use of that helper is grep-able and reviewed.

The result

There is no path through the application where data from tenant A reaches a session bound to tenant B. Not from a missing filter, not from a populated reference, not from an aggregation. The tenant boundary is a property of the data layer, not a property of any individual route.