launch-quickly
Decisions

0010 — Offset pagination, deliberately

Cursor pagination is better at scale and worse at everything a starting product needs.

Context

Cursor pagination is the technically superior answer and every engineer knows it. It is stable under insertion, it does not degrade at high offsets, and it is what you would choose for a feed with a million rows.

Decision

Offset pagination, chosen knowingly.

Cursor pagination is better at scale and worse at everything a starting product needs: jumpable page numbers, shareable URLs, a total count for "page 3 of 12".

A product with three hundred invoices needs "page 3 of 12" far more than it needs keyset stability. A product with three hundred thousand needs the opposite — and by then it has engineers who can make that change.

Consequences

The migration path is the reason this is defensible rather than merely pragmatic. Paginated<T> is the only thing callers see, so the strategy can change without touching call sites. The decision is reversible at a known cost, and that is recorded here so the next person does not have to rediscover it.

nuqs carries the page in the URL (0011), which is what makes "shareable URLs" true rather than aspirational.

When to revisit

When any of these becomes true, and not before:

  • a list routinely exceeds a few thousand rows per tenant
  • count(*) shows up in slow query logs
  • rows are inserted fast enough that page 2 visibly skips or repeats items

None of these is a matter of taste, which is the point of writing them down: the trigger is measurable, so the argument does not have to be had again from first principles.

On this page