Cursors
Keyset (cursor) pagination encodes the ordering values of the boundary row into an opaque, URL-safe string instead of an offset — so paging stays correct and fast even as rows are inserted or deleted. The codec lives in the Rust core, which makes cursors portable across languages.
A typed, deterministic format
The encoding is deterministic and typed: integers, strings, booleans, null, and
tagged temporal / decimal values all round-trip. A cursor is the ordered list of the
ORDER BY columns' values for the boundary row, serialized to JSON and base64url-
encoded. Typed scalars (datetime, date, decimal, uuid) are tagged so they decode back
to the same type, which is what lets keyset comparisons stay correct across languages.
The parity fixture pins the format byte-for-byte across Rust, Python, and TypeScript.
Encoding and decoding
TypeScript exposes the codec directly:
import { encodeCursor, decodeCursor } from "@cyblow/paginate";
const cursor = encodeCursor([42, "2025-06-01T00:00:00"]);
decodeCursor(cursor); // → [42, "2025-06-01T00:00:00"]
Try encodeCursor / decodeCursor live in the Playground — it runs
the same Rust codec in your browser.
In Python the same codec powers the SQLAlchemy and Django keyset adapters rather than
being called directly: the adapters over-fetch limit + 1, trim, and emit a
CursorPage with next_cursor / previous_cursor. See the
Python and TypeScript
pagination guides for end-to-end keyset examples.
Why it's shared
Because the codec is one implementation:
- a cursor minted by a Python service decodes in a TypeScript client and back;
- switching a service's implementation language never invalidates a client's existing cursors;
- every ORM adapter (SQLAlchemy, Django, Prisma, Drizzle) produces the same cursor bytes for the same ordering values.
The keyset predicate
For ORDER BY (a ASC, b DESC) and a cursor (v1, v2), the boundary comparison is:
(a > v1) OR (a = v1 AND b < v2)
The Rust core owns the structure of that OR-of-AND predicate (and flips the
directions for backward paging); each ORM adapter only renders the column OP value
comparisons in its own query builder. That's how every language agrees on exactly
which rows a cursor includes.