Mobile Architecture (JWT + Offline-first) · MemphisLab Docs
MemphisLab Docs
Checklist Mobile

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.