# Backward pagination with opaque scope-bound cursors
Use this pattern when the datastore fetches newest first, the API returns each page oldest first, and continuation tokens are controlled by clients that may change conversations or filters.
## Ordering invariant
1. Apply the current exclusive backward boundary, if present.
2. Query in descending total order for page size plus one rows.
3. If more than page size rows arrive, remove the final row from the descending buffer. It is the oldest fetched row and only a lookahead.
4. Derive the next boundary from the oldest retained row, never from the discarded lookahead.
5. Reverse the retained rows for chronological output.
6. The next query uses a strict earlier-than comparison against the retained boundary.
Use a composite boundary such as sequence plus a unique tie-breaker whenever sequence alone is not unique.
## Make the cursor authenticated and opaque
Signing plaintext prevents modification but does not hide identifiers. If thread, snapshot, or boundary identifiers must remain private, use one of these designs:
- A vetted authenticated-encryption format whose external token contains only a format version, key selector, nonce, ciphertext, and authentication tag.
- A cryptographically random opaque handle whose server-side record contains the protected cursor state and has a bounded lifetime.
Do not use a readable signed JSON token when confidentiality is required. If a separate asymmetric signature is mandatory, sign the claims and then encrypt the signed object, or use an approved envelope that provides both authenticity and confidentiality.
Protect these claims:
- format version and cursor purpose or audience;
- thread binding derived from the server-resolved canonical thread scope;
- filter binding derived from a canonical representation of every semantic filter and default;
- direction and complete sort definition;
- immutable snapshot reference or high-water boundary;
- exclusive composite page boundary;
- issued-at and expiry times;
- authorization-context binding when cursors must not move between users or tenants;
- page-size policy when changing the requested size is not supported.
Internal identifiers may appear only inside encrypted state or server-side state. A keyed binding digest is another option. Never treat possession of a cursor as authorization.
## Canonicalize request scope
Parse and validate filters before binding them. Materialize semantic defaults, encode types unambiguously, sort set-valued inputs, preserve order where order is meaningful, normalize only where the API contract permits it, and version the canonicalization scheme. Hash the canonical bytes with domain separation. Include every query option that can change membership or order.
## First-page processing
1. Authenticate and authorize the requested thread through the normal endpoint rules.
2. Resolve the thread internally and canonicalize filters, direction, sort, and other query semantics.
3. Establish a snapshot.
4. Query the server-resolved scope in descending order with the snapshot constraint and limit plus one.
5. Trim overflow, derive the retained boundary, reverse for output, and issue an opaque authenticated cursor only when older rows remain.
For an append-only stream, a high-water ordering key can exclude later appends. If rows can be edited, deleted, or inserted below that key, use a durable revision, temporal read, or database snapshot that remains valid for the cursor lifetime.
## Continuation validation
1. Reject oversized or malformed tokens.
2. Select only a server-approved key and algorithm for the declared format version.
3. Authenticate and decrypt before trusting any claim.
4. Strictly validate schema, purpose, lifetime, and authorization-context binding.
5. Independently authorize the currently requested thread.
6. Recompute the thread, filter, direction, sort, and policy bindings from the current request and compare them with the protected claims.
7. Validate that the protected snapshot is still available and belongs to the same scope.
8. Only then query using the server-resolved request scope plus the verified snapshot and boundary.
Do not let protected claims silently replace conflicting request parameters. Reject the conflict. Do not query one scope and then filter results in application code.
Return one generic cursor error for tampering, scope mismatch, direction mismatch, unavailable snapshots, and expired cursors when the endpoint contract permits it. Do not name the expected thread, filter, snapshot, boundary, key, or failing field. Keep normal authorization failures non-enumerating. Redact cursor values from logs.
## Eight-message trace
For sequence values one through eight, page size three, backward direction, and a snapshot at eight:
- Fetch eight, seven, six, five. Remove lookahead five, return six, seven, eight, and protect boundary six with snapshot eight.
- A valid continuation fetches five, four, three, two. Remove two, return three, four, five, and protect boundary three with the same snapshot.
- The final continuation fetches two, one, returns one, two, and emits no cursor.
A later message nine is excluded by snapshot eight. Reusing the first cursor with another thread, different filters, or forward direction produces the same generic invalid-cursor response and performs no pagination query. Using discarded lookahead five as an exclusive boundary would skip message five.
## Operational notes
Use short bounded lifetimes and key rotation. Replay of a read-only stateless cursor normally repeats the same page and is not a forgery; require server-side single-use state only when replay itself is prohibited. Changing UI scope should proactively clear the cursor, but server validation remains mandatory because clients are untrusted.