# Paginate Supabase reads before the table reaches its row cap

> A Supabase response can stop at the configured API row limit without looking broken. Paginate with a stable order so large dictionaries load completely.

Source: https://saascode.ai/inside/translation-paginate · Published: 2026-08-21 · Section: academy

---
A translation loader can be wrong for months without throwing an error. Everything works while the table is small. Then one locale pushes the row count past the API limit, the response stops at a clean boundary, and the rest of the interface falls back to raw keys.

This failure appeared in at least four builds across three review cycles. In those environments, the REST API returned at most 1,000 rows per response. Asking for `.range(0, 4999)` did not override the server's configured maximum.

Treat 1,000 as the measured configuration, not a law of nature. Supabase projects can change the API maximum. Your loader should know its page size, keep it at or below the configured cap, and paginate whenever the collection can outgrow one response.

## Offset pagination needs a stable order

Supabase `.range(from, to)` uses zero-based, inclusive bounds. Fetching 1,000 rows at a time therefore uses `to = from + 999`:

```ts
const PAGE_SIZE = 1_000
const translations = []

for (let from = 0; ; from += PAGE_SIZE) {
  const { data, error } = await supabase
    .from('translations')
    .select('id,locale,key,value')
    .order('id', { ascending: true })
    .range(from, from + PAGE_SIZE - 1)

  if (error) throw error
  translations.push(...data)

  if (data.length < PAGE_SIZE) break
}
```

The `.order()` is not decoration. Without a deterministic order, rows can move between pages and produce gaps or duplicates. If the table changes heavily while you read it, use keyset pagination instead: order by an immutable unique key and request rows greater than the last key you received.

## Do not load the universe by reflex

Pagination fixes completeness, not architecture. A browser rarely needs every translation for every locale. Prefer the active locale, the namespaces required by the current application, and a cache keyed by locale plus content version. Select only the columns the client uses.

The same rule applies to audit logs, messages, catalog items, and every other collection that grows quietly. A development table with 80 rows will never reveal a 1,000-row boundary.

## Test both sides of the limit

Seed `PAGE_SIZE + 1` rows and assert that the final item arrives. Then seed exactly `PAGE_SIZE * 2` rows; the loader should fetch the second full page and stop cleanly after the following empty response. Also test a transient error so a partial dictionary is not cached as complete.

The production symptom was partial translation, but the lesson is about bounded APIs: if a collection is allowed to grow, a single successful response is not proof that you received all of it.
