Secure Backward Cursor Pagination with Chronological Pages
Design backward keyset pagination that returns chronological pages without gaps while preventing cursor replay across conversations, filters, directions, authorization scopes, ordering rules, or snapshots.
Secure Backward Cursor Pagination with Chronological Pages
Trigger
Use this guidance when an API loads older records in descending keyset order, fetches page size plus one, returns each page chronologically, and accepts a user-controlled cursor that can remain in UI state after its conversation or filters change.
Practical steps
- Define one stable, unique ordering tuple. Add an immutable tie-breaker when the primary sequence can collide.
- On the first request, authenticate the caller, independently authorize the requested conversation, derive canonical effective filters after defaults and access restrictions, fix the backward direction and ordering version, and establish a snapshot.
- Fetch page size plus one matching rows in descending order within the snapshot. If overflow exists, remove the final descending row, then reverse the retained rows for chronological output.
- Emit a continuation cursor only when overflow proves more rows exist. Set its exclusive position to the oldest retained row, never the discarded lookahead.
- Preserve the conversation, canonical filters, direction, authorization scope, ordering version, snapshot, purpose, and expiry in every continuation. Advance only the exclusive position.
- Treat the cursor as hostile input. Use a signed random handle with server-side state or authenticated encryption. A readable signed payload prevents alteration but does not conceal internal identifiers or sensitive filter state.
- Before querying, verify structure, authenticity and expiry, reauthorize the currently requested conversation independently, recompute current bindings, compare every binding, and validate snapshot availability. Derive the query only from validated server-side values.
- Reject malformed, altered, expired, or context-mismatched cursors with one generic external error. Do not reveal identifiers, expected bindings, or the mismatch reason.
Limits
A high-water ordering tuple excludes later inserts only when records are append-only and filter membership is immutable. Edits, deletions, mutable ordering, or changing filter membership require temporal reads or a durable result-set snapshot. Expired snapshot state requires a fresh walk. Cursor authenticity never replaces authorization.
Failure prevented
This prevents skipped or duplicated boundary rows, false continuation at the end, cross-conversation or cross-filter replay, direction and authorization confusion, snapshot drift, and disclosure of internal identifiers through tokens or error details.
Support
The support is reasoned sequence and security analysis, including an independent reasoning review. No implementation or executable tests were run.