Skip to content

Resources API

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.

  • 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... perform edit on document: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.

POST /api/v1/resource-types
Authorization: Bearer <access-token>
Content-Type: application/json
Required 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:

  • owner is a direct relation. Either a relation exists or it doesn't.
  • editor is granted by this (someone marked directly) OR by computing from owner (every owner is an editor).
  • viewer is granted directly OR by being an editor.
  • parent is a direct relation linking a document to a folder.
  • inherit-from-parent walks: take whoever this document's parent is, then ask who has editor on that parent.
POST /api/v1/resources
Content-Type: application/json
Required 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.

POST /api/v1/resources/{type}/{external_id}/relations
Content-Type: application/json
Required 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).

POST /api/v1/resources/check
Content-Type: application/json
Required 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.

POST /api/v1/resources/check-batch
Content-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.

GET /api/v1/resources/{type}/{external_id}/relations
Required scope: resources:read
Query parameters:
relation — filter to a specific relation name
cursor

Returns every direct relation on this resource. Computed/inherited relations are not listed (they live in the rewrite graph, not as rows).

DELETE /api/v1/resources/{type}/{external_id}/relations
Content-Type: application/json
Required scope: resources:write
{
"subject": { "type": "user", "id": "usr_01HZX..." },
"relation": "editor"
}

Returns 204. Removing a non-existent relation is 404 relation_not_found.

DELETE /api/v1/resources/{type}/{external_id}
Required scope: resources:write

Cascades: 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.

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/bulk
Content-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).

ErrorWhen
type_not_foundThe resource type isn't defined
relation_unknownThe relation name isn't in the type's definition
subject_invalidThe subject type isn't a recognised type
cycle_detectedA tuple_to_userset rewrite would create an infinite walk
resource_not_foundDelete / check against a resource that doesn't exist