# Secure backward pagination with context-bound cursors

Prevent gaps, cross-query cursor reuse, internal-state disclosure, and unstable snapshots when backward keyset pages are returned chronologically.

Exact reference: {"kind":"skill_version","skill_id":"skl_FL2WVf2TdPZzv6zYXgRLlQ","version_id":"skv_HwUYSz6q5zX-qZjfmTQjZQ"}

Applicability: []

# Secure backward pagination with context-bound cursors

## Trigger

Use this when storage is read newest-first with page size plus one, responses are returned oldest-first, and a client-held cursor may survive changes to the selected resource, filters, or direction.

## Practical steps

1. Query by the complete unique sort key in descending order for page size plus one.
2. Detect overflow before changing order. Remove the final fetched row, which is the oldest lookahead.
3. Build the exclusive next position from the oldest retained row, then reverse only the retained rows for chronological output. Omit the cursor when there is no lookahead.
4. Protect token version, endpoint, authenticated security boundary, canonical resource, canonical filters, direction, sort policy, snapshot, complete keyset position, and expiry.
5. Keep the entire claim set opaque. Use authenticated encryption, or a random server-side handle of at least 128 bits authenticated before lookup. A readable signed payload provides integrity but not confidentiality.
6. On continuation, authenticate the token before trusting fields, recompute every request binding, reauthorize current access, and apply both the snapshot and exclusive position.
7. Return one generic invalid-cursor response for malformed, expired, mismatched, missing, or unauthorized state. Never echo or log tokens, claims, identifiers, or differing values.
8. Bound telemetry with a closed reason vocabulary and at most one warning per request. Permit only fixed dimensions such as endpoint and service version.

## Snapshot requirement

A maximum display key is not automatically a stable snapshot. Backdated inserts, equal-key inserts, or keys allocated before commit can become visible later below the cursor. For append-only data, use a committed insertion watermark whose order matches visibility and constrain every page to the initial watermark. For stronger consistency, use an MVCC as-of view or materialized result. Freezing edits and deletes requires those stronger mechanisms or versioned rows.

## Auxiliary effects

Keep background publication or sharing outside the primary pagination path. Absence of an acknowledgement does not prove that an external effect did not commit before a worker crash. If a worker can change visibility, use an authoritative durable gate or an idempotent durable outbox with status reconciliation and fencing. Let the primary request finish after a worker deadline, but do not infer sharing permission from an ambiguous outcome.

## Limits

The design assumes a deterministic immutable composite order. Canonicalization must exactly match query semantics, including defaults, absence, types, duplicate handling, and ordered versus set-like values. Client-side cursor clearing improves usability but never replaces server validation. A high-water insertion watermark alone does not freeze updates or deletions.

## Failure prevented

This prevents skipped boundary rows, duplicate traversal, reuse of a valid cursor against another resource or filter set, disclosure of sensitive cursor state, phantom rows caused by weak snapshots, high-cardinality diagnostic leakage, and incorrect fail-open behavior after an auxiliary worker crashes.

## Supporting basis and limitations

Supported by reasoned analysis in the cited conversation, including an independent reasoning review. No executable tests were run.

## Change and rationale

Adds a compact design for overflow trimming, opaque signed cursor bindings, commit-ordered snapshots, generic mismatch rejection, bounded diagnostics, and crash-safe auxiliary effects.

A client-held position alone is unsafe when the user can switch resources or filters, and a display-key high-water mark can admit late-visible rows. The combined invariants are reusable across message timelines and similar append-oriented feeds.
