FGA Guide — Fine-Grained Authorization
Overview
ZewstID FGA extends RBAC with resource-level permissions. While RBAC answers "does this user have the admin role?", FGA answers "can this user edit this specific document?"
FGA is built on the Zanzibar model: authorization is a graph of relationships (tuples). A check traverses the graph to find if a path exists between a user and a resource.
When to use FGA vs RBAC
| Need | Use |
|---|---|
| Admin / user / viewer roles | RBAC (simpler, JWT-based) |
| User can edit document:123 | FGA |
| Team members can view project files | FGA |
| Folder permissions inherit to documents | FGA |
Core Concepts
Tuples
A tuple is a relationship:
(user, relation, object)user:alice editor document:budget-2026 team:finance viewer folder:q1-reports folder:q1 parent document:budget-2026
Authorization Model
Defines types, relations, and how relations compute:
{ "types": [ { "type": "document", "relations": { "owner": { "this": {} }, "editor": { "union": ["owner"] }, "viewer": { "union": ["editor"] } } } ] }
This means:
viewereditorseditorownersCheck
"Can user:alice view document:budget-2026?"
The engine:
- Checks direct tuple: does exist?
(user:alice, viewer, document:budget-2026) - Checks union: is alice an ? Is she an
editor?owner - Checks group membership: is alice in a team that has on the document?
viewer - Checks parent traversal: does the document's parent folder grant access?
API Reference
Write tuples
curl -X POST https://api.zewstid.com/api/v1/authz/apps/$APP_ID/tuples \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tuples": [ {"user":"user:alice","relation":"editor","object":"document:budget-2026"}, {"user":"team:finance","relation":"viewer","object":"folder:q1-reports"} ] }'
Up to 100 tuples per request.
Delete tuples
curl -X DELETE https://api.zewstid.com/api/v1/authz/apps/$APP_ID/tuples \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tuples": [ {"user":"user:alice","relation":"editor","object":"document:budget-2026"} ] }'
List tuples
curl -G https://api.zewstid.com/api/v1/authz/apps/$APP_ID/tuples \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "userType=user" \ --data-urlencode "userId=alice" \ --data-urlencode "relation=editor" \ --data-urlencode "objectType=document"
All filter params (
userTypeuserIdrelationobjectTypeobjectIdlimitoffsetCheck
curl -X POST https://api.zewstid.com/api/v1/authz/check \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"user":"user:alice","relation":"viewer","object":"document:budget-2026"}'
Response:
{"allowed":true,"cached":false,"resolution_time_ms":5}The
appIdazpusersubList objects
"What documents can alice view?"
curl -X POST https://api.zewstid.com/api/v1/authz/apps/$APP_ID/list-objects \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"user":"user:alice","relation":"viewer","objectType":"document"}'
Response:
{"objects":["budget-2026","meeting-notes","project-plan"]}List users
"Who has access to document:budget-2026?"
curl -X POST https://api.zewstid.com/api/v1/authz/apps/$APP_ID/list-users \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"relation":"viewer","object":"document:budget-2026"}'
Response:
{"users":["user:alice","user:bob","team:finance"]}Expand (debug)
Shows the resolution tree for a check — useful for debugging why a check returned
allowed: truefalsecurl -X POST https://api.zewstid.com/api/v1/authz/apps/$APP_ID/expand \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"user":"user:alice","relation":"viewer","object":"document:budget-2026"}'
Get / Update authorization model
# Read current model curl https://api.zewstid.com/api/v1/authz/apps/$APP_ID/model \ -H "Authorization: Bearer $TOKEN" # Update model (replaces existing) curl -X PUT https://api.zewstid.com/api/v1/authz/apps/$APP_ID/model \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "model": { "types": [ { "type": "document", "relations": { "owner": { "this": {} }, "editor": { "union": ["owner"] }, "viewer": { "union": ["editor"] } } } ] } }'
SDK Usage
Client-side
import { useAuthz } from '@zewstid/id-nextjs'; function DocumentView({ docId }: { docId: string }) { const { check } = useAuthz(); const [canEdit, setCanEdit] = useState(false); useEffect(() => { check('editor', `document:${docId}`).then(setCanEdit); }, [docId]); return canEdit ? <Editor /> : <ReadOnlyView />; }
Server-side
import { createServerAuthz } from '@zewstid/id-nextjs/server'; const authz = createServerAuthz(); export async function PUT(req, { params }) { const session = await getServerSession(authOptions); const canEdit = await authz.check(session.accessToken, 'editor', `document:${params.id}`); if (!canEdit) return Response.json({ error: 'Forbidden' }, { status: 403 }); // ... update document }
Write tuples when creating resources
// When a user creates a document, make them the owner await authz.writeTuples(session.accessToken, appId, [ { user: `user:${session.user.id}`, relation: 'owner', object: `document:${doc.id}` } ]);
Model Patterns
Team-based access
{ "types": [ { "type": "team", "relations": { "member": { "this": {} } } }, { "type": "project", "relations": { "team": { "this": {} }, "viewer": { "tupleToUserset": { "tupleset": "team", "computedUserset": "member" } } } } ] }
Tuple:
(team:engineering, team, project:alpha)(user:alice, member, team:engineering)can user:alice view project:alpha?Folder hierarchy
{ "types": [ { "type": "folder", "relations": { "viewer": { "this": {} } } }, { "type": "document", "relations": { "parent": { "this": {} }, "viewer": { "union": [], "tupleToUserset": { "tupleset": "parent", "computedUserset": "viewer" } } } } ] }
Performance & Caching
The FGA engine is designed for sub-20ms checks at scale.
Latency targets:
- Simple checks (direct tuple): <5ms
- Role-based checks (user -> role -> permission): <10ms
- Model-based checks (1-2 hops): <20ms
3-layer cache:
- In-memory LRU — 10s TTL, per-process. Catches hot paths in tight loops.
- Redis tuple cache — 60s TTL. Shared across API Gateway instances.
- Redis role cache — 300s TTL. Materialized role-permission graph for RBAC checks.
Tuples and role assignments invalidate the relevant cache entries on write so changes propagate within a few seconds.
Engine guardrails:
- Max depth: 10 hops. Prevents runaway recursion through deep tupleToUserset chains.
- Resolution timeout: 50ms. Returns and logs a warning if exceeded.
allowed: false - Cycle detection: Visited set prevents infinite loops in self-referential models (e.g., of
manager).manager
Use the
expandWas this page helpful?
Let us know how we can improve our documentation