Resources are how IntelliAuth implements ReBAC — relation-based access control. Instead of "the user has these scopes" (RBAC), you say "the user has this relation to this resource", and ask permission questions like "can this user edit that document".
ReBAC is the right tool when your authorization rules talk about specific things — folders, documents, teams, projects — rather than blanket capabilities. RBAC fits "anyone with the editor role can publish". ReBAC fits "the user who created this document can publish it, anyone they shared it with can comment on it, anyone with edit access to the parent folder can edit it too."
The data model is intentionally small: types, instances, relations.
The model
Section titled “The model”- Resource type (
/api/v1/resource-types/*) — a kind of thing. "document", "folder", "team", "project". You define the relations a type can have: "owner", "editor", "viewer", "parent". - Resource (
/api/v1/resources/*) — an instance. A specific document, a specific folder. Identified by its type + external id (you choose the external id; the platform doesn't care what format). - Relation — a triple
(subject, relation, resource). Examples:(user:usr_01HZX..., owner, document:doc_42),(team:eng, editor, folder:internal). - Check — the question. "Can
user:usr_01HZX...performeditondocument:doc_42?" The platform walks the relation graph and answers yes / no.
You define the types up front, write the relation rules once, and then your app's job is just to add and remove relations.
Define a resource type
Section titled “Define a resource type”POST /api/v1/resource-typesAuthorization: Bearer <access-token>Content-Type: application/jsonRequired scope: resources:write
{ "name": "document", "description": "A document users can author and share.", "relations": [ { "name": "owner", "rewrites": [] }, { "name": "editor", "rewrites": [ { "kind": "this" }, { "kind": "computed", "relation": "owner" } ] }, { "name": "viewer", "rewrites": [ { "kind": "this" }, { "kind": "computed", "relation": "editor" } ] }, { "name": "parent", "rewrites": [] }, { "name": "inherit-from-parent", "rewrites": [ { "kind": "tuple_to_userset", "tupleset": "parent", "computed": "editor" } ] } ]}The rewrites shape is the Zanzibar-style userset rewrite system. Plain English for the above:
owneris a direct relation. Either a relation exists or it doesn't.editoris granted bythis(someone marked directly) OR by computing fromowner(every owner is an editor).vieweris granted directly OR by being an editor.parentis a direct relation linking a document to a folder.inherit-from-parentwalks: take whoever this document'sparentis, then ask who haseditoron that parent.
Add a resource
Section titled “Add a resource”POST /api/v1/resourcesContent-Type: application/jsonRequired scope: resources:write
{ "type": "document", "external_id": "doc_42", "attributes": { "title": "Q2 financial report", "owner_team": "finance" }}external_id is your application's identifier — it does not need to match an IntelliAuth ID format. Attributes are optional and are stored alongside but do not affect authorization checks.
Create a relation
Section titled “Create a relation”POST /api/v1/resources/{type}/{external_id}/relationsContent-Type: application/jsonRequired scope: resources:write
{ "subject": { "type": "user", "id": "usr_01HZX..." }, "relation": "owner"}For group subjects (give everyone in the team editor access):
{ "subject": { "type": "group", "id": "grp_01HZY..." }, "relation": "editor"}For tuple-to-userset relations (set a document's parent folder):
{ "subject": { "type": "folder", "id": "internal" }, "relation": "parent"}Creating an already-existing relation is a no-op (idempotent).
Check (the central question)
Section titled “Check (the central question)”POST /api/v1/resources/checkContent-Type: application/jsonRequired scope: resources:read
{ "subject": { "type": "user", "id": "usr_01HZX..." }, "action": "edit", "resource": { "type": "document", "external_id": "doc_42" }}The platform walks the rewrite graph for the action on the type, evaluates each branch, and answers:
{ "data": { "allowed": true, "reasons": [ { "via": "editor", "rewrite": "computed: owner" } ] }}reasons is optional debug output explaining why the answer is yes (or empty when no, indicating which paths were tried). For production use, ignore reasons and just read allowed.
The action maps to a relation by convention — edit checks editor, view checks viewer, delete checks owner. You can configure custom action → relation mappings per resource type.
Batch check
Section titled “Batch check”POST /api/v1/resources/check-batchContent-Type: application/json
{ "subject": { "type": "user", "id": "usr_01HZX..." }, "checks": [ { "action": "edit", "resource": { "type": "document", "external_id": "doc_42" } }, { "action": "edit", "resource": { "type": "document", "external_id": "doc_43" } }, { "action": "delete", "resource": { "type": "document", "external_id": "doc_42" } } ]}Returns an array of { allowed: boolean } in the same order. Useful for rendering a list view that filters by permission — one round trip instead of N.
List relations
Section titled “List relations”GET /api/v1/resources/{type}/{external_id}/relationsRequired scope: resources:read
Query parameters: relation — filter to a specific relation name cursorReturns every direct relation on this resource. Computed/inherited relations are not listed (they live in the rewrite graph, not as rows).
Remove a relation
Section titled “Remove a relation”DELETE /api/v1/resources/{type}/{external_id}/relationsContent-Type: application/jsonRequired scope: resources:write
{ "subject": { "type": "user", "id": "usr_01HZX..." }, "relation": "editor"}Returns 204. Removing a non-existent relation is 404 relation_not_found.
Delete a resource
Section titled “Delete a resource”DELETE /api/v1/resources/{type}/{external_id}Required scope: resources:writeCascades: all direct relations targeting this resource are removed. Other resources that referenced this one through parent (or any tuple-to-userset) lose those edges automatically.
Cost model
Section titled “Cost model”Check calls are cheap (typically sub-millisecond). The platform caches the rewrite graph and the recent relation lookups in memory. For very high-throughput pages (rendering large permission-filtered lists), prefer check-batch over many serial check calls.
Adding and removing relations is also fast but contends on the relation index — burst writes of 1000+ relations on the same resource may hit per-resource serialisation. Spread bulk relation adds across resources or use the bulk-mutation endpoint:
POST /api/v1/resources/relations/bulkContent-Type: application/json
{ "operations": [ { "op": "create", "resource": {...}, "subject": {...}, "relation": "..." }, { "op": "delete", "resource": {...}, "subject": {...}, "relation": "..." } ]}Up to 500 operations per call; applied atomically (all or nothing per call; multiple bulk calls are not interleaved-safe).
Common errors
Section titled “Common errors”| Error | When |
|---|---|
type_not_found | The resource type isn't defined |
relation_unknown | The relation name isn't in the type's definition |
subject_invalid | The subject type isn't a recognised type |
cycle_detected | A tuple_to_userset rewrite would create an infinite walk |
resource_not_found | Delete / check against a resource that doesn't exist |