# Secure backward pagination with scoped opaque cursors

Design gap-free backward pagination with chronological page output and hostile cursors bound to the exact authorized query and snapshot.

Exact reference: {"kind":"skill_version","skill_id":"skl_WBoLRIJ1UVflsR1Y_dMcwA","version_id":"skv_16I-pXRE4tg4fpj_lT9tWQ"}

Applicability: []

# Secure backward pagination with scoped opaque cursors

## Trigger

Use this guidance when an API fetches older records in descending order with one lookahead row, returns each page chronologically, and accepts a user-controlled cursor that may remain after the caller changes threads or filters.

## Failure prevented

This prevents gaps from discarding or resuming at the wrong row, duplicates from inconsistent ordering, cursor reuse across callers or query scopes, authorization bypass, internal identifier disclosure, snapshot drift, and retry races caused by mutable cursor state.

## Practical steps

1. Define one immutable total order. If sequence values can tie, add a unique tie-breaker and use the same tuple with lexicographic comparisons for ordering, snapshot boundaries, and resume predicates. Keep internal tie-breakers inside protected state.

2. On the first request, authenticate the caller, authorize the requested thread, derive the effective authorization scope and filters after defaults and policy restrictions, serialize filters canonically, and establish a snapshot.

3. Fetch page size plus one rows in descending order within that snapshot. When overflow exists, remove the final descending row, which is the oldest lookahead, before reversing the retained page. Build the exclusive next position from the oldest retained row, never from the discarded lookahead. Emit a cursor only when overflow proves more rows exist.

4. Expose a signed cryptographically random handle whose server-side record binds endpoint purpose, format version, caller and effective authorization scope, internal thread, canonical filter digest, backward direction, original snapshot, ordering definition, exclusive position, and expiration. Authenticated encryption is a stateless alternative. A readable signed payload has integrity but does not conceal identifiers.

5. On continuation, bound parsing, verify authenticity and expiry before trusting cursor state, independently reauthorize the current request, and recompute every scope binding from that request. Reject any mismatch before querying. Never let cursor claims select the thread or filters, and never treat a valid cursor as authorization.

6. Return one generic restart-pagination error for malformed, tampered, expired, missing, unauthorized, or wrong-context cursors. Do not reveal the failed binding or log raw cursor values.

7. Query with the original snapshot and an exclusive predicate below the protected resume tuple. Mint a fresh immutable cursor for each next page by copying the protected context and changing only the position. Do not mutate a shared handle, so retries and concurrent requests remain safe.

8. For sequences eight through one and page size three, fetch eight, seven, six, five; discard five; return six, seven, eight; and resume strictly below six. The discarded five remains eligible for the next page.

## Limits

A high-water order boundary excludes later monotonic inserts only when records are immutable and never backfilled below the boundary. Edits, deletions, backfills, or changes in filter membership require revisioned point-in-time reads or a materialized snapshot for strict repeatability. Cursor expiration must not exceed snapshot retention. Bind page size when changing it would alter the pagination contract.

## Support

This guidance is supported by a worked sequence trace, security analysis, and independent review conducted as reasoned analysis. No implementation, database, or executable tests were run.

## Supporting basis and limitations

The supporting conversation contains a worked eight-message sequence trace, a security analysis of user-controlled cursors across thread and filter changes, and an independent reasoning review. The support is reasoned analysis rather than executed tests.

## Change and rationale

Clarify lexicographic composite boundaries and require a fresh immutable cursor per page while retaining overflow trimming, opaque scope binding, generic rejection, and snapshot limits.

The current skill exactly matches the problem, so updating it is more accurate than creating overlapping guidance. The revision preserves its core design and adds the two conditional hardening points identified during independent reasoning review.
