Paginação
Paginação por cursor opaco: parâmetros, formato da resposta com nextCursor, quais rotas paginam hoje e o helper de cursor do gateway de entidade.
A API pagina por cursor opaco, nunca por número de página. O cliente não monta nem interpreta o cursor: devolve o nextCursor que a página anterior entregou. null é o único sinal de fim.
Pedido
| Parâmetro | O que é |
|---|---|
cursor | O nextCursor da página anterior. Ausente = primeira página |
limit | Itens por página. O máximo depende da rota (veja a tabela abaixo) |
| filtros | Cada rota tem os seus (q, status, kind, from/to…). Mantenha os mesmos filtros enquanto anda com o cursor |
Resposta
Hoje as rotas paginadas põem a lista e o nextCursor dentro de data, com a lista num campo com nome:
{
"success": true,
"data": {
"users": [{ "uid": "…" }],
"nextCursor": "2026-10-19T14:02:11.000Z|8812"
}
}O backend também tem um formato com o cursor no envelope, onde data é a própria lista (pageResponse e reply.page em backend/src/http/response.ts):
{ "success": true, "data": [{ "uid": "…" }], "nextCursor": "eyJ0YWJsZSI6…" }Esse segundo formato deixa entrar outros fatos da página ao lado (contagens, totais). A lista de alertas (GET /accounts/{uid}/alerts) usa ele, com counts por status ao lado. A caixa de entrada (GET /notifications) ainda devolve um objeto com notifications e nextCursor (o cursor é o ULID da última), pra não quebrar o sino atual; ela passa pro formato do envelope com a caixa de entrada da onda 4.
Quais rotas paginam
Todas são do backoffice e pedem sessão de operador:
| Rota | Lista em data | limit (padrão / máx.) | Cursor |
|---|---|---|---|
GET /admin/users | users | 30 / 100 | instante + id |
GET /admin/dependents | dependents | 30 / 100 | instante + id |
GET /admin/connections | connections | 30 / 100 | instante + id |
GET /admin/consents | consents | 30 / 100 | instante + id |
GET /admin/accounts | accounts | 30 / 100 | created_at + id |
GET /admin/customers | customers | 30 / 100 | offset (ordenar por MRR ou atividade não é único nem monotônico) |
GET /admin/subscriptions | subscriptions | 30 / 100 | created_at + id |
GET /admin/seats/{uid}/timeline | events | 50 / 200 | instante + fonte + id (consentimento e pareamento escrevem no mesmo milissegundo) |
GET /admin/audit | actions | 50 / 200 | instante + id |
O formato do cursor aparece na tabela só pra explicar. Não dependa dele: pode mudar sem aviso.
Do lado do responsável, nada pagina ainda. GET /notifications devolve as 50 mais recentes mais o total de não lidas (unread). O id de cada notificação é um ULID e ordena por tempo.
O helper do gateway de entidade
Entity.cursorPage (backend/src/core/contracts/entity.ts) é o caminho padrão pra paginar uma tabela por keyset:
- anda sobre
id(ascpor padrão, oudesc) e aceita uma janela opcional emcreated_at(after/before, exclusivas); - o
limitpadrão é 50; - o cursor é um JSON em base64url com
{ table, lastId, limit, orderBy }; - só gera
nextCursorcom a página cheia. Uma página incompleta é a última; - um cursor quebrado, ou usado com outra tabela, outro
limitou outra ordem, responde400 BAD_REQUESTdizendo o que mudou. Mudança de rumo é barulhenta, nunca silenciosa.
static list(accountId: number, query: CursorQuery) {
return this.cursorPage<SeatRow>(
this.qb<SeatsDatabase>().selectFrom("seats").selectAll()
.where("account_id", "=", accountId),
query,
);
}
// na rota: return reply.page(c, await SeatEntity.list(account.id, c.req.valid("query")));let cursor: string | null = null;
do {
const qs = new URLSearchParams({ limit: "100", ...(cursor && { cursor }) });
const { data } = await api(`/admin/users?${qs}`);
render(data.users);
cursor = data.nextCursor;
} while (cursor);Em aberto
Entity.cursorPage e reply.page não são usados por nenhuma rota. As listas
do backoffice montam o próprio cursor (backend/src/domains/insights,
backend/src/domains/audit). Falta decidir se elas migram pro helper e pro
formato com cursor no envelope, ou se o helper sai.
Fontes
backend/src/http/response.ts(pageResponse,reply.page)backend/src/core/contracts/entity.ts(CursorQuery,CursorPage,cursorPage)backend/src/http/routes/admin/operator-lists.schemas.ts,customers.schemas.ts,accounts.schemas.ts,audit.route.tsbackend/src/domains/insights/use-cases/operator-lists.ts,customers.ts,subscriptions.ts,accounts.ts,seat-timeline.tsbackend/src/domains/audit/use-cases/read.tsbackend/src/domains/notifications/entities/notification.entity.ts,backend/src/http/routes/notifications/schemas.ts.specs/features/redesign/api-gaps.md(G-04, G-07)
Limites de taxa
Quais rotas têm limite, as janelas e os números de cada uma, a resposta 429 RATE_LIMITED com Retry-After e como o contador funciona no Redis.
Webhooks e eventos
A tabela events (ULID), como uma mudança de estado vira evento, os webhooks que chegam dos provedores pelo gateway e a situação dos webhooks de saída.
