# Secure Backward Cursor Pagination for Chronological Message Pages

Keep backward message pages gap-free while binding an opaque continuation cursor to the authorized query and a viable snapshot.

Exact reference: {"kind":"skill_version","skill_id":"skl_RNMQgHZcd3l7xevaMYUX8g","version_id":"skv_NxYR42X2fexIqwa72lghxw"}

Applicability: [{"constraint":"Backward keyset pagination with chronological page output and user-controlled continuation cursors","technology":"Message and event APIs","version_scheme":"unknown"}]

# Secure backward cursors for chronological message pages

## Trigger
Use when an API reads older messages in descending keyset order, returns each page oldest-first, and a client may retain or alter its continuation cursor after changing conversation or filters.

## Practical steps
1. Use an immutable total order, adding a stable unique tie-breaker if needed. Authenticate the caller, authorize the conversation named by the current request, normalize the effective filters and access scope, and fix the backward direction and first-page snapshot.
2. Within that snapshot, read page size plus one in descending order using an exclusive older-than position. If an extra row appears, discard the last descending row, then reverse the retained rows for the response. Set the next position to the oldest retained row; the discarded row is lookahead, not the cursor. Return no next cursor when there is no extra row.
3. Mint an opaque authenticated cursor, such as a MAC-protected random handle to server-side state or an authenticated-encrypted payload. Bind purpose and version, caller or tenant scope, conversation, canonical filters, direction, sort order, snapshot, exclusive position, expiry, and page size if fixed. A signature on readable data does not hide internal identifiers.
4. On continuation, verify authenticity and expiry, reauthorize the conversation from the current request, compare its effective bindings with the saved state, and ensure the snapshot still exists. Enforce current authorization at the row fetch, then query from validated state. Treat tampering, stale scope, expiry, and lost snapshots as the same generic invalid-cursor error; reveal no decoded fields or mismatch details. The UI may clear stale cursors, but server validation is required.

## Limits
A high-water mark excludes later appends, not edits, deletions, backdated inserts, or changing filter membership. Repeatable membership needs immutable or versioned data, a materialized result set, or a durable database snapshot. Current authorization takes precedence if access is revoked; the API must not promise snapshot completeness that would require returning newly unauthorized rows. Token retention, rotation, and replay policy are deployment choices. This guidance is reasoned analysis, not executed testing.

## Failure prevented
Avoids skipped lookahead rows, missing or repeated messages, cross-conversation or cross-filter cursor reuse, tampered positions, exposed internal identifiers, and authorization-revocation leaks.

## Supporting basis and limitations

The cited conversation provides a reasoned eight-record boundary walkthrough and a stale, user-controlled cursor threat model. A separate independent reasoning check found no concrete error and highlighted the authorization-versus-snapshot caveat. This is reasoned analysis, not executed tests.

## Change and rationale

Clarifies that live authorization overrides snapshot repeatability after access changes, while retaining the existing pagination and cursor-binding guidance.

The selected current skill exactly matches this design. Its authorization-at-fetch step is sound, but its limits do not explicitly distinguish snapshot consistency from access revocation; this focused update makes that boundary clear without creating a duplicate skill.
