Sincronização SQL Offline‑First que não vai derreter seu backend: um playbook para CTOs

Por Diogo Hudson Dias
Field technician using a tablet at a São Paulo construction site while a nearby laptop displays database sync metrics.

O modo offline do seu app provavelmente é um spinner e um desejo. Aí uma equipe de campo entra num túnel, cinco entregas se acumulam e, quando voltam à rede, seu backend leva uma tempestade de gravações. O Show HN acabou de ferver com o Syncular — sincronização SQL offline‑first com núcleos em TypeScript e Rust — lembrando a todos que local‑first não é mais brinquedo. Se você atende fluxos mobile, de edge ou regulados, precisa tratar sync como um produto, não como uma política de repetição.

A boa notícia: dá para entregar offline‑first com SQL puro e uma camada de sync sólida, sem reescrever sua stack nem adotar um banco feito sob medida. A má notícia: você precisa tomar, de saída, algumas decisões de design inegociáveis — formato do oplog, semântica de conflitos, estratégia de migração de esquema, sync seletivo, observabilidade e segurança de dispositivos. Acerte isso e você terá convergência previsível, raio de impacto controlado e um backend que não cede quando 10.000 celulares se reconectam às 17h01.

Quando Offline‑First Realmente Compensa

Offline‑first não é um recurso; é uma postura arquitetural. Compensa quando:

  • Dispositivos operam entrando e saindo de cobertura: entregadores, inspetores, serviço de campo, PDV de varejo, rondas de saúde e canteiros de obra. Espere desconexões medidas em dezenas de minutos a horas — não segundos.
  • Latência importa mais do que serializabilidade estrita: capturar evidências, escanear itens, registrar notas ou aceitar pagamentos que podem ser liquidados depois.
  • Privacidade ou pressão regulatória empurra a computação para a borda: mascaramento de PII no dispositivo, notas clínicas ou inferência no cliente.

Brazil e a LATAM mais ampla são mercados offline‑first clássicos: metrópoles densas com zonas mortas, viagens de longa distância e muitos planos de dados pré‑pagos. Se você está construindo para essas realidades (ou para operações de campo nos EUA com a mesma cara), precisa de um modelo de sync tediosamente confiável.

O Sync Mínimo Viável (MVS): SQL + Oplogs + Mesclas Determinísticas

Você não precisa de mágica. Precisa de um protocolo restrito e testável que rode nos clientes e mescle com sua fonte de verdade (muitas vezes Postgres):

1) Chaves primárias são imutáveis e globalmente únicas

  • Use UUIDv7 (ordenado por timestamp) ou ULIDs para linhas criadas no dispositivo. Nunca reaproveite chaves primárias.
  • Mantenha chaves estrangeiras estáveis; evite chaves compostas em tabelas críticas para sync.

2) Mudanças são expressas como um oplog somente apêndice

  • Cada cliente adiciona operações (insert/update/delete) a uma tabela de oplog local com: op_id (UUID), table, row_id, site_id (dispositivo), counter (monotônico por dispositivo), timestamp, op_type, payload (deltas de colunas) e metadados de domínio opcionais.
  • Exclusões são tombstones com um campo deleted_at explícito. Hard‑deletes servem apenas para tarefas de arquivamento.
  • Lotes de sync transmitem entradas do oplog para um endpoint no servidor que valida, mescla e retorna as operações aceitas pelo servidor, além de quaisquer operações remotas perdidas para pull.

3) Vetores de versão para ordenação causal

  • Mantenha um contador por dispositivo e um vetor de versão compacto no servidor (e opcionalmente por linha) para calcular o que cada lado está perdendo.
  • É assim que você evita diffs quadráticos — e por que soluções ingênuas de “comparar o último timestamp” desmoronam sob skew de relógio.

4) Semântica determinística de conflitos

  • Use por padrão o último a escrever vence (LWW) no nível de coluna para dados não críticos. É simples e previsível.
  • Para dinheiro, inventário e contadores idempotentes, use CRDTs (por exemplo, PN-Counters) ou mesclas específicas de domínio. Não aplique “LWW” a valores monetários.
  • Use registradores multi‑valor para conflitos reais que você quer que humanos resolvam; armazene todos os concorrentes e gere uma tarefa.

Se isso soa como o que Syncular, ElectricSQL, cr-sqlite, Realm, Couchbase Lite e variantes do Replicache fazem — é exatamente isso. Os padrões convergem porque o problema é antigo. A diferença é o quanto você privilegia SQL, a riqueza de CRDTs ou a ergonomia para desenvolvedores.

Estratégia de Conflitos: Seja simples em 80%, preciso nos 20%

A maioria dos esquemas tem três categorias de tabelas. Trate-as de forma diferente.

Categoria A: Logs de eventos e telemetria (LWW ou somente apêndice)

  • Exemplos: trilhas de auditoria, confirmações de leitura, logs de uso. Você raramente atualiza linhas antigas; você só adiciona ou aplica LWW em campos triviais.
  • Dica de armazenamento: comprima payloads e agrupe em lotes para reduzir tráfego de rede desnecessário.

Categoria B: Documentos e perfis (LWW por campo + resolução humana)

  • Exemplos: perfis de clientes, formulários, notas.
  • Abordagem: LWW por campo para a maioria das colunas; registradores multi‑valor para campos em que divergência importa (por exemplo, nome legal). Gere uma chave de conflito determinística para que humanos resolvam uma única vez.

Categoria C: Contadores e inventário (CRDTs ou mesclas de domínio)

  • Exemplos: níveis de estoque, leituras de medidores, ajustes financeiros.
  • Abordagem: PN-Counters para incrementos/decrementos; Observed-Remove Sets (OR-Set) para pertencimento; para dinheiro, persista lançamentos imutáveis em um ledger e compute saldos — não sobrescreva saldos.

Regra prática: mire em menos de 0,5% dos lotes de sync produzindo conflitos visíveis a humanos no uso normal. Se vir mais do que isso, você está pedindo que pessoas depurem seu modelo de consistência.

Migrações de Esquema sob Sync sem precisar parar por uma semana

Mudanças de esquema são onde projetos offline‑first vão para morrer. Coloque proteções agora:

  • Aditivas primeiro: adicione novas colunas/tabelas; não faça drop nem renomeie in place. Use backfills e transforms de dual-write durante uma janela de transição.
  • Sync com controle de versão: cada cliente declara uma versão de esquema; o servidor recusa sync além de N‑1. Publique um caminho de upgrade forçado antes de lançar uma migração destrutiva.
  • Transforme nas bordas: se o enum “status” virar “state + substate”, seu ingresso de sync aplica um transform determinístico para clientes antigos e grava ambos os formatos durante o cutover.
  • Migrações em sombra: faça deploy em duas fases — crie novas estruturas + transformadores, depois faça o flip de leituras e, por fim, faça o cleanup após um ciclo completo de upgrades dos dispositivos.

Meta prática: suportar uma breaking migration por trimestre com uma janela de upgrade de 14–28 dias. Mais rápido do que isso e você vai deixar dispositivos para trás.

Sync Seletivo e Privacidade por Padrão

Empurrar o banco inteiro para cada dispositivo é como você vai parar no noticiário. Seletividade é sua aliada — por desempenho, custo e compliance.

  • Filtros em nível de linha: sincronize apenas as equipes do usuário, rotas atribuídas ou inventário nas proximidades. Codifique os predicados de filtro no login e rotacione com os tokens de autenticação.
  • Redação em nível de coluna: não faça sync de SSNs, PANs de cartão ou segredos para os dispositivos. Para campos sensíveis porém necessários, considere criptografia no nível de campo.
  • Política de anexos: binários te matam. Limite o tamanho do sync inicial (por exemplo, 100–200 MB), fragmente arquivos grandes e carregue pré‑vias sob demanda mediante ações explícitas do usuário.
  • Ciclo de vida do dispositivo: credenciais por dispositivo, atestação quando disponível, wipe remoto e uma saída quando o dispositivo perde check‑ins.

Se você precisa de criptografia ponta a ponta (E2EE) para certas colunas, aceite que busca no servidor e transforms iniciados pelo servidor nesses campos ficam de fora. Ainda dá para fazer sync seletivo em blobs criptografados com chaves de filtro mantidas no servidor, mas você não vai indexar o que não consegue ver.

Planejamento de Capacidade: não deixe picos de reconexão incendiarem seu DB

Faça as contas antes de lançar:

  • Ops por dispositivo por dia: comece com 300–1.000 writes e 2–5× reads para apps de campo típicos.
  • Tamanho da entrada no oplog: 150–300 bytes para metadados + deltas se você evitar JSON verboso; 0,5–1,0 KB se serializar linhas inteiras como JSON. Escolha com sabedoria.
  • Conta de guardanapo: 10.000 dispositivos × 1.000 ops/dia × 250 bytes ≈ 2,5 GB/dia de ingestão na sua camada de sync. Se 30% se reconectam em uma janela de 10 minutos, você precisa absorver ~75 MB de operações por minuto, além de pulls com amplificação de leitura.
  • Batching: limite lotes por tamanho (por exemplo, 256–512 KB) e por tempo (por exemplo, 250 ms) para suavizar tempestades de reconexão.

Sua fonte de verdade é Postgres? Coloque um broker de mensagens e um serviço dedicado de mesclagem na frente dele. Evite que milhares de dispositivos esmaguem diretamente seu pool de conexões do DB. Considere filas write-ahead por tenant e aplique mesclas em workers controlados com limites de taxa.

Observabilidade e SLOs para Sync (para você dormir)

Se você não consegue ver, não convergiu. Trate sync como um pipeline de pagamentos com SLOs claros:

Métricas para instrumentar desde o primeiro dia

  • sync_batch_ingest_latency_ms (P50/P95/P99)
  • sync_batch_apply_latency_ms (server-side merge)
  • device_backlog_ops (gauge) and backlog_age_seconds
  • merge_conflict_rate (per table/column)
  • time_to_convergence_seconds after reconnection (P50/P95)
  • bytes_synced_per_device_per_day and initial_full_sync_bytes

SLOs sugeridos

  • P95 de tempo para convergência abaixo de 120 segundos após um dispositivo recuperar conectividade.
  • Taxa de conflitos de merge abaixo de 0,5% dos lotes em estado estável.
  • Sync inicial do dispositivo conclui em menos de 5 minutos em 4G mediano.
  • Sem perda de dados: desduplica por op_id; toda operação é idempotente na aplicação.

Testando de verdade

  • Caos estilo Jepsen: skew de tempo de ±15 minutos, lotes duplicados, entrega fora de ordem, perda de pacotes simulada.
  • Disciplina de relógio: contadores monotônicos por dispositivo; nunca confie em relógios de parede para ordenação.
  • Testes baseados em modelo: gere interleavings aleatórios de operações e assegure invariantes de CRDTs ou resultados LWW; depois compare com o estado final no servidor.

Segurança: dispositivos são um perímetro que você não controla

Sync amplia sua superfície de ataque. Feche os buracos óbvios:

  • Credenciais por dispositivo e menor privilégio: cada dispositivo recebe um token com escopo que codifica seus filtros de sync; rotacione em logout/perda do dispositivo.
  • Transporte: exija TLS 1.3 em dispositivos modernos; mantenha 1.2 apenas se for absolutamente necessário suportar Android legado. Acompanhe deprecações (por exemplo, trocas de chaves obsoletas do TLS 1.2 estão saindo de cena conforme RFCs atuais do IETF) e planeje upgrades.
  • Em repouso no dispositivo: criptografia apoiada pelo keystore do SO para o SQLite local; faça wipe ao detectar jailbreak/root ou falhas repetidas de atestação.
  • Auditoria: toda mesclagem é atribuível — quem, o quê, quando, de qual dispositivo. Mantenha 30–90 dias de oplog para forense; arquive por mais tempo se houver regulação.

O que usar: construir vs comprar (e o que cada um realmente faz)

  • ElectricSQL (centrado em Postgres, TypeScript): sync SQL‑first, ótimo para web e mobile com ferramentas familiares. Bom se você está all‑in em esquemas Postgres e quer tipagem forte.
  • cr-sqlite (camada de CRDTs sobre SQLite): multi‑master de verdade no SQLite via CRDTs; excelente para peer‑to‑peer ou edições offline pesadas. Você fará mais trabalho de integração.
  • Replicache (focado no cliente, web): UI otimista, reconciliação no servidor. Ótima DX; você implementa merges no servidor.
  • Realm/Couchbase Lite (mobile‑first, baterias incluídas): SDKs móveis robustos com estratégias de conflito; exigem maior adesão e diferem do modelo de dados de SQL puro.
  • Syncular (novo, núcleos em TS/Rust): sync em SQL com postura offline‑first. Vale pilotar se você quer uma experiência de desenvolvedor TypeScript‑first com um core de performance em Rust.
  • LiteFS/Litestream (replicação de SQLite): fantástico para failover de servidor, não para edições multi‑master vindas de muitos dispositivos. Não use isso como camada de sync móvel.

Heurística de decisão:

  • Se sua fonte de verdade é Postgres e você quer esquemas SQL fim a fim, comece com abordagens no estilo ElectricSQL.
  • Se você espera edições offline concorrentes pesadas e cenários peer‑to‑peer, aposte em SQLite com CRDTs (cr-sqlite).
  • Se você prioriza UX web e atualizações otimistas com lógica customizada no servidor, padrões do Replicache caem bem.
  • Se você quer um caminho gerenciado mobile‑first e aceita um novo modelo de dados, Realm/Couchbase Lite reduz o esforço.

Um rollout 30‑60‑90 que não vai derreter suas operações

Dia 0: desenhe seu raio de impacto

  • Escolha uma tabela que represente seu fluxo mais pesado de edições offline, mas que não envolva dinheiro (por exemplo, ordens de serviço, notas).
  • Defina SLOs e instrumente cliente e servidor.

Dias 1‑30: entregue o MVS (minimal viable sync)

  • Implemente oplog local, vetores de versão, push/pull em lotes e merges LWW para a tabela piloto.
  • Limite o tamanho do sync inicial e adicione credenciais por dispositivo.
  • Rode caos: skew de tempo, reordenação, duplicatas. Mire em P95 de convergência abaixo de 120 segundos.

Dias 31‑60: adicione precisão de conflitos e migrações

  • Adicione CRDTs ou mesclas de domínio aos 20% de campos que importam (contadores, conjuntos).
  • Introduza portas de versão de esquema e rode uma migração aditiva segura com dual writes.
  • Comece filtros de sync seletivo (equipes, rotas).

Dias 61‑90: endureça e escale

  • Mova merges para fora do pool de conexões do seu DB primário, para um serviço dedicado com filas e limites de taxa.
  • Introduza criptografia em nível de campo para colunas sensíveis, se necessário.
  • Faça rollout para 10–20% dos usuários de produção; observe janelas de reconexão e medidores de backlog. Ajuste tamanhos de lote e backpressure.

Em cada etapa, mantenha o mesmo modo de falha: convergência atrasada, nunca corrupção ou perda de dados. Se não conseguir garantir isso, pare e adicione mais determinismo antes de escalar.

Custos que você realmente verá (e como mantê-los previsíveis)

  • Egress de rede: sync seletivo economiza dinheiro de verdade. Limitar o sync inicial a 200 MB e usar deltas pode reduzir o egress em 50–70% comparado a pulls ingênuos de linhas inteiras em JSON.
  • CPU em merges: merges de CRDTs são baratos (matemática de vetores e operações de conjuntos) comparados a resolver conflitos via diffs de documentos inteiros. Evite blobs JSON de linha inteira; envie deltas de colunas.
  • Storage: retenção de oplog por 30–90 dias costuma ficar abaixo de 10% do tamanho total do DB se você compactar lotes e comprimir. Arquive histórico mais antigo em cold storage.
  • Custo de equipe: espere 2–4 engenheiros experientes em tempo integral por 2–3 trimestres para levar sync do piloto ao entediante. Pods nearshore em Brazil com 6–8 horas de sobreposição de fuso podem reduzir a queima de caixa em 20–30% mantendo a velocidade.

Armadilhas que matam projetos offline

  • Culto ao relógio: depender de timestamps em vez de contadores/vetores. Com skew, você vai perder gravações.
  • Perdas silenciosas: falhar em persistir o oplog de forma durável antes de dar ACK ao usuário. Crash -> mudança perdida.
  • Roleta de esquema: renomeações e migrações destrutivas sem controle de versão. Você vai deixar dispositivos à deriva.
  • Anexos em todo lugar: imagens e PDFs no mesmo canal de sync que dados críticos. Separe as pistas.
  • UX do tipo tudo ou nada: forçar convergência total antes que o usuário possa continuar. Deixe o app ser útil sob sync parcial com indicadores claros.

No fim das contas

Offline‑first está em alta porque o ecossistema finalmente o torna viável em stacks mainstream. Sync centrado em SQL (como tem aparecido em projetos como Syncular) significa que sua equipe pode dominar o modelo, testá‑lo como qualquer outro sistema distribuído e manter um envelope operacional enxuto. Trate sync como um produto: protocolo mínimo viável, precisão de conflitos onde importa, migrações que não deixam dispositivos para trás, sync seletivo por privacidade e custo, e SLOs que você consegue explicar ao seu CEO.

Pontos‑chave

  • Use SQL com um oplog somente apêndice e vetores de versão; evite ordenação apenas por timestamp.
  • Aplique LWW a 80% dos campos; use CRDTs ou mesclas de domínio para dinheiro, inventário e contadores.
  • Desenhe migrações como aditivas com gates de versão; transforme no ingresso/egresso durante cutovers.
  • Sync seletivo é inegociável — por privacidade, custo e performance. Limite o tamanho do sync inicial.
  • Instrumente SLOs reais: P95 de convergência abaixo de 120 s; taxa de conflitos abaixo de 0,5%.
  • Proteja a borda: credenciais por dispositivo, TLS 1.3 quando possível, criptografia em repouso apoiada por keystore, wipe remoto.
  • Planeje capacidade para picos de reconexão; desacople merges do seu DB primário com filas e limites de taxa.
  • Espere 2–4 engenheiros por 2–3 trimestres para tornar sync entediante; pods nearshore podem reduzir custos em 20–30% com 6–8 horas de sobreposição.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation