GET /api/v1/powersync/lastchange¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06
Endpoint de stale-check consumido pelo app Flutter
(PowerSyncStaleChecker — RF-7). Retorna o timestamp da modificação
mais recente da tabela notacompl; o app compara com o
MAX(COALESCE(updated_at, created_at)) local para detectar bancos travados.
O COALESCE é necessário porque registros antigos (anteriores a V10) ou
importados sem upsert ficam com updated_at = NULL e apenas created_at
preenchido — sem o fallback o MAX do servidor ficaria menor que o do app e
o stale-check entraria em flapping.
Desde a decisão 0014, notacompl.updated_at
é auto-populado por @DateUpdated (Micronaut Data) em toda escrita de entidade —
antes vinha do cliente ou ficava NULL. Novas escritas passam a ter updated_at
confiável (server-time), o que fortalece o stale-check. A coluna permanece
nullable de propósito (sem backfill): manter as linhas legadas com NULL é o
que preserva o fallback COALESCE acima — um NOT NULL DEFAULT now() as
promoveria todas ao instante da migração e distorceria o MAX.
URL por cliente¶
- Sul Plata:
https://int.sulplata.xadm.biz/api/v1/powersync/lastchange - On Petro Trading:
https://int.onpetrotrading.xadm.biz/api/v1/powersync/lastchange
Autenticação¶
Público — nenhum token exigido.
O caminho está decidido pela ApiBearerSecurityRule (micronaut-security): /api/v1/powersync/lastchange é ALLOWED sem Bearer. Todas as outras rotas sob
/api/** continuam protegidas pelo Bearer estático.
A justificativa para abrir este endpoint:
- o payload é apenas um timestamp agregado, sem PII;
- o app Flutter precisa chamar mesmo em cenários de boot frio / sessão expirada, quando ainda não tem Bearer em mãos;
- a spec original mencionava "Firebase ID Token", mas o integrador não valida JWT; manter o token estático aqui só adicionaria acoplamento operacional sem ganho de segurança.
Response¶
200 OK¶
{ "last_changed_at": "2026-04-16T13:42:08.123Z" }
last_changed_at: ISO-8601 em UTC (Z), sempre com 3 casas decimais.- Quando
notacomplestá vazia: retorna"1970-01-01T00:00:00.000Z"(epoch) em vez denull.
500 Internal Server Error¶
{ "error": "internal_error" }
Falha de DB / erro inesperado. Stack é logada via SLF4J e reportada pelo
handler global (Sentry). O app trata qualquer status ≠ 200 como "endpoint
indisponível" → assume upToDate silenciosamente.
Implementação¶
- Controller:
PowersyncLastChangeController. - Query: JDBC direto via
DataSource→SELECT MAX(COALESCE(updated_at, created_at)) FROM notacompl. Usa JDBC puro em vez de Micronaut Data porque o annotation processor de Micronaut Data JDBC não suporta@Querysem parâmetros nem derived queries comfindFirst...OrderBy...em repositórios com UUID v7. OCOALESCEmantém simetria com o cálculo local do app (ver nota acima). - Formatação:
DateTimeFormattercom padrãoyyyy-MM-dd'T'HH:mm:ss.SSS'Z'fixado em UTC — independe de configuração global de Jackson/Serde.
Performance¶
- Latência alvo: < 200 ms p99.
- Stateless; cache no integrador é opcional e ainda não implementado (o app chama no máximo 1×/h via timer, ou burst de pull-to-refresh).
Evolução futura¶
A v2 pode retornar breakdown por tabela sem quebrar consumidores v1:
{
"last_changed_at": "...",
"by_table": { "notacompl": "...", "itens": "...", "vendas": "..." }
}
Basta estender LastChangeResponse com um campo adicional —
o app v1 ignora campos desconhecidos.
Testes¶
PowersyncLastChangeControllerTest
cobre:
- timestamp formatado em UTC
Zcom ms; - tabela vazia (MAX retorna NULL) → epoch;
- ResultSet vazio → epoch;
- normalização UTC independente de offset;
- erro de DB / connection refused → 500 com
{"error": "internal_error"}.
O bypass público é coberto em:
ApiBearerSecurityRuleTest— caminho exato e com barra final passam direto ao controller mesmo com Bearer habilitado;ApiBearerSecurityRuleIntegrationTest— chamada HTTP real retorna 200 sem headerAuthorization.