Transactional Outbox no JurisOS: Resiliência Local-First e Criptografia End-to-End com Elixir & BEAM
Construir softwares para escritórios de advocacia exige um nível rigoroso de confiabilidade. Secretárias e advogados operam sob alta pressão de prazos processuais e não podem tolerar telas congeladas por latência de rede ou perdas de rascunhos de minutas quando a conexão cai. O JurisOS foi arquitetado sob o paradigma Local-First, garantindo latência zero (< 1ms) nas interações locais da interface, combinado com sincronização resiliente em segundo plano.
A infraestrutura do aplicativo combina Elixir Desktop e Burrito para empacotar o runtime da BEAM em um binário nativo, utilizando SQLite (com SQLCipher) para persistência local, Mnesia para estado distribuído leve na camada local e Tailscale VPN para o túnel de transporte seguro.
1. O Desafio do Local-First
Sistemas jurídicos legados dependem de conexões síncronas diretas com instâncias centralizadas de bancos de dados (como PostgreSQL na AWS). Em redes instáveis — escritórios com Wi-Fi saturado, tribunais com sinal precário ou tribunais remotos —, essa abordagem resulta em falhas de requisição, timeouts e perda de dados não salvos.
O modelo Local-First do JurisOS inverte essa lógica: o cliente lê e escreve primariamente em seu armazenamento local integrado. O banco de dados central na nuvem deixa de ser um ponto de falha síncrono e passa a ser um destino de eventual consistência. Para viabilizar essa arquitetura sem corromper o estado global, o ecossistema BEAM provê o isolamento de processos e a concorrência necessária para gerenciar filas locais de sincronização sem bloquear o fluxo de trabalho do usuário.
2. O Padrão Transactional Outbox com Ecto
O maior perigo em arquiteturas híbridas (Local + Nuvem) é o Dual-Write Problem: atualizar o banco de dados local e, em seguida, disparar uma requisição de rede para a mensageria ou nuvem. Se a aplicação cair após a escrita local mas antes do envio da rede, o evento de sincronização é perdido, gerando divergência permanente entre o cliente e o servidor central.
Para eliminar essa classe de erro, o JurisOS implementa o Padrão Transactional Outbox. A entidade de negócio (ex: Contrato, CartaoKanban) e o evento de sincronização correspondente são gravados na mesma transação ACID do SQLite local usando Ecto.Multi.
defmodule JurisOS.Contratos.Context do
import Ecto.Query
alias JurisOS.Repo
alias JurisOS.Contratos.Contrato
alias JurisOS.Sync.OutboxEntry
def criar_contrato(attrs) do
Ecto.Multi.new()
|> Ecto.Multi.insert(:contrato, Contrato.changeset(%Contrato{}, attrs))
|> Ecto.Multi.insert(:outbox, fn %{contrato: contrato} ->
payload = %{
event_type: "contrato_criado",
aggregate_id: contrato.id,
data: Map.from_struct(contrato)
}
OutboxEntry.changeset(%OutboxEntry{}, %{
event_id: JurisOS.Snowflake.generate_id(),
payload: payload,
status: "pending"
})
end)
|> Repo.transaction()
end
end
Se a transação falhar, nada é persistido. Se for bem-sucedida, o evento na tabela sync_outbox está garantido no disco e será processado pelo motor de sincronização assim que houver conectividade.
3. Criptografia & Privacy by Design (Cloak.Ecto + LGPD/GDPR)
Manter dados jurídicos confidenciais (CPFs, laudos médicos, estratégias de litígio, sigilo bancário) armazenados em SQLite no disco local expõe o escritório a riscos severos em caso de furto, perda ou acesso não autorizado à estação de trabalho.
O JurisOS resolve essa vulnerabilidade integrando o Cloak.Ecto diretamente nos schemas do Ecto. O payload JSON armazenado na outbox e as tabelas sensíveis de negócio passam por criptografia AES-256-GCM antes de serem gravadas no arquivo do SQLite.
defmodule JurisOS.Vault do
use Cloak.Vault, otp_app: :juris_os
@impl true
def init(config) do
config =
Keyword.put(config, :aes_gcm,
key: Base.decode64!(System.fetch_env!("JURIS_ENCRYPTION_KEY"))
)
{:ok, config}
end
end
defmodule JurisOS.Sync.EncryptedBinary do
use Cloak.Ecto.Binary, vault: JurisOS.Vault
end
defmodule JurisOS.Sync.OutboxEntry do
use Ecto.Schema
import Ecto.Changeset
schema "sync_outbox" do
field :event_id, :string
field :payload, JurisOS.Sync.EncryptedBinary
field :status, :string, default: "pending"
timestamps()
end
def changeset(entry, attrs) do
entry
|> cast(attrs, [:event_id, :payload, :status])
|> validate_required([:event_id, :payload, :status])
end
end
Com essa estratégia, mesmo que o arquivo do banco de dados seja extraído do disco do laptop da secretária, os dados permanecem completamente indecifráveis sem a chave mestra isolada no ambiente de execução seguro.
4. O Motor de Sincronização (GenServer + libcluster + WebSockets)
O transporte dos eventos outbox acumulados é gerido por um GenServer especializado que consome a fila local em ordem FIFO (First-In, First-Out).
Para garantir segurança máxima sem expor portas públicas na internet ou depender de certificados complexos, o tráfego de rede flui exclusivamente pelo túnel privado da Tailscale VPN (100.64.0.0/10), conectando o cliente desktop diretamente ao nó mestre na AWS.
+-------------------------------------------------------------+
| JurisOS Desktop (Cliente Local) |
| [ SQLite / SQLCipher ] ---> [ Outbox GenServer ] |
+------------------------------|------------------------------+
| (Túnel Criptografado Tailscale)
v
+-------------------------------------------------------------+
| Nó Mestre AWS (Cloud Backend) |
| [ Phoenix Endpoint ] ---> [ Processador de Eventos (ACK) ] |
+-------------------------------------------------------------+
O Ciclo de Vida do Evento
- Pending: O evento é gerado e salvo na outbox local de forma transacional.
- Em trânsito: O GenServer lê o lote, decifra o payload em memória, envia via WebSocket seguro pela Tailscale.
- ACK (Acknowledgement): O Nó Mestre AWS valida o evento, persiste no PostgreSQL central e retorna um ACK criptografado.
-
Synced: O worker local recebe o ACK e marca o registro na outbox como
synced(ou o remove após retenção de auditoria). - Propagação Real-Time: O nó central utiliza o Phoenix.PubSub para despachar atualizações instantâneas para as demais estações de trabalho do escritório.
5. Garantias de Integridade e Resolução de Conflitos
Sistemas offline enfrentam o problema clássico de concorrência: duas pessoas editando o mesmo documento ou lançando valores em paralelo sem conexão mútua. O JurisOS aplica três camadas de proteção arquitetural:
-
Snowflake IDs (64 bits - layout 41-10-12): Cada estação cliente possui um
worker_idúnico configurado no build do aplicativo. As chaves primárias geradas offline contêm timestamp de alta precisão e o identificador do nó, eliminando 100% de colisões de chaves primárias mesmo quando múltiplos advogados geram registros simultaneamente sem rede. -
Optimistic Locking (
sync_version): Tabelas críticas de lançamentos financeiros e integração com ERP SAP possuem uma coluna de versão. Atualizações enviadas com uma versão desatualizada são rejeitadas pelo servidor, forçando a reavaliação do estado atual. -
Branching de Rascunhos (
versoes_minuta): Edições concorrentes em minutas contratuais nunca sofrem overwrite cego. O sistema cria ramificações locais isoladas de rascunhos que são submetidas a um Quality Gate (/qualidade/revisao) para fusão manual ou assistida por IA.
6. Developer Experience (DX) & Testabilidade
Um dos grandes diferenciais da stack BEAM é a capacidade de isolar estados em testes unitários. A suíte de testes do JurisOS executa inteiramente em sub-segundos utilizando instâncias efêmeras de SQLite em memória (:memory:) e stubs locais para os serviços de rede.
defmodule JurisOS.ContratosTest do
use JurisOS.DataCase, async: true
alias JurisOS.Contratos.Context
test "criar_contrato grava entidade e insere outbox atomicamente" do
attrs = %{titulo: "Prestação de Serviços Jurídicos", valor: 15000.00}
assert {:ok, %{contrato: contrato, outbox: outbox}} = Context.criar_contrato(attrs)
assert contrato.id != nil
assert outbox.status == "pending"
assert is_binary(outbox.payload) # Payload cifrado no disco
end
end
A ausência de dependências externas como containers Docker do PostgreSQL ou emuladores de AWS acelera o ciclo de feedback do desenvolvedor (mix test), permitindo rodar centenas de testes de resiliência transacional em frações de segundo.
Top comments (0)