Skip to Content

Fine-Grained Authorization (FGA) API

The Fine-Grained Authorization engine provides Zanzibar-style relationship-based access control (ReBAC). It extends the RBAC layer with object-level authorization: instead of “user has permission X globally,” FGA answers “does user Alice have relation viewer on document readme?”

The system is built around three concepts:

  1. Authorization Models define object types, their relations, and rewrite rules using an OpenFGA-compatible DSL.
  2. Relationship Tuples are the facts of the system. Each tuple asserts that a subject has a relation to an object.
  3. Authorization Queries (check, expand, list-objects) evaluate tuples against the model’s rewrite rules to answer access questions.

All FGA endpoints require the x-tenant header and a valid access token.

Authorization Model DSL

The DSL defines types and their relations. Each relation can have rewrite rules that compose access from other relations or indirect relationships.

type user type group relations define member: [user] type document relations define owner: [user] define editor: [user, group#member] define viewer: [user, group#member] or editor or owner

Rewrite rule types:

RuleSyntaxDescription
Direct (this)[user]Subject must be directly assigned via a tuple
UnionA or BSubject must satisfy at least one of the relations
IntersectionA and BSubject must satisfy all of the relations
ExclusionA but not BSubject must satisfy A and must not satisfy B
Computed usersetownerInherits from another relation on the same object
Tuple-to-usersetgroup#memberFollows a relation on a related object (indirect)

The FGA engine uses recursive evaluation with a maximum depth of 25 and cycle detection via a visited set. This prevents infinite loops in circular relation definitions.

Authorization Models

List Models

GET/api/fga/modelsRequires: view:fga_models

List all authorization models for the tenant, ordered by version descending. Only one model can be active at a time. The active model is used for all tuple validation and authorization queries.

Query parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 20)

Success response

{ "ok": true, "data": [ { "id": "model_abc123", "name": "SaaS Authorization Model", "version": 3, "isActive": true, "createdAt": "2025-02-10T00:00:00Z", "updatedAt": "2025-02-12T08:30:00Z" }, { "id": "model_def456", "name": "SaaS Authorization Model", "version": 2, "isActive": false, "createdAt": "2025-02-05T00:00:00Z", "updatedAt": "2025-02-05T00:00:00Z" } ] }

Create Model

POST/api/fga/modelsRequires: manage:fga_models

Create a new authorization model by providing a name and a DSL definition. The DSL is parsed line-by-line and validated before storage. If the DSL contains syntax errors or references undefined types or relations, the request is rejected with a detailed error message including the line number.

Request body

{ "name": "Document Access Model", "dsl": "type user\n\ntype group\n relations\n define member: [user]\n\ntype document\n relations\n define owner: [user]\n define editor: [user, group#member]\n define viewer: [user, group#member] or editor or owner\n" }

Success response

{ "ok": true, "data": { "id": "model_ghi789", "name": "Document Access Model", "version": 1, "isActive": false, "dsl": "type user\n\ntype group\n relations\n define member: [user]\n\ntype document\n relations\n define owner: [user]\n define editor: [user, group#member]\n define viewer: [user, group#member] or editor or owner\n", "schema": { "typeDefinitions": [ { "type": "user", "relations": {} }, { "type": "group", "relations": { "member": { "this": {} } } }, { "type": "document", "relations": { "owner": { "this": {} }, "editor": { "this": {} }, "viewer": { "union": { "children": [ { "this": {} }, { "computedUserset": { "relation": "editor" } }, { "computedUserset": { "relation": "owner" } } ] } } } } ] }, "createdAt": "2025-02-18T10:00:00Z" } }

Error codes

CodeHTTPDescription
DSL_PARSE_ERROR400DSL syntax is invalid. The message field includes the line number and description of the error
DSL_VALIDATION_ERROR400DSL is syntactically valid but references undefined types, relations, or contains cycles
VALIDATION_ERROR400Missing required fields (name or dsl)

DSL parse error example

{ "ok": false, "error": { "code": "DSL_PARSE_ERROR", "message": "Line 7: Unknown keyword 'defines'. Did you mean 'define'?" } }

Get Model

GET/api/fga/models/[id]Requires: view:fga_models

Retrieve a specific authorization model by its ID. Returns the full model including the raw DSL text and the parsed schema object.

Success response

{ "ok": true, "data": { "id": "model_ghi789", "name": "Document Access Model", "version": 1, "isActive": false, "dsl": "type user\n\ntype group\n relations\n define member: [user]\n...", "schema": { "typeDefinitions": [ { "type": "user", "relations": {} }, { "type": "group", "relations": { "member": { "this": {} } } }, { "type": "document", "relations": { "owner": { "this": {} }, "editor": { "this": {} }, "viewer": { "union": {} } } } ] }, "createdAt": "2025-02-18T10:00:00Z", "updatedAt": "2025-02-18T10:00:00Z" } }

Error codes

CodeHTTPDescription
NOT_FOUND404Model does not exist or belongs to a different tenant

Update Model

PUT/api/fga/models/[id]Requires: manage:fga_models

Update a model’s name or DSL definition. When the DSL is updated, it is re-parsed and re-validated. The model’s version is not automatically incremented on update — create a new model for versioned changes.

Request body

{ "name": "Document Access Model v2", "dsl": "type user\n\ntype document\n relations\n define owner: [user]\n define viewer: [user] or owner\n" }

Both name and dsl are optional. Only provided fields are updated.

Success response

{ "ok": true, "data": { "id": "model_ghi789", "name": "Document Access Model v2", "version": 1, "isActive": false, "schema": { "typeDefinitions": [] }, "updatedAt": "2025-02-18T11:00:00Z" } }

Error codes

CodeHTTPDescription
NOT_FOUND404Model does not exist
DSL_PARSE_ERROR400Updated DSL has syntax errors
DSL_VALIDATION_ERROR400Updated DSL references undefined types or relations

Delete Model

DELETE/api/fga/models/[id]Requires: manage:fga_models

Delete an authorization model. Active models cannot be deleted — you must activate a different model first, or deactivate by activating another model.

Deleting a model does not automatically delete tuples that were written against it. Orphaned tuples are ignored by authorization queries but remain in the database until manually cleaned up.

Success response

{ "ok": true, "data": { "deleted": true } }

Error codes

CodeHTTPDescription
NOT_FOUND404Model does not exist
MODEL_IS_ACTIVE400Cannot delete the currently active model

Activate Model

POST/api/fga/models/[id]/activateRequires: manage:fga_models

Set a model as the active authorization model for the tenant. Any previously active model is automatically deactivated. All subsequent check, expand, and list-objects queries will use the newly activated model’s rewrite rules. The model cache is invalidated immediately.

Request: No body required.

Success response

{ "ok": true, "data": { "activated": true, "modelId": "model_ghi789" } }

Error codes

CodeHTTPDescription
NOT_FOUND404Model does not exist
ALREADY_ACTIVE400This model is already the active model

The FGA engine caches the active model in memory with a 5-minute TTL. After activating a new model, queries may use the old model for up to 5 minutes on other server instances. Force-refresh happens immediately on the instance that processed the activation request.

Relationship Tuples

Tuples are the authorization facts. Each tuple states that a subject has a relation to an object.

Tuple format: objectType:objectId#relation@subjectType:subjectId

Examples:

  • document:readme#viewer@user:alice — Alice is a viewer of document “readme”
  • document:readme#editor@group:engineering#member — members of group “engineering” are editors of document “readme”
  • folder:projects#owner@user:bob — Bob owns folder “projects”

List Tuples

GET/api/fga/tuplesRequires: view:fga_tuples

List relationship tuples with optional filtering. At least one filter parameter is recommended to avoid returning the entire tuple store. Supports pagination.

Query parameters

ParameterTypeDescription
objectTypestringFilter by object type (e.g., document)
objectIdstringFilter by object ID
relationstringFilter by relation name
subjectTypestringFilter by subject type
subjectIdstringFilter by subject ID
pageintegerPage number (default: 1)
limitintegerItems per page (default: 20, max: 100)

Success response

{ "ok": true, "data": { "data": [ { "id": "tuple_abc123", "objectType": "document", "objectId": "readme", "relation": "viewer", "subjectType": "user", "subjectId": "alice", "subjectRelation": null, "createdAt": "2025-02-15T10:00:00Z" }, { "id": "tuple_def456", "objectType": "document", "objectId": "readme", "relation": "editor", "subjectType": "group", "subjectId": "engineering", "subjectRelation": "member", "createdAt": "2025-02-15T10:05:00Z" } ], "pagination": { "page": 1, "limit": 20, "total": 2, "totalPages": 1 } } }

Write Tuple

POST/api/fga/tuplesRequires: manage:fga_tuples

Write a single relationship tuple. The tuple is validated against the active authorization model before storage. The object type, relation, and subject type must exist in the active model, and the relation must accept the subject type as a valid assignment target.

Request body — direct user assignment

{ "objectType": "document", "objectId": "readme", "relation": "viewer", "subjectType": "user", "subjectId": "alice" }

Request body — subject set (indirect/group membership)

When the subject is not an individual user but a set of users defined by a relation on another object, include subjectRelation:

{ "objectType": "document", "objectId": "readme", "relation": "editor", "subjectType": "group", "subjectId": "engineering", "subjectRelation": "member" }

This means: “all entities that have the member relation on group:engineering are also editor on document:readme.”

Success response

{ "ok": true, "data": { "id": "tuple_ghi789", "objectType": "document", "objectId": "readme", "relation": "viewer", "subjectType": "user", "subjectId": "alice", "subjectRelation": null, "createdAt": "2025-02-18T10:00:00Z" } }

Error codes

CodeHTTPDescription
NO_ACTIVE_MODEL400No active authorization model exists for this tenant
INVALID_TYPE400Object type does not exist in the active model
INVALID_RELATION400Relation does not exist on this object type in the active model
INVALID_SUBJECT_TYPE400The relation does not accept this subject type as a valid target
TUPLE_EXISTS409An identical tuple already exists
VALIDATION_ERROR400Missing required fields

Delete Tuple

DELETE/api/fga/tuplesRequires: manage:fga_tuples

Delete a specific relationship tuple by providing the tuple data in the request body. All tuple fields must match exactly. Returns success even if the tuple does not exist (idempotent delete).

Request body

{ "objectType": "document", "objectId": "readme", "relation": "viewer", "subjectType": "user", "subjectId": "alice" }

Success response

{ "ok": true, "data": { "deleted": true } }

Bulk Write/Delete Tuples

POST/api/fga/tuples/bulkRequires: manage:fga_tuples

Write and/or delete multiple tuples in a single atomic request. All operations in the batch are validated against the active model before any writes occur. If any single tuple fails validation, the entire batch is rejected and no changes are made.

Request body

{ "writes": [ { "objectType": "document", "objectId": "api-spec", "relation": "owner", "subjectType": "user", "subjectId": "bob" }, { "objectType": "document", "objectId": "api-spec", "relation": "viewer", "subjectType": "group", "subjectId": "engineering", "subjectRelation": "member" } ], "deletes": [ { "objectType": "document", "objectId": "readme", "relation": "viewer", "subjectType": "user", "subjectId": "alice" } ] }

Both writes and deletes are optional, but at least one must be present. Each array can contain up to 100 tuples.

Success response

{ "ok": true, "data": { "written": 2, "deleted": 1 } }

Error codes

CodeHTTPDescription
NO_ACTIVE_MODEL400No active authorization model
INVALID_RELATION400A tuple references a relation that does not exist in the active model
INVALID_TYPE400A tuple references an object type not in the active model
BATCH_TOO_LARGE400Total writes + deletes exceeds the maximum batch size
VALIDATION_ERROR400One or more tuples are missing required fields

Bulk operations are atomic. If tuple #47 out of 100 fails validation, none of the 100 tuples are written or deleted. Check the error response for the specific failing tuple.

Authorization Queries

Check

POST/api/fga/checkRequires: debug:fga

Check whether a subject has a specific relation to an object. The engine evaluates the full rewrite rules from the active model recursively, following computed usersets, tuple-to-userset indirections, unions, intersections, and exclusions. Optionally returns a resolution tree showing exactly how the decision was reached.

Request body

{ "objectType": "document", "objectId": "readme", "relation": "viewer", "subjectType": "user", "subjectId": "alice", "explain": false }
FieldTypeRequiredDescription
objectTypestringYesThe type of the target object
objectIdstringYesThe ID of the target object
relationstringYesThe relation to check
subjectTypestringYesThe type of the subject (typically user)
subjectIdstringYesThe ID of the subject
explainbooleanNoWhen true, includes the resolution tree (default: false)

Success response (without explain)

{ "ok": true, "data": { "allowed": true } }

Success response (with explain)

{ "ok": true, "data": { "allowed": true, "resolution": { "type": "union", "relation": "viewer", "result": true, "children": [ { "type": "this", "relation": "viewer", "result": false }, { "type": "computedUserset", "relation": "editor", "result": false }, { "type": "computedUserset", "relation": "owner", "result": true, "children": [ { "type": "this", "relation": "owner", "result": true, "tupleFound": "document:readme#owner@user:alice" } ] } ] } } }

The resolution tree above shows: Alice is not a direct viewer and not an editor, but she is an owner, and the viewer relation includes or owner, so the check passes.

Error codes

CodeHTTPDescription
NO_ACTIVE_MODEL400No active authorization model
INVALID_TYPE400Object type does not exist in the active model
INVALID_RELATION400Relation does not exist on the specified type
MAX_DEPTH_EXCEEDED400Evaluation exceeded the maximum recursion depth of 25
VALIDATION_ERROR400Missing required fields

In production, resource servers should call the check endpoint using an M2M token with debug:fga rather than exposing it to end users. The explain mode is particularly useful during development and debugging but adds overhead and should be disabled in hot paths.

Expand

POST/api/fga/expandRequires: debug:fga

Expand a relation on an object to discover all subjects that have that relation. Returns a tree structure that follows the rewrite rules, showing both direct tuple assignments and indirect relationships (computed usersets, tuple-to-userset).

Request body

{ "objectType": "document", "objectId": "readme", "relation": "viewer" }
FieldTypeRequiredDescription
objectTypestringYesThe type of the target object
objectIdstringYesThe ID of the target object
relationstringYesThe relation to expand

Success response

{ "ok": true, "data": { "tree": { "root": { "type": "union", "relation": "viewer", "nodes": [ { "type": "leaf", "relation": "viewer", "subjects": [ { "type": "user", "id": "dave" }, { "type": "user", "id": "eve" } ] }, { "type": "computedUserset", "relation": "editor", "subjects": [ { "type": "user", "id": "bob" } ] }, { "type": "computedUserset", "relation": "owner", "subjects": [ { "type": "user", "id": "alice" } ] } ] } } } }

This shows that document:readme has the following viewers: Dave and Eve (directly), Bob (via editor), and Alice (via owner).

Error codes

CodeHTTPDescription
NO_ACTIVE_MODEL400No active authorization model
INVALID_TYPE400Object type not found in model
INVALID_RELATION400Relation not found on this type

List Objects

POST/api/fga/list-objectsRequires: debug:fga

List all objects of a given type that a subject can access via a specific relation. Performs a reverse lookup through the tuple store and rewrite rules. Useful for building filtered views such as “show me all documents this user can view.”

Request body

{ "objectType": "document", "relation": "viewer", "subjectType": "user", "subjectId": "alice" }
FieldTypeRequiredDescription
objectTypestringYesThe type of objects to search for
relationstringYesThe relation the subject must have
subjectTypestringYesThe type of the subject
subjectIdstringYesThe ID of the subject
pageintegerNoPage number for pagination (default: 1)
limitintegerNoItems per page (default: 100, max: 1000)

Success response

{ "ok": true, "data": { "objectIds": ["readme", "api-spec", "changelog", "roadmap"], "total": 4 } }

Error codes

CodeHTTPDescription
NO_ACTIVE_MODEL400No active authorization model
INVALID_TYPE400Object type not found in model
INVALID_RELATION400Relation not found on this type

The list-objects query can be expensive for large tuple stores as it requires scanning and evaluating multiple tuples. Use pagination and consider caching results for frequently accessed patterns.

Permissions Reference

PermissionDescription
view:fga_modelsView authorization models and their DSL definitions
manage:fga_modelsCreate, update, delete, and activate authorization models
view:fga_tuplesList and read relationship tuples
manage:fga_tuplesWrite and delete relationship tuples (single and bulk)
debug:fgaExecute check, expand, and list-objects queries

Complete Example

This walkthrough demonstrates a common authorization pattern: a team-based document access system.

Step 1: Create the model

POST /api/fga/models { "name": "Team Documents", "dsl": "type user\n\ntype team\n relations\n define member: [user]\n define admin: [user]\n\ntype document\n relations\n define owner: [user]\n define team: [team]\n define editor: [user, team#admin]\n define viewer: [user, team#member] or editor or owner\n" }

Step 2: Activate the model

POST /api/fga/models/{modelId}/activate

Step 3: Write tuples

POST /api/fga/tuples/bulk { "writes": [ { "objectType": "team", "objectId": "engineering", "relation": "member", "subjectType": "user", "subjectId": "alice" }, { "objectType": "team", "objectId": "engineering", "relation": "member", "subjectType": "user", "subjectId": "bob" }, { "objectType": "team", "objectId": "engineering", "relation": "admin", "subjectType": "user", "subjectId": "alice" }, { "objectType": "document", "objectId": "arch-doc", "relation": "team", "subjectType": "team", "subjectId": "engineering" }, { "objectType": "document", "objectId": "arch-doc", "relation": "editor", "subjectType": "team", "subjectId": "engineering", "subjectRelation": "admin" }, { "objectType": "document", "objectId": "arch-doc", "relation": "viewer", "subjectType": "team", "subjectId": "engineering", "subjectRelation": "member" } ] }

Step 4: Check access

POST /api/fga/check { "objectType": "document", "objectId": "arch-doc", "relation": "editor", "subjectType": "user", "subjectId": "alice", "explain": true } // Result: { "allowed": true } — Alice is an admin of engineering, which grants editor
POST /api/fga/check { "objectType": "document", "objectId": "arch-doc", "relation": "editor", "subjectType": "user", "subjectId": "bob" } // Result: { "allowed": false } — Bob is a member but not an admin

Step 5: List objects a user can view

POST /api/fga/list-objects { "objectType": "document", "relation": "viewer", "subjectType": "user", "subjectId": "bob" } // Result: { "objectIds": ["arch-doc"] } — Bob can view via team membership