Enforcing Multi-Tenant Trust Boundaries with FaunaDB RBAC
Application-level tenant filters fail silently. FaunaDB RBAC pushes isolation into the database by scoping roles to child databases, so a leaked token cannot cross tenant boundaries.
07 Aug 2026, 12:25 UTC

The Problem With Application-Only Tenant Checks
Multi-tenant SaaS applications typically enforce isolation in application code: every query filters by a tenant identifier, and every microservice is expected to remember that filter. This breaks when a new endpoint forgets the check, a background job runs with elevated credentials, or an operator debugs with an administrative token. The failure is silent—the database returns rows that should have been invisible.
FaunaDB's role-based access control (RBAC) moves that enforcement into the database itself. A token issued for one tenant carries only the privileges declared for its role, so even a buggy application cannot read another tenant's documents. The useful takeaway: model each tenant as a separate child database with its own roles, and treat the parent database as the only trusted control plane.
Requirements for a Database-Enforced Boundary
Before choosing a design, define what the boundary must guarantee:
- Hard isolation: no query, index, or function in one tenant's context can reach another tenant's documents.
- Least privilege: application services receive read or write access only to the collections they need, not database administration rights.
- Auditability: an operator can list every role and its privileges without guessing.
- Revocation: access can be withdrawn by deleting or altering a role, not by redeploying application code.
RBAC alone does not encrypt data at rest. If confidentiality must survive a database-level compromise—for example, a stolen backup—sensitive fields need client-side encryption before they reach FaunaDB.
Smallest Suitable Design: Parent Database Plus Child Databases
The simplest architecture that satisfies hard isolation uses one parent database and one child database per tenant. The parent database holds the tenant registry and provisioning logic. Each child database contains that tenant's collections, indexes, and user-defined functions. Because FaunaDB roles can be scoped to a specific database or child database, a role defined inside a tenant's child database cannot reference resources in a sibling tenant.
In this model, the application never receives a parent-database secret. Instead, a provisioning service running in the parent context creates a child database, defines roles inside it, and issues a token or key scoped to that child database. The application uses that credential for all subsequent operations.
This design is not free. Creating a child database per tenant adds operational objects to track, and FaunaDB may impose limits on the number of child databases per parent. When tenant count grows into the thousands, a single shared database with carefully written FQL predicates may be more practical, but that shifts isolation from a hard database boundary to the correctness of every predicate.
Defining Roles and Tokens in FQL
Roles in FaunaDB are created with FQL (Fauna Query Language). The following example creates a read-only role for an orders collection inside a tenant's child database. Run it in the Fauna dashboard query console, fauna-shell, or an application driver connected to the target child database. You need an admin or server key for that database.
CreateRole({
name: "orders_readonly",
membership: [
{ resource: Collection("orders") }
],
privileges: [
{
resource: Collection("orders"),
actions: {
read: true,
write: false,
create: false,
delete: false
}
}
]
})
Replace orders_readonly and orders with your role and collection names. After creation, generate a token for the role and use it to attempt a write. The write should fail with an authorization error. If it succeeds, the role's privilege set is too broad.
To remove the role during testing, run DeleteRole("orders_readonly") in the same child database context. This is a state-changing operation; it takes effect immediately for new queries, though FaunaDB documentation notes that role changes are eventually consistent across global clusters, so a brief window may exist where outdated permissions are still enforced.
Operational Checks That Catch Misconfiguration
RBAC is easy to get subtly wrong. These checks turn intent into verified behavior.
Inventory every role
Run Role.all() in each child database. Compare the returned privileges against your design document. Any role with write: true on a collection that should be read-only is a defect.
Negative testing with a real token
Create a token for the role under test, then attempt an operation the role should not perform. For a read-only role, attempt Create(Collection("orders"), { data: { test: true } }). The request must return an authorization error. Catch that error explicitly in application code so it is not confused with a network failure or a syntax error.
Monitor for unexpected authorization windows
After changing a role, watch FaunaDB logs for Unauthorized events. A spike may indicate that tokens are still carrying old privileges, or that a service is attempting an operation it should no longer be able to perform.
Failure Modes and Their Symptoms
| Failure mode | Symptom | Immediate check |
|---|---|---|
| Overly broad membership | Token can read documents outside its intended scope | Inspect the role's membership array; ensure it references only the required collection or index |
| Missing privilege | Authorization error on a legitimate operation | Run Role.all() and confirm the required action is set to true |
| Eventual consistency lag | Old permissions still work seconds after a role update | Wait and retry; monitor logs for Unauthorized events until the window closes |
| Parent key leakage | Cross-tenant access becomes possible | Rotate the parent key immediately and audit child database secrets |
When This Design Stops Fitting
Move away from per-tenant child databases when cross-tenant reporting becomes a core requirement. Analytics that span every tenant are awkward when data is partitioned by database. In that case, a shared database with a tenant identifier on every document and FQL predicates that compare the document's tenant to the token's identity may be justified, but every predicate becomes a security boundary that must be tested.
Also revisit the design if regulatory or contractual requirements demand encryption at rest. FaunaDB RBAC controls who can reach data; it does not make data unreadable to someone with filesystem access. Client-side encryption of sensitive fields remains necessary.
Practical Verification
After implementing the design, verify the boundary end to end: issue a token for Tenant A, attempt to read a collection in Tenant B's child database, and confirm the request fails. Then list roles in both child databases with Role.all() and confirm no privilege crosses the database boundary. Because FaunaDB syntax and platform behavior evolve, treat the FQL snippets above as illustrative patterns and confirm them against your cluster's current documentation before deploying to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.