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:
- Authorization Models define object types, their relations, and rewrite rules using an OpenFGA-compatible DSL.
- Relationship Tuples are the facts of the system. Each tuple asserts that a subject has a relation to an object.
- 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 ownerRewrite rule types:
| Rule | Syntax | Description |
|---|---|---|
Direct (this) | [user] | Subject must be directly assigned via a tuple |
| Union | A or B | Subject must satisfy at least one of the relations |
| Intersection | A and B | Subject must satisfy all of the relations |
| Exclusion | A but not B | Subject must satisfy A and must not satisfy B |
| Computed userset | owner | Inherits from another relation on the same object |
| Tuple-to-userset | group#member | Follows 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
/api/fga/modelsRequires: view:fga_modelsList 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
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
limit | integer | Items 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
/api/fga/modelsRequires: manage:fga_modelsCreate 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
| Code | HTTP | Description |
|---|---|---|
DSL_PARSE_ERROR | 400 | DSL syntax is invalid. The message field includes the line number and description of the error |
DSL_VALIDATION_ERROR | 400 | DSL is syntactically valid but references undefined types, relations, or contains cycles |
VALIDATION_ERROR | 400 | Missing 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
/api/fga/models/[id]Requires: view:fga_modelsRetrieve 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
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Model does not exist or belongs to a different tenant |
Update Model
/api/fga/models/[id]Requires: manage:fga_modelsUpdate 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
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Model does not exist |
DSL_PARSE_ERROR | 400 | Updated DSL has syntax errors |
DSL_VALIDATION_ERROR | 400 | Updated DSL references undefined types or relations |
Delete Model
/api/fga/models/[id]Requires: manage:fga_modelsDelete 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
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Model does not exist |
MODEL_IS_ACTIVE | 400 | Cannot delete the currently active model |
Activate Model
/api/fga/models/[id]/activateRequires: manage:fga_modelsSet 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
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Model does not exist |
ALREADY_ACTIVE | 400 | This 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
/api/fga/tuplesRequires: view:fga_tuplesList relationship tuples with optional filtering. At least one filter parameter is recommended to avoid returning the entire tuple store. Supports pagination.
Query parameters
| Parameter | Type | Description |
|---|---|---|
objectType | string | Filter by object type (e.g., document) |
objectId | string | Filter by object ID |
relation | string | Filter by relation name |
subjectType | string | Filter by subject type |
subjectId | string | Filter by subject ID |
page | integer | Page number (default: 1) |
limit | integer | Items 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
/api/fga/tuplesRequires: manage:fga_tuplesWrite 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
| Code | HTTP | Description |
|---|---|---|
NO_ACTIVE_MODEL | 400 | No active authorization model exists for this tenant |
INVALID_TYPE | 400 | Object type does not exist in the active model |
INVALID_RELATION | 400 | Relation does not exist on this object type in the active model |
INVALID_SUBJECT_TYPE | 400 | The relation does not accept this subject type as a valid target |
TUPLE_EXISTS | 409 | An identical tuple already exists |
VALIDATION_ERROR | 400 | Missing required fields |
Delete Tuple
/api/fga/tuplesRequires: manage:fga_tuplesDelete 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
/api/fga/tuples/bulkRequires: manage:fga_tuplesWrite 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
| Code | HTTP | Description |
|---|---|---|
NO_ACTIVE_MODEL | 400 | No active authorization model |
INVALID_RELATION | 400 | A tuple references a relation that does not exist in the active model |
INVALID_TYPE | 400 | A tuple references an object type not in the active model |
BATCH_TOO_LARGE | 400 | Total writes + deletes exceeds the maximum batch size |
VALIDATION_ERROR | 400 | One 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
/api/fga/checkRequires: debug:fgaCheck 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
}| Field | Type | Required | Description |
|---|---|---|---|
objectType | string | Yes | The type of the target object |
objectId | string | Yes | The ID of the target object |
relation | string | Yes | The relation to check |
subjectType | string | Yes | The type of the subject (typically user) |
subjectId | string | Yes | The ID of the subject |
explain | boolean | No | When 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
| Code | HTTP | Description |
|---|---|---|
NO_ACTIVE_MODEL | 400 | No active authorization model |
INVALID_TYPE | 400 | Object type does not exist in the active model |
INVALID_RELATION | 400 | Relation does not exist on the specified type |
MAX_DEPTH_EXCEEDED | 400 | Evaluation exceeded the maximum recursion depth of 25 |
VALIDATION_ERROR | 400 | Missing 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
/api/fga/expandRequires: debug:fgaExpand 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"
}| Field | Type | Required | Description |
|---|---|---|---|
objectType | string | Yes | The type of the target object |
objectId | string | Yes | The ID of the target object |
relation | string | Yes | The 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
| Code | HTTP | Description |
|---|---|---|
NO_ACTIVE_MODEL | 400 | No active authorization model |
INVALID_TYPE | 400 | Object type not found in model |
INVALID_RELATION | 400 | Relation not found on this type |
List Objects
/api/fga/list-objectsRequires: debug:fgaList 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"
}| Field | Type | Required | Description |
|---|---|---|---|
objectType | string | Yes | The type of objects to search for |
relation | string | Yes | The relation the subject must have |
subjectType | string | Yes | The type of the subject |
subjectId | string | Yes | The ID of the subject |
page | integer | No | Page number for pagination (default: 1) |
limit | integer | No | Items per page (default: 100, max: 1000) |
Success response
{
"ok": true,
"data": {
"objectIds": ["readme", "api-spec", "changelog", "roadmap"],
"total": 4
}
}Error codes
| Code | HTTP | Description |
|---|---|---|
NO_ACTIVE_MODEL | 400 | No active authorization model |
INVALID_TYPE | 400 | Object type not found in model |
INVALID_RELATION | 400 | Relation 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
| Permission | Description |
|---|---|
view:fga_models | View authorization models and their DSL definitions |
manage:fga_models | Create, update, delete, and activate authorization models |
view:fga_tuples | List and read relationship tuples |
manage:fga_tuples | Write and delete relationship tuples (single and bulk) |
debug:fga | Execute 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}/activateStep 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 editorPOST /api/fga/check
{
"objectType": "document",
"objectId": "arch-doc",
"relation": "editor",
"subjectType": "user",
"subjectId": "bob"
}
// Result: { "allowed": false } — Bob is a member but not an adminStep 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 membershipRelated
- FGA / Zanzibar Model — How the authorization model and rewrite rules work
- Fine-Grained Authorization Guide — Practical guide to implementing FGA
- FGA Debugger — Test checks and inspect resolution trees in the Console
- Roles & Permissions API — RBAC endpoints that complement FGA
- JavaScript SDK —
client.fga.check()and tuple management from the SDK