Using SpiceDB Caveats for Time‑Restricted Access Control
Learn how to use SpiceDB caveats to enforce time‑restricted access without storing extra tuples, with a concrete schema example and latency considerations.
25 Apr 2026, 12:51 UTC

Problem: Time‑based access without extra tuples
Many applications need to grant or deny permission based on a user’s validity window (e.g., a contract start and end date). Modeling this with plain relationships would require a separate tuple for every moment in time or a nightly job to add/remove tuples, which is inefficient and error‑prone.
What are SpiceDB caveats?
A caveat is a conditional expression attached to a relation in the SpiceDB schema. When a Check request is made, the expression is evaluated at request time using values supplied in the request context and any attributes stored on the user or object. This lets you encode attribute‑based rules—such as time windows—without persisting extra tuples.
Defining a time‑window caveat
First, declare the caveat using the schema language. The CEL expression can reference context.time (the timestamp supplied in the check) and attributes on the user object, such as user.valid_from and user.valid_to.
def caveat valid_hours {
// true when the request time falls inside the user’s validity window
context.time >= user.valid_from && context.time <= user.valid_to
}
# object types
user: user
document: document
# relation with the caveat attached
view: view @valid_hours
Writing the data
Store the user’s validity window as attributes on the user object, then create the relationship that grants the view permission.
# user attributes (object type: user)
zed tuple write user:123 valid_from "2026-01-01T00:00:00Z"
zed tuple write user:123 valid_to "2026-12-31T23:59:59Z"
# relationship: user 123 can view document 456, subject to the caveat
zed tuple write user:123#view@document:456
Performing a check
Invoke the Check API with a context that includes the timestamp you want to test. The CLI command below shows how to do this; replace placeholders with your actual values.
zed check \
--token \
--schema \
user:123 view document:456 \
--context '{"time":"2026-06-01T12:00:00Z"}'
If the supplied time lies between the user’s valid_from and valid_to, the response will contain {@code "allowed": true}; otherwise it will be false. No extra tuples are consulted—the decision comes solely from the caveat evaluation.
Trade‑off: latency and schema versioning
- Per‑request cost. Because the caveat is evaluated for every check, a complex CEL expression or a large context payload can increase latency. Keep expressions simple and bound the size of the context to avoid CPU spikes under high QPS.
- Schema versioning. Changing the caveat definition creates a new schema version. Existing checks continue to use the old version until the schema is migrated, which may require a brief downtime or a rolling upgrade strategy.
How to verify the caveat is working
- Start a local SpiceDB instance, for example with Docker:
docker run -p 50051:50051 authzed/spicedb. - Load the schema containing the caveat:
zed schema set. - Write the attribute tuples and the relationship as shown above.
- Run two checks: one with a timestamp inside the window and one outside.
- Compare the {@code allowed} field; the inside check should be true, the outside false.
- Enable debug logging (
--log-level debug) to confirm that the caveat expression was evaluated once per request.
Next steps
If you need more sophisticated attribute‑based rules—such as checking a user’s department, a resource’s classification, or combining multiple attributes—extend the caveat expression to read those values from the context or from user/object tuples. Always monitor latency under realistic load and keep the expression simple to preserve the performance benefits of SpiceDB’s relational checks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.