A resposta perdida
O pior caso em campo não é ficar sem rede. É o servidor gravar e a resposta se perder no caminho: o app tenta de novo e duplica. Por isso cada registro nasce com uma chave. O servidor reaproveita o que já existe com aquela chave, e um índice único parcial no banco garante o resto, mesmo que o código erre.
-- migration 0022: resposta perdida depois do commit vira retry, e retry duplicava
alter table activities add column if not exists client_key text;
create unique index if not exists uq_activities_client_key
on activities (client_key) where client_key is not null;
