# Secure Backward Cursor Pagination for Chronological Message Pages

Page backward without gaps while making each continuation token valid only for its caller, query, and preserved snapshot.

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

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 fetches older records in descending keyset order, presents each page oldest-first, and a client can retain or alter a cursor after switching conversations or filters.

## Practical steps
1. Authenticate the caller and authorize the conversation in the current request. Canonicalize effective filters, including defaults; choose a stable total sort order and a first-page snapshot.
2. Fetch page size plus one under that scope in descending order, before the exclusive position. The final fetched row is lookahead: remove it, reverse the retained rows, and use the oldest retained row as the next exclusive position. Return no continuation when there is no lookahead.
3. Issue an opaque authenticated token, such as a MAC-protected random handle to server-side state. Bind its purpose and version, authenticated principal **and** tenant or authorization scope, conversation, canonical filters, direction and sort, snapshot, position, expiry, and page size when fixed. A MAC over readable identifiers gives integrity without confidentiality.
4. On continuation, verify the token and expiry, authorize the requested conversation again, and compare every saved binding with the current principal and request. Reject use by another caller even if that caller can independently read the conversation. Require a viable snapshot, then enforce the current authorization and saved query scope in the row fetch. Give tampering, mismatch, expiry, and lost snapshots one generic invalid-cursor response without echoing internal values. A UI may clear stale cursors, but the server must enforce the binding.

## Limits
A sequence high-water mark excludes later appends only when ordering and membership are immutable. Edits, deletions, backdated inserts, and changing filter membership need versioned data, a materialized result, or a durable as-of snapshot for repeatable pages. Revoked access overrides snapshot consistency. Token retention, rotation, and replay policy depend on the service. This procedure is reasoned design guidance, not an executed test result.

## Failure prevented
Prevents skipped lookahead rows, cross-conversation or cross-filter reuse, cross-caller token transfer within one tenant, altered positions, identifier disclosure, and continuation after authorization revocation.

## Supporting basis and limitations

The source conversation reasoned through an eight-message page boundary and a stale, user-controlled cursor after conversation or filter changes. An independent reasoning check identified the missing explicit principal binding. This support is design analysis, not executed tests.

## Change and rationale

Requires principal binding in addition to tenant or access scope, and explicitly rejects cross-caller reuse even when both callers can read the conversation.

The prior wording allowed binding to a tenant alone. A token could then be transferred to another authorized caller in that tenant, despite the corrected design treating the cursor as caller-bound untrusted input. The existing skill otherwise exactly matches the pagination problem, so a narrow update is appropriate.
