Securing Multi‑Tenant Data in FaunaDB with Role‑Based Access Control
Learn how FaunaDB’s RBAC lets you lock down tenant data with minimal code. The post walks through a practical example, highlights performance trade‑offs, and gives a clear checklist for secure deployment.
18 Jul 2025, 08:04 UTC

Why You Need Fine‑Grained Security in a Multi‑Tenant App
When a single FaunaDB instance serves many customers, the database itself must enforce that one tenant cannot read or modify another’s data. Relying on client‑side checks or custom middleware adds complexity and opens a window for privilege escalation. FaunaDB’s built‑in role‑based access control (RBAC) lets you declare permissions in the database, so the engine blocks disallowed queries before they hit your application logic.
What RBAC Looks Like in FaunaDB
RBAC in FaunaDB is a set of immutable JSON objects called Roles. Each role lists the actions (read, write, delete, query) you allow on specific collections or even individual documents. A role can be attached to a User or a Service Account. When a query runs, Fauna checks the caller’s roles, merges the permissions, and either allows or denies the operation.
Role Definition Example
{
"name": "tenant_reader",
"permissions": {
"customers": ["read"]
}
}
In this JSON, the role tenant_reader grants read access to the customers collection only. Notice that the role is immutable – once created, you cannot change its definition; you must create a new role for a different set of permissions. This guarantees that policy changes are versioned and auditable.
Putting It Into Practice
Below is a step‑by‑step sequence that demonstrates how to:
- Create a role that can only read a collection.
- Attach that role to a test user.
- Verify that a write attempt is blocked.
1. Create the Role
fauna role create \
--name tenant_reader \
--permissions='{"customers": ["read"]}'
Run this command on your terminal where the fauna CLI is installed. You need an API key with role:write privileges. The CLI will return the role’s ID, which you’ll use later.
2. Create a Test User
fauna user create \
--name tenant_user \
--email [contact removed] \
--password Secret123!
Again, the command requires user:write permissions. The user is now in the database but has no roles yet.
3. Assign the Role to the User
fauna user grant \
--user tenant_user \
--role tenant_reader
After this step, tenant_user can only read from customers. All other actions are denied.
4. Test the Policy
Use the Fauna shell or any SDK to log in as tenant_user and run two queries: a read (which should succeed) and a write (which should fail with a 403).
const client = new Client({
secret: 'user:tenant_user:Secret123!', // replace with real secret
domain: 'db.fauna.com'
});
// Read – expected to succeed
try {
const res = await client.query(
q.Get(q.Collection('customers'), q.Ref(q.Collection('customers'), 'cust123'))
);
console.log('Read succeeded:', res);
} catch (e) {
console.error('Read failed:', e);
}
// Write – expected to fail
try {
await client.query(
q.Create(q.Collection('customers'), {
data: { name: 'New Customer' }
})
);
} catch (e) {
console.error('Write failed as expected:', e);
}
When the write attempt runs, Fauna’s engine will evaluate the user’s roles, see that write is not in the customers permissions, and return a 403 error. The error object includes error: "PermissionDenied", which you can catch in your application to show a friendly message.
Performance and Trade‑Offs
Because permissions are checked at query time, there is a very small overhead per request. Benchmarks on typical workloads show ~1–3 ms extra latency for a simple read, negligible compared to network round‑trip time. However, if you create deeply nested role hierarchies or grant permissions on every document, the engine may perform more checks, slightly increasing latency.
Another consideration is administrative complexity. A large number of fine‑grained roles can become hard to manage and audit. A common pattern is to start with coarse roles (e.g., tenant_admin, tenant_user) and refine only when necessary.
How to Verify Your Setup
- Audit logs: Fauna records every denied request. In the console, navigate to Audit → Denied to confirm that the write attempt was blocked.
- Latency test: Run a script that sends 10,000 read queries with the role and compare the average latency to a script that omits RBAC. You should see only a few milliseconds difference.
- Role propagation: After changing a role’s permissions, all users attached to that role immediately see the new policy. Test by updating the role to add
writeand re‑running the write query.
Actionable Checklist for Production
- Define a minimal set of roles that reflect your business logic.
- Use immutable role definitions; version your policies.
- Attach roles to users or service accounts, not to the application code.
- Enable Fauna’s audit logging and regularly review denied requests.
- Run a latency benchmark before and after RBAC changes.
- Document role changes in your deployment pipeline to avoid accidental privilege escalation.
By following these steps, you can confidently secure your multi‑tenant data in FaunaDB without adding extra middleware or compromising performance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.