Designing Tenant Isolation in FaunaDB: Why Indexes and ABAC Roles Belong in the Same Decision
FaunaDB's index-driven reads and ABAC predicates work best when designed together. A practical pattern for enforcing tenant isolation without application-level filtering.
13 Dec 2025, 15:29 UTC

The Forgotten WHERE Clause
You are building a multi-tenant application. Every order, invoice, and user record belongs to a specific tenant. The standard approach is to add a tenant identifier to every table and filter every query in application code. It works—until a new endpoint ships without the filter, a copied query drops a condition, or an ORM change silently alters the scope. The failure mode is a cross-tenant data leak, and it is invisible in code review because the bug is an omission, not a typo.
The engineering question is whether the database itself can enforce tenant isolation structurally, so that an unscoped query returns nothing rather than someone else's data. In FaunaDB, the answer depends on treating two features as a single design decision: the index-driven query model and Attribute-Based Access Control (ABAC).
The Thesis: Indexes and Roles Are One Decision
FaunaDB does not support arbitrary collection scans. Every read is served by a materialized index, and those indexes are maintained transactionally on writes. Separately, FaunaDB evaluates ABAC predicates inside roles at query time using FQL (Fauna Query Language) expressions. Because reads must already pass through an index, and because ABAC predicates are evaluated per query, the most reliable multi-tenant design chooses indexes and role predicates together. You are not just optimizing read latency; you are defining the surface where access control is enforced.
Why Every Read Needs an Index
In FaunaDB, you cannot issue a generic "find all documents in this collection" query. You must define an index that specifies the terms and values your queries will use, and reads are served from that materialized index. This constraint feels rigid at first, but for multi-tenant systems it forces upfront clarity: you must decide which fields will be queried before you write the code that queries them.
For tenant isolation, this means creating indexes that include the tenant identifier as a term. A query then becomes a lookup by tenant rather than a filtered scan. The index itself does not enforce security, but it gives the ABAC predicate a stable, queryable structure to evaluate against.
ABAC as a Query-Time Guardrail
Attribute-Based Access Control in FaunaDB is expressed through roles. A role contains a predicate—an FQL expression that is evaluated when a query runs. The predicate can inspect the identity making the request, the index terms being used, or the documents being read. For multi-tenancy, the pattern is to compare the tenant identifier in the query against a tenant claim on the caller's identity.
If the predicate fails, FaunaDB does not throw an authorization error that reveals the existence of data. Instead, the query behaves as if the data does not exist. This matters for multi-tenant applications because it prevents attackers from probing for valid tenant identifiers by observing error messages.
Worked Example: Tenant-Scoped Order Reads
Consider an orders collection where every document contains a tenant_id field. The goal is to let a user read only the orders belonging to their own tenant, enforced by the database. Run the following definitions in a test database using an admin or server key; apply to production only through your standard review process.
Step 1: Define the index. Create an index on orders with tenant_id as a term. This ensures every read is already scoped by tenant at the index level.
// Conceptual FQL pattern; exact syntax varies by FaunaDB version
CreateIndex({
name: "orders_by_tenant",
source: Collection("orders"),
terms: [{ field: ["data", "tenant_id"] }]
})
Step 2: Define the role with a predicate. Create a role that grants read access to the index, but attach a predicate that checks the tenant_id term against the caller's identity. The predicate is evaluated at query time, not at role-definition time.
// Conceptual FQL pattern; exact syntax varies by FaunaDB version
CreateRole({
name: "tenant_order_reader",
membership: { resource: Collection("users") },
privileges: [{
resource: Index("orders_by_tenant"),
actions: { read: true }
}],
predicate: Query(
Lambda(["tenant_id"],
Equals(
Var("tenant_id"),
Select(["data", "tenant_id"], Identity())
)
)
)
})
Step 3: Query as a specific principal. The application issues a read against the index using the caller's tenant identifier. If the caller's identity does not carry a matching tenant claim, the predicate evaluates to false and the index returns an empty set.
This pattern moves the tenant check from application code into the database. A developer who forgets to filter by tenant in a new endpoint will still get an empty result set if the role predicate is correctly configured, because the database rejects the read before it reaches the index.
Trade-offs and Limitations
This approach is not free. ABAC predicates are evaluated on every query, which adds latency. When predicates are nested or reference multiple collections, debugging becomes difficult because the failure is silent—the query simply returns no data. You cannot easily distinguish between "no records exist" and "the predicate blocked access" without instrumenting the application separately.
The index requirement is also rigid. Every new query shape requires a new index. If you later need to sort orders by creation date within a tenant, you must define a second index with both tenant_id and the timestamp as terms. Index proliferation is a real maintenance cost.
FaunaDB's temporal features, which expose document history through system collections, are useful for audit trails but increase storage and read costs with write volume. Time-travel queries against high-churn collections can become expensive.
Finally, FQL syntax and feature availability are version-sensitive across major releases. The predicate structure shown above is a pattern, not a guaranteed API. Role capabilities, identity token structure, and index definition syntax may differ between FaunaDB versions. Always verify against the documentation for your specific deployment.
How to Verify This Works
Do not assume the pattern is correct. Build a small test database and confirm the behavior directly using an admin or server key in a non-production environment.
- Test allow and deny. Create a role with an ABAC predicate referencing a tenant identifier. Issue the same query as two different principals—one with the matching tenant claim, one without—and confirm the second returns an empty set.
- Confirm the index requirement. Attempt to read a collection without using an index. FaunaDB should reject the query, proving that all reads are index-backed.
- Test transactional boundaries. Perform a transactional update of two related documents and introduce a concurrent conflicting write. Observe whether both documents commit atomically or the transaction aborts.
If the deny case returns data, your predicate is not evaluating as expected. If the index-less read succeeds, your FaunaDB version may behave differently than assumed. These checks take minutes and catch configuration errors before they reach production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.