O documento é seu, está no seu dispositivo, funciona sem internet — e ainda assim vocês editam juntos em tempo real

Apostila completa de Local-First & Apps Colaborativos em Tempo Real

Local-first inverte a relação com a nuvem: os dados vivem no dispositivo, as operações são locais e instantâneas, e a sincronização acontece em background — sem que o app pare quando a rede cai. Esta apostila cobre os ideais e o modelo, a resolução de conflito, os CRDTs a fundo, as bibliotecas e os "sync engines", a persistência e o transporte no cliente, a colaboração em tempo real (presence, rich text, undo colaborativo), o sync com um backend autoritativo, e o que muda em produção.

10 módulosCRDT · OT · convergênciaYjs · Automerge · sync enginespresence · rich textoffline · multi-deviceExercícios com gabarito
MÓDULO 01 · BÁSICO

O que é local-first

Objetivo: entender os ideais do local-first, o problema que ele resolve, como se diferencia de offline-first e local-only, e quando (não) vale a pena.

1.1 O problema com o "cloud-first"

Na arquitetura padrão (SPA + API + banco na nuvem), o servidor é o dono dos dados: toda leitura e escrita passa por ele. Consequências:

1.2 Os ideais do local-first

O ensaio Local-first software (Ink & Switch, 2019) propõe sete propriedades desejáveis:

  1. Rápido — sem esperar a rede para nada.
  2. Multi-dispositivo — seus dados em todos os seus aparelhos.
  3. Offline — funciona sem conexão, sincroniza depois.
  4. Colaboração — várias pessoas editando, com merge automático.
  5. Longevidade — os dados continuam acessíveis "para sempre", mesmo se o serviço morrer.
  6. Privacidade e segurança — por padrão, e com E2EE possível.
  7. Controle do usuário — você é o dono; pode exportar, mover, usar outro cliente.

1.3 O espectro

AbordagemFonte da verdadeOfflineColaboração
Cloud-firstservidornão (ou cache read-only)via servidor
Offline-firstservidor, com cache de escrita e fila de syncparcial; conflitos tratados na voltapossível, mais frágil
Local-firsto dispositivo; o servidor é relay/backuptotalnativa, merge automático (CRDT)
Local-onlyo dispositivo, sem synctotalnão

1.4 Quando vale — e quando não

💡 A regra que organiza a apostila

Local-first é uma decisão de arquitetura de dados, não uma biblioteca. O cerne é: o cliente pode ler e escrever sem a rede, e a convergência entre cópias é automática. Tudo o mais — CRDT vs OT, qual sync engine, P2P ou servidor, E2EE ou não — decorre de quão longe você quer levar cada um dos sete ideais.

💼 Mercado de trabalho

Perguntas de abertura: "O que é local-first e o que ele resolve?" (dados no dispositivo, ler/escrever sem rede, merge automático; resolve latência, offline, controle, colaboração), "Diferença entre offline-first e local-first?" (offline-first: servidor ainda é o dono, com cache e fila; local-first: o dispositivo é a fonte), "Quando você não usaria local-first?" (dados que precisam de verdade única/transações fortes, apps que são a nuvem, o cliente não pode ter os dados).

✏️ Exercício 1 — Classifique e decida

Para cada produto, diga se local-first faz sentido (todo, em parte, ou não), e por quê: (a) um app de notas pessoais; (b) um editor de documentos colaborativo estilo Notion; (c) um sistema de reservas de voo; (d) um app de lista de compras compartilhada com a família; (e) o painel de administração de um e-commerce.

Gabarito (uma boa resposta): (a) total — dados pessoais, offline importa, sem colaboração complexa; caso ideal. (b) total no editor (CRDT de rich text + presence) + servidor para permissões, busca, compartilhamento; o "core" é local-first, o entorno é híbrido. (c) não — assento é recurso escasso com verdade única; precisa de transação forte no servidor; local-first geraria overbooking. (d) total — pequeno, colaborativo, offline no mercado é o cenário; um CRDT set/list resolve. (e) não (ou mínimo) — dados de negócio autoritativos, agregações, relatórios, controle de acesso fino; cloud-first com talvez um cache offline read-only.

MÓDULO 02 · BÁSICO

O modelo: dados no dispositivo

Objetivo: entender o fluxo — cópia local como fonte, leituras/escritas instantâneas, sync em background, o servidor como relay — e por que o conflito é inevitável.

2.1 O fluxo

UI  ⇄  estado local (a fonte da verdade para este cliente)
                │
                ├─ persiste no dispositivo (IndexedDB / SQLite-WASM / OPFS)
                │
                └─ sync engine  ⇄  rede (WebSocket / WebRTC)  ⇄  outros clientes / servidor-relay

2.2 Cópia completa ou parcial

2.3 Por que o conflito é inevitável

Se dois clientes podem escrever sem coordenação prévia (é o ponto do local-first), então dois usuários — ou o mesmo usuário em dois aparelhos — vão, cedo ou tarde, editar "a mesma coisa" enquanto separados. Quando reconectam, há duas histórias divergentes. O sistema precisa de uma regra determinística para reconciliá-las de forma que todos os clientes cheguem ao mesmo resultado (convergência) — é o Módulo 3.

💼 Mercado de trabalho

Perguntas: "Como uma escrita funciona num app local-first?" (aplica no estado local na hora — UI otimista nativa — e enfileira para sync), "O servidor deixa de existir?" (não; vira relay/backup e, no híbrido, valida e hospeda dados derivados — mas não é o caminho de cada ação), "Cópia completa ou parcial?" (completa para dados pequenos; replicação parcial via shapes para datasets grandes), "Por que conflito é garantido?" (escrita sem coordenação → histórias divergentes → precisa de reconciliação determinística).

✏️ Exercício 2 — Desenhe o fluxo

Descreva o fluxo de dados de um app de tarefas local-first quando: (a) o usuário marca uma tarefa como concluída offline; (b) ele fica 3 dias offline e nesse tempo um colega adiciona 5 tarefas e edita o título de uma que o usuário também editou; (c) o usuário volta a ficar online.

Gabarito (uma boa resposta): (a) o clique aplica done=true ao estado local na hora; a UI atualiza; a operação (com um timestamp lógico / ID) é persistida no dispositivo e colocada na fila de sync, que não envia porque não há rede. (b) enquanto isso, o colega gera 5 operações de "add" e 1 de "edit title" no doc dele, que sincronizam com o servidor-relay e com os outros clientes online. (c) ao reconectar, o cliente do usuário envia suas operações pendentes (o "done" e o "edit title" dele) e baixa as que faltavam (as 5 adds + o "edit title" do colega). O CRDT faz o merge: as 5 tarefas novas aparecem; o "done" aplica; para o título editado por ambos, a regra do CRDT decide (ex.: LWW pelo timestamp lógico maior) — determinística, então o resultado é o mesmo em todos os clientes. Idealmente a UI sinaliza "este título foi alterado por 2 pessoas" se o produto quiser expor isso.

MÓDULO 03 · BÁSICO

Resolução de conflito

Objetivo: comparar last-write-wins, merge manual, Operational Transform e CRDTs; entender convergência; e a diferença entre consistência técnica e intenção do usuário.

3.1 As estratégias

EstratégiaComoTrade-off
Last-write-wins (LWW)a escrita com timestamp maior vence; a outra é descartadatrivial de implementar; perde dados silenciosamente; ruim para texto e listas
Merge manual (Git-style)detecta conflito e pede ao humano resolvernão perde nada; interrompe o fluxo; inaceitável em edição ao vivo
Operational Transform (OT)transforma cada operação contra as concorrentes para que apliquem na ordem certausado por Google Docs; difícil de implementar corretamente; costuma exigir um servidor central que serializa
CRDT (Conflict-free Replicated Data Type)estruturas de dados que por construção convergem, sem coordenaçãoo caminho local-first; peer-to-peer possível; custo de metadados; a base das libs modernas (Módulo 4)

3.2 Convergência

A propriedade que importa: dadas as mesmas operações (em qualquer ordem, com repetições), todos os clientes chegam ao mesmo estado final. CRDTs garantem isso porque as operações de merge são comutativas (a ordem não importa), associativas (o agrupamento não importa) e idempotentes (aplicar duas vezes = uma vez). OT garante por transformação + ordenação. LWW garante de forma pobre (converge, mas descartando).

3.3 Convergência ≠ intenção

💼 Mercado de trabalho

Perguntas: "Compare LWW, OT e CRDT" (LWW: simples, perde dados; OT: Google Docs, difícil, costuma exigir servidor serializador; CRDT: converge por construção, P2P possível, custo de metadados), "O que é convergência?" (mesmas operações → mesmo estado final; via merge comutativo/associativo/idempotente), "Convergir garante o resultado que o usuário queria?" (não — garante consistência; a intenção se protege com presence e granularidade, ou expondo o conflito).

✏️ Exercício 3 — Escolha a estratégia

Para cada dado num app de projeto, escolha a estratégia de conflito (LWW, CRDT específico, expor conflito) e justifique: (a) o título de um cartão; (b) a descrição em rich text; (c) a lista ordenada de cartões de uma coluna; (d) a lista de responsáveis (um set); (e) o status (enum: todo/doing/done); (f) um contador de "votos".

Gabarito (uma boa resposta): (a) título: LWW register — texto curto, editar junto é raro; last-write é aceitável (talvez sinalizar). (b) descrição: CRDT de sequência/rich text (RGA-like, tipo Yjs/Automerge Text) — edição concorrente é o caso de uso. (c) ordem dos cartões: CRDT de lista/sequência com posições fracionárias ou identificadores estáveis — inserções concorrentes convergem sem perder cartão. (d) responsáveis: OR-Set (observed-remove set) — adds e removes concorrentes se resolvem sem "ressuscitar" nem "sumir" itens indevidamente. (e) status: LWW register pelo timestamp lógico — é um valor único; ou, se importa, expor "2 pessoas mudaram o status". (f) votos: CRDT counter (PN-Counter) — incrementos concorrentes somam corretamente; LWW perderia votos.

MÓDULO 04 · INTERMEDIÁRIO

CRDTs a fundo

Objetivo: o conceito formal, state-based vs op-based, os tipos (counter, register, set, sequence, map, tree), e o custo (metadados, tombstones, GC).

4.1 O conceito

Um CRDT é uma estrutura de dados replicada em que a operação de merge forma um semilattice: é comutativa, associativa e idempotente. Isso garante Strong Eventual Consistency: réplicas que receberam o mesmo conjunto de atualizações têm o mesmo estado, sem precisar de consenso, sem rollback, sem coordenação.

4.2 State-based (CvRDT) × op-based (CmRDT)

4.3 O zoológico de tipos

TipoParaNotas
G-Counter / PN-Countercontadores (só incrementa / incrementa e decrementa)cada réplica tem seu sub-contador; o valor é a soma
LWW-Register / MV-Registerum valor único (LWW) ou "vários valores concorrentes" (multi-value)LWW usa timestamp lógico (Lamport/híbrido); MV expõe o conflito
G-Set / 2P-Set / OR-SetconjuntosOR-Set (observed-remove) é o prático: add/remove concorrentes se comportam bem
Sequence / List / Text (RGA, Logoot, Yata, Fugue)listas ordenadas e textoo problema mais difícil — cada caractere/item ganha um ID posicional imutável; algoritmos diferem em "interleaving" e tamanho de metadados
Mapchave→valor, com valores que são outros CRDTscompõe os anteriores; a base de "documentos" (Automerge, Yjs)
Tree / Moveárvores (outline, sistema de arquivos), mover subárvores"mover" concorrente é sutil (evitar ciclos); há algoritmos dedicados

4.4 O custo

🔬 Aprofundamento

Você quase nunca implementa um CRDT do zero — usa Yjs, Automerge, Loro ou Diamond Types. O valor de entender o interno é escolher e diagnosticar: por que o documento incha (tombstones, sem GC), por que o merge de texto ficou estranho (interleaving), por que a memória sobe (state-based sem delta), e o que a lib garante sobre "mover" e árvores. Leia CRDT.tech e os papers de Kleppmann/Shapiro para o modelo formal.

💼 Mercado de trabalho

Perguntas: "O que torna um CRDT 'conflict-free'?" (merge comutativo, associativo, idempotente → strong eventual consistency, sem consenso), "State-based vs op-based?" (manda o estado/delta vs manda as operações; trade-off de robustez × tamanho; delta-CRDTs no meio), "Qual o custo de um CRDT?" (metadados por elemento, tombstones, GC difícil, crescimento monotônico, interleaving de texto), "Por que texto é o caso difícil?".

✏️ Exercício 4 — Diagnostique

Um app de notas colaborativas com CRDT de texto está com: (a) documentos de 2 páginas ocupando 4 MB; (b) memória do cliente subindo ao longo da sessão; (c) às vezes, ao dois usuários digitarem no mesmo ponto, sai uma palavra com letras embaralhadas. Explique a causa provável de cada e a mitigação.

Gabarito (uma boa resposta): (a) metadados + tombstones sem GC: cada caractere já digitado e cada caractere apagado carrega ID e fica no histórico; 2 páginas com muita edição/apagamento acumulam. Mitigação: usar uma lib com compressão eficiente (Yjs/Loro), fazer snapshots periódicos e descartar o histórico antigo quando seguro, "arquivar" versões antigas. (b) histórico/updates acumulando em memória ou state-based sem delta; aplicar compaction e trabalhar com o documento carregado + updates incrementais, liberando o que já foi integrado. (c) interleaving do algoritmo de sequência sob inserção concorrente no mesmo ponto. Mitigação: usar um algoritmo que minimiza interleaving (Yjs/YATA, Fugue, Diamond Types) e, no produto, presence mostrando o cursor do outro para as pessoas não digitarem exatamente no mesmo lugar.

MÓDULO 05 · INTERMEDIÁRIO

Bibliotecas e sync engines

Objetivo: conhecer Yjs e Automerge, o ecossistema de "sync engines" e de "colaboração como serviço", e o trade-off entre montar você mesmo e comprar.

5.1 As libs de CRDT

5.2 Os "sync engines"

Ferramentas que resolvem o sync como problema de plataforma — geralmente ligando o cliente a um Postgres (ou outro banco) com replicação parcial e reatividade:

5.3 Colaboração como serviço

5.4 Montar × comprar

CaminhoQuando
Yjs/Automerge + você hospeda (Partykit, Durable Objects, um y-websocket)você quer controle, o caso é sobretudo "um documento colaborativo", e o time aguenta operar o sync
Sync engine (Electric, Zero, PowerSync…)o app é "dados relacionais reativos com offline", você já tem/quer Postgres, e quer que sync + replicação parcial + auth venham resolvidos
Colab-as-a-service (Liveblocks, Convex)time-to-market, "só quero multiplayer e comentários", pouca vontade de operar infra de sync
💼 Mercado de trabalho

Perguntas: "O que é o Yjs e o que ele te dá?" (CRDT web mais usado; tipos compartilhados, providers de rede/persistência, bindings de editor, awareness/presence), "O que é um 'sync engine' e como difere de usar um CRDT direto?" (resolve sync como plataforma, geralmente ligando o cliente a Postgres com replicação parcial e reatividade; menos "documento", mais "dados relacionais reativos"), "Montar com Yjs ou usar Liveblocks/Convex?" (controle e caso "documento" vs time-to-market e não operar infra).

✏️ Exercício 5 — Escolha a stack

Para cada produto, recomende a stack de sync e justifique: (a) um editor de texto colaborativo tipo Google Docs; (b) um app de CRM offline-capaz para vendedores em campo (muitos registros, cada vendedor vê só a sua carteira); (c) um app de quadro branco (whiteboard) multiplayer; (d) um app de tarefas pessoal com sync entre os dispositivos do próprio usuário e "compartilhar uma lista" ocasional.

Gabarito (uma boa resposta): (a) Yjs + binding ProseMirror/TipTap + hospedagem por documento (Partykit / Durable Objects / y-websocket) + y-indexeddb para offline + awareness para cursores. O caso é "um documento", que é o forte do Yjs. (b) sync engine relacional (PowerSync ou ElectricSQL) com Postgres no servidor e SQLite no dispositivo, replicação parcial por carteira (shape), auth e validação no servidor. É "dados relacionais reativos com offline", não um documento. (c) Yjs (Y.Map/Y.Array de formas) + presence forte (cursores, seleção) + hospedagem por sala; ou Liveblocks se quiser acelerar. (d) um CRDT simples (Automerge ou Yjs) com automerge-repo/provider, persistência local, e um relay leve; multi-device do mesmo usuário é o caso principal, compartilhamento é a exceção — não precisa de sync engine relacional.

MÓDULO 06 · INTERMEDIÁRIO

Persistência e transporte no cliente

Objetivo: onde e como guardar o estado no dispositivo (IndexedDB, OPFS, SQLite-WASM), e como transportar as mudanças (WebSocket, WebRTC, entre abas).

6.1 Persistência no navegador

OpçãoBom paraNotas
IndexedDBo padrão para guardar o binário do CRDT + snapshots + updatesassíncrono, transacional, cotas generosas; API verbosa (use um wrapper: idb, Dexie)
OPFS (Origin Private File System)arquivos grandes, acesso rápido, base para SQLite no navegadoracesso síncrono via Worker; sem prompt ao usuário
SQLite-WASM (wa-sqlite, sql.js, official)quando você quer consultas SQL reativas no cliente (a base dos sync engines relacionais)roda sobre OPFS; permite queries locais ricas
Cache Storageassets, modelos, snapshots imutáveisvia Service Worker
evite localStoragesíncrono, minúsculo, string-only; só para flags

6.2 Como guardar o CRDT

6.3 Transporte

6.4 Multi-aba

O mesmo usuário com 3 abas abertas não deve abrir 3 conexões nem divergir entre abas. Padrões: uma aba "líder" mantém a conexão e distribui via BroadcastChannel; ou o Service Worker é o dono; ou cada aba tem sua conexão mas todas convergem pelo CRDT (mais tráfego).

💼 Mercado de trabalho

Perguntas: "Onde você guarda o estado local de um app local-first?" (IndexedDB para o binário do CRDT + snapshots/updates; OPFS + SQLite-WASM quando quer SQL reativo), "Snapshot + updates — por quê?" (não reprocessar milhares de deltas; compaction periódica), "WebSocket ou WebRTC?" (servidor-relay simples e escalável vs P2P sem custo de dados mas frágil e com signaling), "Como sincronizar abas do mesmo usuário?" (BroadcastChannel / aba líder / Service Worker).

✏️ Exercício 6 — Persistência e transporte

Projete a camada de persistência e transporte de um app de notas colaborativas web: como guarda no dispositivo, como carrega rápido, como sincroniza entre usuários, entre dispositivos do mesmo usuário, e entre abas. Considere um usuário que abre uma nota de 1 ano com muito histórico.

Gabarito (uma boa resposta): Persistência: por nota, um snapshot + fila de updates em IndexedDB (via y-indexeddb ou equivalente). Ao abrir, carregar o snapshot (rápido) e aplicar os updates pendentes; um job de compaction funde updates num novo snapshot quando passam de N ou de X KB, para a nota de 1 ano não levar segundos para abrir. Histórico antigo (para "ver versões") pode ir para snapshots imutáveis separados, carregados sob demanda. Entre usuários: WebSocket para um servidor-relay "um por nota" (Durable Object/Partykit) que redistribui updates e guarda backup; awareness para presence. Entre dispositivos do mesmo usuário: o mesmo relay + o backup no servidor garante que o outro aparelho baixe o que falta ao abrir. Entre abas: uma aba líder mantém o WebSocket e distribui via BroadcastChannel; as outras abas leem/escrevem no mesmo IndexedDB e recebem updates da líder — uma conexão só. Fallback: se a líder fecha, outra aba assume.

MÓDULO 07 · AVANÇADO

Colaboração em tempo real

Objetivo: presence/awareness, edição concorrente de rich text, comentários e sugestões, atribuição, e undo/redo colaborativo.

7.1 Presence / awareness

7.2 Rich text concorrente

7.3 Comentários e sugestões

7.4 Atribuição ("quem fez o quê")

7.5 Undo/redo colaborativo

💼 Mercado de trabalho

Perguntas: "O que é awareness/presence e por que não vai no CRDT do documento?" (estado efêmero — cursores, online; some quando a pessoa sai; protege a intenção), "Como um comentário fica ancorado ao texto certo depois de edições?" (âncoras relativas do CRDT, não offsets), "Como funciona undo num editor colaborativo?" (desfaz só as operações do próprio autor — UndoManager ciente de autoria), "Que editores têm binding de CRDT?" (ProseMirror/TipTap, Lexical, CodeMirror, Slate…).

✏️ Exercício 7 — Casos de borda de um editor colaborativo

Liste 6 situações de borda que você testaria num editor de rich text colaborativo, e o comportamento esperado de cada.

Gabarito (uma boa resposta): (1) dois usuários digitam no mesmo ponto → nenhum caractere se perde; o texto converge igual em todos; presence deveria ter avisado. (2) A apaga o parágrafo que B está editando → o parágrafo some para os dois; B não fica "digitando no vazio" de forma incoerente; idealmente B recebe um aviso sutil. (3) A transforma um trecho em lista enquanto B digita nele → converge para uma lista contendo o texto de B. (4) A dá Ctrl+Z → desfaz só a última edição de A, não a de B feita depois. (5) Um comentário ancorado a "prazo: sexta"; alguém troca "sexta" por "segunda" → o comentário segue o range editado; se o range é totalmente apagado, o comentário vira órfão com um estado claro. (6) B fica offline, edita muito, volta → o merge não perde o trabalho de B nem o de quem ficou online; a ordem de blocos converge; nada de tela em branco. Extra: colar 50 KB de texto; dois aplicam negrito em ranges sobrepostos; reconectar com relógio do dispositivo errado (usar timestamp lógico, não wall-clock).

MÓDULO 08 · AVANÇADO

Sync com backend autoritativo

Objetivo: o híbrido real — local-first no cliente + Postgres no servidor: replicação parcial, autorização, validação de escrita, migrações e dados derivados.

8.1 Por que quase todo produto sério é híbrido

Local-first "puro" (P2P, sem servidor) é elegante mas bate em limites: autorização (quem pode ver o quê), validação (o cliente não é confiável), busca e agregações globais, onboarding (novo cliente precisa buscar tudo de alguém), integrações (webhooks, e-mail, cobrança) que vivem no servidor. Solução: o cliente é local-first, e há um servidor autoritativo (tipicamente Postgres) que participa do sync.

8.2 Replicação parcial (shapes / partial sync)

8.3 Autorização

8.4 Validação de escrita

8.5 Migrações de schema com clientes offline

8.6 Dados derivados

💼 Mercado de trabalho

Perguntas: "Por que um app local-first sério ainda precisa de servidor?" (autorização, validação, agregações/busca, onboarding, integrações), "O que é replicação parcial / shapes?" (o cliente sincroniza só o subconjunto relevante; troca de shape ao navegar), "Como funciona a validação de escrita?" (cliente aplica otimista e envia; servidor valida e confirma/rejeita; no rejeita, reverte e avisa), "Como você lida com um cliente que volta após meses offline?" (schema aditivo/compatível, versionar o formato, migradores, forçar update se necessário — nunca migração destrutiva).

✏️ Exercício 8 — Desenhe o híbrido

Um app de gestão de projetos quer ser local-first no cliente com Postgres autoritativo. Descreva: a replicação parcial, a autorização, o caminho de uma escrita (criar tarefa), o que acontece se a escrita viola uma regra, e onde ficam os relatórios agregados.

Gabarito (uma boa resposta): Replicação parcial: ao abrir um projeto, o cliente assina a shape "tarefas + membros + comentários deste projeto" (mais "meus projetos" sempre). O servidor faz streaming das mudanças dessa shape para SQLite/estado local. Autorização: o servidor filtra a shape pelas permissões do usuário (só projetos de que participa) e valida cada escrita contra o papel dele (membro pode criar tarefa; só admin pode arquivar o projeto). Criar tarefa: o cliente insere a tarefa no estado local (aparece na hora), gera a mutação e envia; o servidor valida (schema, projeto existe, usuário é membro, limites do plano) e confirma — os outros clientes da shape recebem. Violação: ex.: o usuário foi removido do projeto entre o abrir e o criar → o servidor rejeita; o cliente reverte a inserção otimista e mostra "Você não tem mais acesso a este projeto". Relatórios agregados ("horas por membro", "burndown"): computados no servidor a partir do estado sincronizado e entregues ao cliente como dados derivados (read-only), atualizados via a mesma stream.

MÓDULO 09 · MUITO AVANÇADO

Produção

Objetivo: segurança e E2EE, multi-device, dispositivos que voltam depois de meses, crescimento do documento, testes, observabilidade, custo, longevidade e migração de um app existente.

9.1 Segurança e E2EE

9.2 Multi-device e o "cliente que voltou do frio"

9.3 O documento que só cresce

9.4 Testes

9.5 Observabilidade e custo

9.6 Longevidade e LGPD

9.7 Migrar um app cloud existente

⚠️ Armadilhas de produção

Depender do relógio do dispositivo para ordenar (use HLC/Lamport). Não ter GC/compaction e ver docs de MB e abertura lenta. Assumir que todos os clientes atualizaram antes de uma migração destrutiva. Confiar na filtragem do cliente para autorização. E2EE "de marketing" sem plano de gerência de chaves e de adicionar colaborador. Não testar partição/reordenação (o bug de convergência só aparece com um cliente que ficou offline). Guardar o CRDT inteiro a cada tecla. Nenhum caminho de export (longevidade quebrada).

💼 Mercado de trabalho

Perguntas: "O que a E2EE impede num sistema local-first?" (validação server-side, busca/agregação no servidor, dados derivados do conteúdo; complica gerência de chaves e adicionar colaborador), "Como o documento não cresce para sempre?" (snapshots + descartar histórico visto por todos, compaction, arquivar), "Como você testa convergência?" (property-based com operações aleatórias, partições e reordenação, assertando estado final igual e invariantes), "Como migrar um app cloud para local-first?" (começar pela parte que mais ganha, rodar em paralelo com o mesmo backend, feature-flag; a camada de dados do cliente é o grosso do trabalho).

✏️ Exercício 9 — Plano de produção

Você tem um app de notas colaborativas com Yjs em beta. Antes do GA, liste: 4 riscos de produção e a mitigação de cada; o plano de teste de convergência; e a decisão sobre E2EE.

Gabarito (uma boa resposta): Riscos: (1) documentos incham → snapshots + compaction periódica, "arquivar" notas inativas para Markdown, limite de tamanho com aviso. (2) cliente offline por muito tempo volta com avalanche e schema antigo → servidor manda snapshot novo em vez de deltas, schema só aditivo, versão do formato + migrador, forçar update se divergência grande. (3) relógio errado no dispositivo corrompe ordenação de LWW-registers → usar HLC/Lamport, nunca wall-clock. (4) autorização: o estado local é adversário → o servidor-relay valida cada update contra permissões e filtra a shape; nada de confiar no cliente. Teste de convergência: harness que cria K réplicas simuladas, gera sequências aleatórias de ops (inserir/apagar/formatar/mover bloco), particiona e reconecta em ordens variadas, com duplicação e reordenação, e assere que todas as réplicas terminam com o mesmo documento e sem itens perdidos; rodar em CI com muitas seeds. E2EE: como o produto quer busca full-text no servidor e "compartilhar por link", começar sem E2EE (TLS + criptografia em repouso), com metadados e conteúdo acessíveis ao servidor; oferecer "notas privadas E2EE" como modo opt-in mais tarde, ciente de que nelas não haverá busca no servidor nem colaboração fácil.

MÓDULO 10 · CARREIRA

Mercado de trabalho: roadmap, entrevistas e portfólio

Objetivo: converter o conteúdo dos módulos em contratação — onde a habilidade é usada, um plano de estudo, um banco de perguntas e projetos que geram entrevista.

10.1 Onde local-first pesa

10.2 Roadmap de estudo (6–8 semanas)

SemanasFocoPrática
1Ideais, modelo, conflito (Módulos 1–3)Ler o ensaio da Ink & Switch; mapear 3 apps que você usa no espectro cloud→local
2CRDTs (Módulo 4)Implementar um G-Counter e um OR-Set do zero; brincar com um CRDT de texto pequeno
3Yjs (Módulos 5–6)Um editor colaborativo com Yjs + TipTap + y-webrtc + y-indexeddb; testar offline
4Colaboração (Módulo 7)Adicionar presence (cursores), comentários ancorados e undo por autor; provocar os casos de borda
5Sync engine + backend (Módulos 5, 8)Um app de tarefas com ElectricSQL/Zero/PowerSync + Postgres: replicação parcial + auth + validação
6Produção (Módulo 9)Harness de convergência (property-based + partições); medir tamanho de doc e latência de sync
7–8PortfólioPublicar os projetos com os testes e um write-up de decisões

10.3 Banco de perguntas (com a resposta que aprova)

Júnior/pleno — "O que é local-first?"

Arquitetura em que os dados vivem no dispositivo como fonte da verdade: leituras e escritas são locais e instantâneas, funciona offline, e a sincronização entre cópias é automática e sem conflito (CRDT). O servidor vira relay/backup, não o dono. Entrega os sete ideais da Ink & Switch: rápido, multi-device, offline, colaborativo, durável, privado, sob controle do usuário.

Pleno — "CRDT vs OT?"

OT (Google Docs) transforma operações concorrentes para aplicarem na ordem certa; é poderoso mas difícil de implementar e costuma exigir um servidor central que serializa. CRDT usa estruturas que convergem por construção (merge comutativo, associativo, idempotente) — permite peer-to-peer e offline sem servidor árbitro, ao custo de metadados, tombstones e GC. Local-first moderno usa CRDT (Yjs, Automerge).

Pleno — "Convergência garante o resultado que o usuário esperava?"

Não. Convergência garante que todas as réplicas chegam ao mesmo estado consistente — não que esse estado é o desejado. Inserções concorrentes no mesmo ponto convergem (ninguém perde dado) mas a ordem pode surpreender; texto pode virar frase sem sentido. Protege-se a intenção com presence (as pessoas se veem), granularidade fina, e às vezes expondo o conflito.

Pleno/sénior — "Qual o custo de um CRDT em produção?"

Metadados por elemento (ID de réplica + contador), pior em texto; tombstones que não somem até todos verem; GC difícil em sistema aberto; crescimento monotônico do documento; possível interleaving de texto. Mitiga-se com libs otimizadas (Yjs/Loro), snapshots + compaction, arquivar docs inativos, e escolher algoritmos que minimizam interleaving.

Sénior — "Yjs sozinho ou um sync engine?"

Yjs (+ hospedagem por documento como Partykit/Durable Objects) quando o caso é "um documento colaborativo" e você quer controle. Um sync engine (ElectricSQL, Zero, PowerSync) quando é "dados relacionais reativos com offline", você tem/quer Postgres, e precisa de replicação parcial, autorização e validação resolvidas. Colab-as-a-service (Liveblocks, Convex) para time-to-market sem operar infra de sync.

Sénior — "Como funciona o modelo híbrido com Postgres autoritativo?"

O cliente é local-first (estado local, escrita otimista, offline) mas sincroniza com um servidor autoritativo. Replicação parcial ("shapes") entrega ao cliente só o subconjunto a que tem direito. O servidor filtra por permissão, valida cada escrita (schema, regras) e confirma/rejeita — no rejeita, o cliente reverte o otimista. Dados derivados (agregações, busca) ficam no servidor. Migrações de schema precisam aceitar clientes que voltam do offline com formato antigo.

Sénior — "O que a criptografia ponta a ponta te tira?"

Se os updates do CRDT são cifrados no cliente, o servidor não pode validar conteúdo, indexar para busca, computar agregações a partir do conteúdo, nem ser um "sync server inteligente" — só guarda e repassa bytes. E gerenciar chaves por dispositivo/documento e adicionar um colaborador (compartilhar/re-cifrar) fica complexo. Muitos produtos fazem híbrido: conteúdo sensível E2EE, metadados no claro.

Armadilha — "É só trocar o fetch por um CRDT"

A camada de dados do cliente muda de paradigma (de "buscar e cachear" para "estado local que sincroniza"), e aparecem problemas novos: convergência sob partição, crescimento do documento, undo por autor, autorização com cliente não confiável, migração de schema para clientes que voltam do frio, presence, e testes property-based. É uma reescrita da camada de dados, não uma troca de biblioteca.

10.4 Projetos de portfólio que geram entrevista

  1. Editor colaborativo (âncora): rich text com Yjs + um editor (TipTap/Lexical), presence com cursores, comentários ancorados, undo por autor, offline com IndexedDB, e um write-up dos casos de borda testados.
  2. App relacional com sync engine: um app de tarefas/CRM com ElectricSQL/Zero/PowerSync + Postgres, replicação parcial por escopo, autorização e validação no servidor, e demo de reversão de escrita rejeitada.
  3. CRDT do zero + harness de convergência: implementar um OR-Set e um counter, e um teste property-based que simula partições/reordenação e prova convergência.
  4. App P2P sem servidor de dados: algo pequeno (lista compartilhada, jogo) com y-webrtc, mostrando os limites do P2P (signaling, quem entra depois).
  5. Estudo de migração: pegar um app cloud-first simples e converter uma tela para local-first mantendo o mesmo backend, documentando o que mudou na camada de dados.

10.5 Fontes para continuar

🏁 Síntese final da apostila

Cinco ideias sustentam local-first: (1) o dispositivo é a fonte da verdade — ler/escrever sem rede, sync em background, servidor como relay; (2) conflito é inevitável e a resposta é CRDT (converge por construção; custo de metadados, tombstones, GC); (3) Yjs/Automerge + presence resolvem o "documento colaborativo"; sync engines resolvem "dados relacionais reativos com offline"; (4) produtos sérios são híbridos — local-first no cliente + Postgres autoritativo com replicação parcial, autorização e validação; (5) produção exige testes de convergência, GC/compaction, migração para clientes offline, e um plano de longevidade e LGPD.