Cursor Pagination and Z Schema Shifts in SpiceDB
SpiceDB cursor pagination scales list operations, but Z schema changes to cardinality or transitivity can silently flip Check decisions. this post explains the mechanics, the datastore coupling, and a practical verification workflow.
09 Aug 2026, 01:56 UTC

how list with cursor works
SpiceDB's List API returns a page of tuples and a cursor token. the cursor is opaque; the server uses it to resume fetching the next page without re-querying from the start. because it doesn't expose raw database IDs, you can iterate over thousands of tuples without pulling the entire relationship graph into memory and without leaking internal identifiers to callers. this is the mechanism that lets applications show permission panels at scale.
when schema changes flip decisions
The hidden risk lives in how Z schemas define relationship cardinality and transitivity. altering a relation from many-to-one to one-to-many—or toggling a transitive closure—can change the outcome of a Check call for the same input tuples. since Check evaluates against the current relationship graph, a schema migration that rewrites what tuples are considered valid will shift allow/deny results without any code change. teams migrating Z schemas often skip re-running authorization checks against a snapshot of historical tuples, with the result that production access decisions shift unexpectedly after a deploy.
a concrete worked example
start with a minimal Z schema:
table user { relation member_of { project } }insert tuples: user:alice member_of project:alpha. a Check for alice can access project:alpha returns allow. now evolve the schema—add transitivity or change cardinality—and re-deploy. if you then Check the same tuple, the result may differ because the new schema interprets the relationship graph differently. the research outlines a practical verification path: write a script that defines this schema, inserts the tuples, calls Check for specified cases, and asserts the returned boolean against expected results. running this script before and after a schema change confirms logical equivalence.
trade-offs and limitations
cursor pagination isn't datastore‑agnostic. the research flags that switching from PostgreSQL to SQLite will invalidate existing cursors and require you to re-fetch from the beginning. additionally, the gRPC API metadata and error handling differ between the open-source SpiceDB server and hosted instances, which can affect how clients parse retry-after or status codes. both are production realities that catch teams off-guard when they assume datastore‑independent behavior.
actionable closing
if you're using List with cursors in production, pair the strategy with a deliberate schema-change testing workflow. before deploying any Z schema update, replay a representative set of tuples through Check and confirm the decisions match your expectations. when a cursor breaks after a datastore migration, don't try to resume with the old token—simply re-fetch from the start and re-apply your schema version. and if compliance requires independent validation, export your Z schema and compare Check outcomes against an engine like OPA for the same input tuples; this is the verification step the research recommends as a logical-equivalence check.
the key takeaway: cursor pagination gives you scale, but only if you treat the cursor as datastore-dependent and treat every Z schema change as a potential authorization boundary shift. test both, version both, and your access control will stay both scalable and stable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.