SuperdindaDocs

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âmetroO que é
cursorO nextCursor da página anterior. Ausente = primeira página
limitItens por página. O máximo depende da rota (veja a tabela abaixo)
filtrosCada 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:

RotaLista em datalimit (padrão / máx.)Cursor
GET /admin/usersusers30 / 100instante + id
GET /admin/dependentsdependents30 / 100instante + id
GET /admin/connectionsconnections30 / 100instante + id
GET /admin/consentsconsents30 / 100instante + id
GET /admin/accountsaccounts30 / 100created_at + id
GET /admin/customerscustomers30 / 100offset (ordenar por MRR ou atividade não é único nem monotônico)
GET /admin/subscriptionssubscriptions30 / 100created_at + id
GET /admin/seats/{uid}/timelineevents50 / 200instante + fonte + id (consentimento e pareamento escrevem no mesmo milissegundo)
GET /admin/auditactions50 / 200instante + 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 (asc por padrão, ou desc) e aceita uma janela opcional em created_at (after/before, exclusivas);
  • o limit padrão é 50;
  • o cursor é um JSON em base64url com { table, lastId, limit, orderBy };
  • só gera nextCursor com a página cheia. Uma página incompleta é a última;
  • um cursor quebrado, ou usado com outra tabela, outro limit ou outra ordem, responde 400 BAD_REQUEST dizendo o que mudou. Mudança de rumo é barulhenta, nunca silenciosa.
Num entity
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")));
Andando até o fim (cliente)
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.ts
  • backend/src/domains/insights/use-cases/operator-lists.ts, customers.ts, subscriptions.ts, accounts.ts, seat-timeline.ts
  • backend/src/domains/audit/use-cases/read.ts
  • backend/src/domains/notifications/entities/notification.entity.ts, backend/src/http/routes/notifications/schemas.ts
  • .specs/features/redesign/api-gaps.md (G-04, G-07)

On this page