The authorization problem
As applications grow, rigid permission models collapse under the weight of real-world relationships. Authorization becomes a maintenance nightmare.
Roles like admin, editor, and viewer work
fine for small teams. But sharing a document with a specific user, a group,
or temporarily requires new roles for every combination — leading to
role explosion.
✗ Breaks when permissions need context
Policies like resource.owner == user.id || user.dept IN allowed_depts
encode business logic into evaluation engines. Policies grow complex,
become hard to audit, and require deep knowledge to change safely.
✗ Hard to reason about at runtime
Permissions flow through a graph of relationships.
alice owns document:readme because
the tuple document:readme#owner@user:alice exists —
no role explosion, no complex policies, just data.
✓ Self-evident, composable, auditable
The Zanzibar model
Google's Zanzibar paper (2019) described the authorization system powering Drive, Maps, and YouTube — serving billions of ACL checks per second. Kraalzibar brings that model to your infrastructure.
At its core, the model is simple: a tuple
object#relation@subject
asserts that a subject holds a relation to an object.
Permissions are then evaluated by traversing the graph of these tuples — with
support for union, intersection, exclusion, and indirect relations through
arrow operators.
Relations can refer to other relations. A group's members inherit access granted to the group — no duplication needed.
Snapshot tokens prevent the "new enemy" problem: a write returns a token; reads can be pinned to that exact snapshot.
Types, relations, and permissions are declared in a DSL. Breaking changes are rejected unless you force-override them.
Ask "can Alice view this?" and "who can view this?" and "what can Alice view?" — all natively supported.
// object#relation@subject document:readme#owner@user:alice document:readme#viewer@user:bob document:readme#viewer@group:eng#member group:eng#member@user:carol // Permission checks evaluate the graph: // alice → view document:readme? ✓ allowed // bob → edit document:readme? ✗ denied // carol → view document:readme? ✓ allowed (via group:eng)
"subject": "user:carol", "permission": "view", "resource": "document:readme" → traversal path: user:carol └─ member of group:eng └─ viewer of document:readme └─ view = viewer + owner ✓ ALLOWED
Model comparison
Each model has its place — but when you need fine-grained, contextual, scalable access control, ReBAC is the clear choice.
document:quarterly-report with a specific external contractor requires a new role, say contractor-doc-viewer-12
A direct tuple: document:quarterly-report#viewer@user:contractor
resource.owner == subject.id || (subject.clearance >= resource.level && !resource.restricted)Permission logic is declared in the schema, not buried in policy strings:
permission view = viewer + owner — composable, readablecheck — does subject have permission on object?lookup_resources — what can this subject access?lookup_subjects — who can access this resource?expand — debug the full permission treeSchema DSL
// Define your object types definition user {} definition group { relation member: user } definition folder { relation owner: user relation editor: user | group#member relation viewer: user | group#member permission view = viewer + editor + owner permission edit = editor + owner permission delete = owner } definition document { relation parent: folder relation owner: user relation viewer: user | group#member // Inherit folder permissions via arrow → permission view = viewer + owner + parent->view permission edit = owner + parent->edit permission delete = owner }
The schema DSL maps directly to the authorization semantics. What you write is what gets checked.
Declares an object type (user, document, group).
Every resource in your system maps to a defined type.
A named edge in the graph. viewer: user | group#member
means the viewer can be a direct user or any member of a group.
A computed boolean from union (+), intersection (&),
and exclusion (-) of relations and other permissions.
parent->view traverses the parent relation
and checks view on the target — enabling permission inheritance.
Breaking change protection: WriteSchema validates
the new schema against existing tuples. Removing a type or relation that's
in use is rejected unless you pass force: true.
Features
Memory-safe, zero-overhead, blazing-fast. Edition 2024 with native async traits. No GC pauses in your hot check path.
Each tenant gets a separate PostgreSQL schema. Data, schema, and API keys are fully isolated — share one cluster, zero cross-tenant leakage.
Both protocols, consistent behavior. Rust, Go, and TypeScript SDKs
included. Protobuf definitions are versioned under proto/.
Writes return a zed token. Subsequent reads can be pinned to that snapshot — preventing the "new enemy" problem of stale cache reads.
Schema cache with TTL + local invalidation on write. Check cache keyed by snapshot token — immutable entries never go stale.
Request counters, latency histograms, and cache hit/miss rates at
/metrics. Drop-in for Grafana dashboards.
Optional OTLP export (feature-gated). Per-request spans for
check, lookup, and schema operations.
Structured JSON events for every schema write, relationship write, and authentication attempt. Routed to stdout — 12-factor compliant.
Pre-built images on Docker Hub. Full docker compose stack with
PostgreSQL, Prometheus, Tempo, Grafana, and OTel Collector included.
Quick start
No database needed to start. The dev mode runs entirely in-memory with no authentication required.
Build and run with Cargo or pull the Docker image. Starts on gRPC :50051 and REST :8080.
# From source cargo run --release # Docker (dev mode — in-memory, no auth) docker run -p 50051:50051 -p 8080:8080 \ pubkraal/kraalzibar-server:latest
Define your types, relations, and computed permissions.
curl -s -X POST http://localhost:8080/v1/schema \ -H 'Content-Type: application/json' \ -d '{ "schema": "definition user {}\n\ndefinition document {\n relation owner: user\n relation viewer: user\n permission view = viewer + owner\n permission edit = owner\n}" }' # Response: { "breaking_changes_overridden": false }
Write tuples to grant access. Returns a snapshot token for causal consistency.
curl -s -X POST http://localhost:8080/v1/relationships/write \ -H 'Content-Type: application/json' \ -d '{ "updates": [ { "operation": "touch", "resource_type": "document", "resource_id": "readme", "relation": "owner", "subject_type": "user", "subject_id": "alice" }, { "operation": "touch", "resource_type": "document", "resource_id": "readme", "relation": "viewer", "subject_type": "user", "subject_id": "bob" } ] }' # Response: { "written_at": "1" }
Ask whether a subject has a permission on a resource. Fast, recursive graph traversal.
curl -s -X POST http://localhost:8080/v1/permissions/check \ -H 'Content-Type: application/json' \ -d '{ "resource_type": "document", "resource_id": "readme", "permission": "edit", "subject_type": "user", "subject_id": "alice" }' { "allowed": true, "checked_at": "1" } ← alice is owner → can edit curl -s -X POST http://localhost:8080/v1/permissions/check \ -H 'Content-Type: application/json' \ -d '{ ..., "subject_id": "bob", "permission": "edit" }' { "allowed": false, "checked_at": "1" } ← bob is only viewer
Ask who has a given permission on a resource — or which resources a subject can access.
# Who can view document:readme? curl -s -X POST http://localhost:8080/v1/permissions/subjects \ -d '{ "resource_type": "document", "resource_id": "readme", "permission": "view", "subject_type": "user" }' { "subjects": [ { "subject_type": "user", "subject_id": "alice" }, { "subject_type": "user", "subject_id": "bob" } ], "looked_up_at": "1" }
Provision tenants, create API keys, and connect persistent storage. Each tenant is fully isolated.
# 1. Run database migrations kraalzibar-server migrate --config config.toml # 2. Provision a tenant kraalzibar-server provision-tenant --name acme --config config.toml # 3. Create an API key kraalzibar-server create-api-key --tenant-name acme --config config.toml # → kraalzibar_a1b2c3d4_s5t6u7v8w9x0y1z2a3b4c5d6e7f8 # 4. Use the key in requests curl -X POST http://localhost:8080/v1/permissions/check \ -H 'Authorization: Bearer kraalzibar_a1b2c3d4_...' \ -d '{ ... }'
Architecture
Kraalzibar is a Cargo workspace with four crates and three client SDKs. The layered design keeps the domain logic free from transport and storage concerns.
| Crate / SDK | Responsibility |
|---|---|
| kraalzibar-core | Schema DSL parser, graph engine, domain types |
| kraalzibar-storage | Storage trait, InMemory + PostgreSQL backends |
| kraalzibar-server | gRPC + REST handlers, config, metrics, CLI |
| kraalzibar-client | Rust gRPC SDK |
| sdks/go | Go gRPC SDK |
| sdks/typescript | TypeScript REST SDK |
12-factor compliant.
All configuration via environment variables (KRAALZIBAR_ prefix).
Structured JSON logs to stdout. Stateless server — all state in PostgreSQL.
Engine limits. Max traversal depth 6. Schema caps: 50 types, 30 relations, 30 permissions per type. Lookup limit 1000 results. Configurable.