Zanzibar permissions guide
assay-engine v0.2.0's auth-zanzibar module implements Google's
Zanzibar paper as a Keto / SpiceDB drop-in: namespaces, relation tuples, and a recursive-CTE
walk that resolves check, expand, lookup_resources,
and lookup_subjects on PG18 + SQLite.
Mental model
Three concepts:
- Namespace — a resource type (
doc,folder,group,user) with a schema declaring its relations and how relations rewrite into other relations. - Tuple — a
(object, relation, subject)triple. e.g.doc:doc-42#owner@user:alicemeans "alice is owner of doc-42". - Userset — a subject that's itself a relation on another object. e.g.
doc:doc-42#viewer@group:eng#membermeans "members of the eng group are viewers of doc-42".
Define a namespace schema
Schemas declare relations and computed-relation rewrites. Example: a document with owners, editors, and viewers, where every owner is automatically also an editor and every editor is automatically a viewer.
local auth = require("assay.auth")
local c = auth.client({
engine_url = "http://localhost:3000",
api_key = env.get("ASSAY_ADMIN_KEY"),
})
c.zanzibar:define_namespace({
name = "doc",
relations = {
owner = {},
editor = { union = { "this", { computed_userset = "owner" } } },
viewer = { union = { "this", { computed_userset = "editor" } } },
},
})
Or write the equivalent JSON to auth.zanzibar_namespaces via the admin HTTP
API. The schema is JSON-serialised; the parser validates it on write.
Write tuples
-- alice owns doc-42
c.zanzibar:write({
object_type = "doc", object_id = "doc-42", relation = "owner",
subject_type = "user", subject_id = "alice",
})
-- the eng group's members are viewers of doc-42
c.zanzibar:write({
object_type = "doc", object_id = "doc-42", relation = "viewer",
subject_type = "group", subject_id = "eng", subject_rel = "member",
})
-- bob is a member of the eng group
c.zanzibar:write({
object_type = "group", object_id = "eng", relation = "member",
subject_type = "user", subject_id = "bob",
})
Check a permission
local can_view = c.zanzibar:check("doc", "doc-42", "viewer", "user", "bob")
-- true: bob ∈ eng#member ⊆ doc-42#viewer
local can_edit = c.zanzibar:check("doc", "doc-42", "editor", "user", "bob")
-- false: bob is only a viewer transitively
local alice_owns = c.zanzibar:check("doc", "doc-42", "owner", "user", "alice")
-- true: direct tuple
The check resolves via a single recursive CTE — no application-side traversal, no N+1. Identical SQL on PG18 and SQLite (recursive CTE is in both standards).
Expand a userset
local tree = c.zanzibar:expand("doc", "doc-42", "viewer")
-- {
-- relation = "viewer",
-- children = {
-- { type = "this", subjects = { "user:bob (via group:eng#member)" } },
-- { type = "computed_userset", relation = "editor", children = {
-- { type = "computed_userset", relation = "owner", children = {
-- { type = "this", subjects = { "user:alice" } } } } } },
-- },
-- }
HTTP surface
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/zanzibar/check | Resolve (object, relation, subject) → bool |
| POST | /auth/zanzibar/expand | Return the userset tree for a relation |
| POST | /auth/zanzibar/write | Insert a tuple (admin) |
| DELETE | /auth/zanzibar/tuples | Delete a tuple (admin) |
| GET | /auth/admin/auth/zanzibar/namespaces | List all namespaces |
| GET | /auth/admin/auth/zanzibar/namespaces/{name} | Get a single namespace schema |
Storage
auth.zanzibar_namespaces (
name TEXT PRIMARY KEY,
schema_json TEXT -- serialised NamespaceSchema
)
auth.zanzibar_tuples (
object_type TEXT,
object_id TEXT,
relation TEXT,
subject_type TEXT,
subject_id TEXT,
subject_rel TEXT -- NULL for direct subjects, set for usersets
)
Indexed both forward ((object_type, object_id, relation, ...)) and reverse
((subject_type, subject_id, relation)) so check and
lookup_resources/lookup_subjects are both fast.
Dashboard
The Zanzibar pane in /auth/console exposes:
- Namespace browser (read schemas)
- Tuple inspector with filter (
WHERE object_type = '...' AND ...) - Check evaluator — interactive "does subject X have permission Y on resource Z?"
- Expand viewer — the userset tree as a collapsible widget
See Auth & IdP overview for the rest of the v0.2.0 IdP surface, and Compare vs Ory for how this stacks up against Keto.