Using SpiceDB Caveats for Attribute-Based Access Control
Learn how SpiceDB caveats let you add attribute‑based checks without bloating the relation graph, with a concrete tenant‑aware example and trade‑offs.
04 Aug 2025, 18:35 UTC

Problem: Need fine‑grained checks without exploding the relation graph
When building multi‑tenant SaaS applications, permission checks often depend on attributes that change per request—such as the caller’s tenant ID, the current time, or a resource tag. Storing a separate relation for every possible attribute value quickly inflates the graph and makes schema evolution cumbersome. SpiceDB’s caveat feature lets you keep the relation graph compact by moving the attribute test into the permission evaluation step.
How caveats work in SpiceDB
A caveat is a named CEL expression that SpiceDB evaluates at check time using a context map supplied by the caller. If the expression returns true, the permission is granted; otherwise it is denied. The caveat is attached to a relation in the schema, so the same relation can be reused for many different attribute values.
# schema.example
caveat tenant_id() {
return request.context.tenant == object.tenant_id
}
definition document {
relation viewer: user
permission view = viewer + tenant_id()
}
In this example the tenant_id() caveat reads the tenant field from the request context and compares it to the tenant_id attribute stored on the document object.
Worked example: tenant‑aware document viewer
- Start a local SpiceDB instance (run wherever you have Docker access):
docker run -d -p 50051:50051 --name spicedb authzed/spicedb:latest - Load the schema using the
zedCLI (replace$SCHEMA_FILEwith the path to the file above):zed schema write --in-line "$(cat $SCHEMA_FILE)" --endpoint localhost:50051 - Write a tuple that relates a user to a document as a viewer:
zed tuple write \\n --operation WriteTuples \\n --mutation "user:123#viewer@document:456" \\n --endpoint localhost:50051 - Run a check with a satisfying context (tenant ID matches the object):
zed check \\n --object "document:456" \\n --permission view \\n --subject "user:123" \\n --context "{\"tenant\":\"acme\"}" \\n --endpoint localhost:50051 - Run a check with a non‑matching context (different tenant):
zed check \\n --object "document:456" \\n --permission view \\n --subject "user:123" \\n --context "{\"tenant\":\"other\"}" \\n --endpoint localhost:50051
The first check should return true because the caveat expression evaluates to true; the second should return false. You can verify the result by examining the exit code of the zed command or the JSON field permissionship in the response.
Trade‑offs and limitations
- Evaluation overhead: each active caveat adds a CEL evaluation step. Keep expressions simple—avoid loops, large collections, or expensive functions.
- Context correctness: the permission outcome depends entirely on the context you supply. A missing or miss‑typed field will cause the caveat to evaluate to false, leading to unexpected denials. Automated tests that vary the context map are essential.
- CEL subset: SpiceDB only supports a safe subset of CEL (no external variable access, no complex macros). If you need more elaborate logic you may need to model it as additional relations instead.
Actionable closing
If your attribute checks are low‑cardinality and change frequently, start by prototyping a caveat in a sandbox SpiceDB instance. Measure latency with a realistic request volume, and compare it to the alternative of duplicating relations per attribute value. When the latency stays within your SLA and the context mapping is covered by unit tests, promote the caveat‑based schema to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.