Mobile Architecture (JWT + Offline-first)
Como a API autentica o app mobile e como o app opera sem conexão em campo
JWT HS256: por que aqui, e o que HS256 implica
O backend deste serviço não usa Sanctum (usado no license-manager) para a API consumida pelo app mobile — usa JWT assinado com HS256, via firebase/php-jwt. JWT (JSON Web Token) é um token auto-contido: ao contrário de um token opaco do Sanctum (que exige consultar o banco para saber a quem pertence), um JWT carrega o próprio payload — sub (id do usuário), tempo de expiração — codificado e assinado dentro do próprio token, decodificável sem round-trip ao banco a cada requisição.
HS256 é assinatura simétrica: a mesma chave (JWT_SECRET) assina e verifica. Isso funciona bem quando quem assina e quem verifica é o mesmo sistema — este backend faz as duas coisas. Se o app mobile precisasse verificar o token por conta própria (sem perguntar ao backend), simétrico seria um problema: dar a chave ao app para verificar também daria a ele a chave para assinar tokens falsos. Não é o caso aqui — o app só manda o token de volta no header, quem verifica é sempre o backend.
// app/Http/Middleware/JwtMiddleware.php
$secret = config('jwt.secret');
$algorithm = config('jwt.algorithm', 'HS256');
$payload = JWT::decode($token, new Key($secret, $algorithm));
$user = User::find($payload->sub);
if (! $user || ! $user->is_active) {
return response()->json(['error' => __('auth.user_not_found_or_inactive')], 401);
}
Auth::setUser($user);
JWT::decode já valida a assinatura e a expiração (lança ExpiredException se vencido) — o middleware ainda confere is_active no banco depois de decodificar, porque o JWT em si não sabe se o usuário foi desativado depois de emitido; o payload é só uma foto do momento da emissão. JWT_TTL (ver deploy) controla por quanto tempo essa foto continua sendo aceita antes do app precisar autenticar de novo.
Offline-first: por que existe
Motorista e técnico executam checklist em campo — pátio, estrada, fazenda — onde conectividade não é garantida. Um app que depende de request síncrono para cada ação (marcar item, tirar foto, salvar odômetro) trava o fluxo de trabalho no exato momento em que a conexão cai. A solução aqui é inverter a dependência: o app grava tudo localmente primeiro (SQLite via expo-sqlite), e sincroniza com o backend quando há conexão — a operação em si nunca espera rede.
// mobile-app/database/database.ts
export function getDB(): SQLite.SQLiteDatabase {
if (!_db) {
_db = SQLite.openDatabaseSync('finatto.db');
runMigrations(_db);
}
return _db;
}
O app roda migração de schema no próprio dispositivo (runMigrations, com um schema_version guardado em app_settings) — o mesmo problema que o backend resolve com php artisan migrate no servidor, resolvido aqui client-side, porque não existe “servidor” no momento em que o app está sendo usado offline.
O orquestrador de sync: Chain of Responsibility
Sincronizar não é uma chamada só — é uma sequência de entidades diferentes (veículos, motoristas, acoplamentos, checklists, ordens de serviço, CNH), cada uma podendo precisar subir dado local para o servidor (push) ou baixar dado do servidor (pull), numa ordem que importa: subir um acoplamento antes do veículo existir localmente não faz sentido, por exemplo. SyncOrchestrator resolve isso encadeando handlers, no padrão Chain of Responsibility:
// mobile-app/services/sync/SyncOrchestrator.ts
vehiclesPush
.setNext(drivers)
.setNext(hitchPush)
.setNext(vehicles)
.setNext(checklistsPush)
.setNext(checklistsPull)
.setNext(workOrders)
.setNext(driverLicensePush);
await vehiclesPush.handle(ctx);
// SyncHandler.ts
async handle(ctx: SyncContext): Promise<void> {
await this.process(ctx);
if (this.next) {
await this.next.handle(ctx);
}
}
Cada handler só sabe processar sua própria etapa (process) e repassar para o próximo (this.next) — SyncOrchestrator não centraliza a lógica de cada entidade, só declara a ordem em que elas devem rodar. A ordem real aqui — veículos sobem primeiro, depois motoristas, depois acoplamentos, veículos de novo (pull), depois checklist (push então pull), depois ordens de serviço, depois CNH — é o que garante que uma entidade dependente nunca tente sincronizar antes da entidade da qual depende existir dos dois lados. ctx.errors acumula falha por handler sem interromper os seguintes; setLastSyncTime só é gravado se a cadeia inteira passou sem erro — uma sincronização parcialmente falha não é marcada como concluída, então a próxima tentativa reprocessa o que faltou.