XRAPH/Writing/Multi-tenancy you cannot forget to write

Multi-tenancy you cannot forget to write

Every cross-tenant leak I have looked at came from one query missing one clause. Making the boundary structural means the mistake stops being available.

Published
Apr 2025
Length
2 min read
Systems
2

#The clause you forget once

Multi-tenancy usually starts as a column. Every table gets a tenant identifier, every query gets a predicate, and a review convention says to check for it.

That holds until somebody writes a query outside the usual shape. An administrative report. A migration backfill. A count for a dashboard. A join that filters the left table and not the right. In every leak I have examined, the fix was one line, and nobody caught it because the correct query and the leaking query look almost identical.

If safety depends on remembering something, you have not built safety. You have built a habit, and habits have a failure rate.

#Moving the boundary under the query

The tenant rides on the request context and is set in exactly one place, from validated claims. Nothing downstream can set it, and every call that touches an engine requires a context carrying one. That is complete mediation applied to a data boundary .

From there each engine enforces it in the way that engine can genuinely enforce.

  • Postgres. A transaction-scoped setting plus row level security. The application role is not a superuser and cannot bypass. A query missing the predicate returns nothing rather than everything, which is fail-safe defaults doing their job.
  • Graph. One graph per tenant. Cross-tenant traversal is not a policy that can be misconfigured, because there is no edge to traverse.
  • Search. Per-tenant indices behind an alias with routing. A search that forgets the filter searches one tenant's index.
  • Cache. Key prefixes, which is the weakest of the four, enforced by a single key building function everything goes through.

A pre-query hook fails loudly in development if a query reaches an engine without a tenant. That is a backstop rather than the mechanism.

#Testing the claim

Row level security has a famous failure: you write the policy, test it as the table owner, and the owner bypasses it. The test passes and the policy does nothing.

So the integration suite connects as a dedicated non-superuser, sets tenant A, writes rows, switches to tenant B, and asserts that an unfiltered select returns zero rows. Not filtered correctly. Zero, because B has written nothing yet.

1// Deliberately no WHERE clause. The database is the thing under test.
2rows, err := nonSuperuserPool.Query(ctxTenantB, "SELECT id FROM assets")
3require.NoError(t, err)
4require.Empty(t, collect(rows)) // A's 400 rows are invisible, not filtered

The end-to-end argument says the check belongs where it can be enforced completely , and for tenancy that is the storage engine.

#Tenancy is not authorisation

Tenancy asks which customer's data this request may see at all. Authorisation asks whether this user may perform this action on this object, which is a different question with a substantial literature and systems built specifically for it .

Keeping them separate in the code mattered more than I expected. When one function answered both, its signature grew until nobody could say what it guaranteed.

#What this does not solve

Noisy neighbours, backup granularity and per-tenant data residency are all real and none fall out of getting the read path right. Nor does it remove the need for identity and access controls above it .

What it buys is that the worst outcome of a badly written query is an empty result set and a confused developer.

References

  1. [1]Jerome H. Saltzer, Michael D. Schroeder, The Protection of Information in Computer Systems, Proceedings of the IEEE, vol. 63, no. 9, pp. 1278-1308, 1975doi:10.1109/PROC.1975.9939
  2. [2]J. H. Saltzer, D. P. Reed, D. D. Clark, End-to-End Arguments in System Design, ACM Transactions on Computer Systems, vol. 2, no. 4, pp. 277-288, 1984doi:10.1145/357401.357402
  3. [3]Ruoming Pang et al., Zanzibar: Google's Consistent, Global Authorization System, USENIX Annual Technical Conference, 2019
  4. [4]Scott Rose, Oliver Borchert, Stu Mitchell, Sean Connelly, Zero Trust Architecture, NIST Special Publication 800-207, 2020doi:10.6028/NIST.SP.800-207