Skip to main content
Every credential (API key or OAuth grant) carries three permission maps. Reads and writes are enforced against them. Admin credentials bypass all checks.

The four maps

{
  "type_permissions":      { "<type_pattern>": "read" | "write" | "none" },
  "extension_permissions": { "<namespace_pattern>": "read" | "write" },
  "edge_permissions":      { "<edge_type_pattern>": "read" | "write" },
  "metadata_permissions":  { "<sub_resource>": "read" | "write" }
}

type_permissions

Controls read and write access to item types. Patterns support * wildcard and parent.* subtree matching.
{
  "core.note": "write",
  "core.bookmark.*": "read",
  "my-app.session": "write",
  "*": "none"
}
Resolution: most specific pattern wins. core.bookmark.* beats *; core.bookmark.readwise beats core.bookmark.*. Reads inherit down the type chain — read core.media returns core.media.* items (book, article, film, etc.). Writes don’t — writing to an inheriting type requires permission on the specific type. This prevents cross-app write-through on inheriting types.

extension_permissions

Controls which extension namespaces the credential can read or write. Extensions are not returned by default — a namespace must be explicitly permitted.
{
  "my-app.*": "write",
  "readwise-reader.reading_progress": "read"
}
A credential with extension_permissions: {} cannot read or write any extension.

edge_permissions

Controls which edge types the credential can create, update, or delete.
{
  "about": "write",
  "parent-of": "write",
  "in-thread": "read",
  "*": "none"
}
New credentials default to edge_permissions: {} — edge access is opt-in; apps must declare each edge type they need.

metadata_permissions

Controls metadata-layer mutations that don’t fit the type / extension / edge axes — currently registering custom types via POST /types.
{
  "types": "write"
}
Keys are sub-resource names; the only one enforced today is types (gates POST /types for non-admin credentials). New credentials default to metadata_permissions: {}. OAuth tokens project the metadata.<sub>:<verb> scope into this map on the synthetic credential — a token granted metadata.types:write carries metadata_permissions: { types: "write" }. Admin credentials bypass.

The dual-gate rule

Edge writes require permission on both the source item’s type AND the edge type. Creating an about edge from a core.note to a core.entity.person needs:
  • type_permissions: write on core.note (the source item).
  • edge_permissions: write on about.
Either missing → 403 edge_permission_denied. Admin credentials bypass both. The rationale: an edge is a mutation of the source item’s graph view. Without type write, the app can’t mutate the item; adding an edge would be a back-door around that. Without edge write on the type, the app can’t create that kind of relationship — even if it owns the item.

Enforcement points

ActionChecks
GET /items?type=Xtype_permissions[X]: read (or inheriting pattern)
POST /items (create X)type_permissions[X]: write
PATCH /items/{id} (update X)type_permissions[X]: write
DELETE /items/{id}type_permissions[X]: write
POST /items/{id}/transitiontype_permissions[X]: write
POST /items with edgestype_permissions[X]: write AND edge_permissions[edge_type]: write per edge
POST /edgestype_permissions[source.type]: write AND edge_permissions[edge_type]: write
DELETE /edges/{id}same as POST /edges
PATCH /edges/{id}same as POST /edges
GET /items/{id}/extensions/{ns}extension_permissions[ns]: read
PUT /items/{id}/extensions/{ns}extension_permissions[ns]: write

Default tier on credentials

Credentials carry a default_tier field — library or feed — that stamps the tier on items written by the credential when the client omits one. Clients can override per-write. There is no read-side tier-gate on the credential; tier filtering is a query concern (GET /items?tier=feed), not a permission boundary.

Patterns and practice

  • A single app typically needs ~3–5 types and one extension namespace. A notes app might have type: write on core.note, metadata: write, edge.about: write, edge.parent-of: write, and extension.notes-app.*: write.
  • Publishing destinations tend to be narrower than capture apps. A read-later service mostly writes core.bookmark.* and core.highlight; it needs edge write on references.
  • Start tight. It’s easier to add scopes than audit what’s actually in use later.

Revocation effect

Revoking a credential returns 401 unauthorized on the next request. In-flight long-lived connections (SSE) terminate on the next heartbeat failure — there’s no active push. The audit trail captures the revocation event.