Visão geral da arquitetura
O WalletsApp é dividido em repositórios independentes, cada um com uma responsabilidade única. Nenhum deles acessa o banco de dados de outro diretamente — a comunicação entre eles é sempre por HTTP assinado.
Cliente (wallet-admin, wallet-cashier, app do participante, PDV)
│
▼
wallet-edge ──── único ponto público
│
├── rota global ─────────────► wallet-control-api
│ (identidade, diretório, gestão)
│
└── rota regional (por eventId) ► wallet-api
(wallets, saldos, ledger daquele evento)
Os repositórios
| Repositório | Responsabilidade | Nunca faz |
|---|---|---|
wallet-edge | Único ponto público. Valida JWT preliminarmente, aplica CORS/CSRF/Turnstile/rate limit, resolve se a rota é global ou regional e assina a chamada de origem. | Não guarda dado nenhum; não é a autoridade final de autenticação. |
wallet-control-api | Identidade global (app.user_profiles), organizações, workspaces (= eventos), contas de PDV/caixa, diretório global de wallets. | Nunca escreve saldo, ledger ou qualquer dado financeiro. |
wallet-api | Uma instância por região. Dono de app.wallets, app.wallet_balances, ledger.* daquele evento. | Nunca decide identidade global nem organização/permissão. |
wallet-admin | Frontend de gestão. | — |
wallet-cashier | Frontend do caixa. | — |
Por que "global" e "regional"
Um evento (workspace) é provisionado numa região física (home_region +
cluster_key). Tudo que é financeiro daquele evento — wallets, saldos,
ledger — mora no banco regional daquela região, não no banco global. Isso
existe para que o dado financeiro fique fisicamente perto de onde o evento
acontece, e para que uma região possa cair sem derrubar identidade/login
globalmente.
A wallet-edge decide o roteamento pelo formato da URL:
/v1/auth/*,/v1/wallet/*,/v1/management/*,/v1/directory/*→ semprewallet-control-api(global)./v1/wallet/*reúne os recursos globais exclusivos do participante (descoberta de wallets, busca de evento por código), do mesmo jeito que/v1/management/*reúne os recursos exclusivos da gestão — o prefixo já diz de quem é o recurso./v1/events/{eventId}/*→ resolvido pela região daqueleeventIdespecificamente, viawallet-apiregional.
Isso tem uma consequência prática: nunca aninhe um endpoint de descoberta
global sob /v1/events/{eventId}/..., porque a Edge vai tentar tratar
qualquer segmento ali como um eventId de verdade e tentar resolver a região
dele. Por isso GET /v1/wallet/events/by-code/{code} (busca de evento por
código) vive sob /v1/wallet, não sob /v1/events.
O diretório global de wallets
global.user_wallet_directory (em wallet-control-api) é um espelho de
descoberta, nunca a fonte de saldo. Quando a wallet-api regional ativa uma
wallet, ela enfileira um evento wallet.activated numa outbox própria
(ops.outbox_events); um dispatcher em segundo plano (mesmo processo,
setInterval, sem cron externo) envia isso pra wallet-control-api via uma
chamada assinada com CONTROL_PLANE_SECRET. Veja
Autenticação e perfis pra entender essa
assinatura, e a seção do Participante pra ver GET /v1/wallet/wallets usando
esse diretório.
Assinaturas internas — dois esquemas diferentes
| Esquema | Quem assina | Quem verifica | Header da assinatura |
|---|---|---|---|
| Origin signature | wallet-edge | API de origem (InternalOriginGuard) | X-Origin-Signature |
| Control-plane signature | wallet-control-api ↔ wallet-api (as duas direções) | ControlPlaneGuard no lado que recebe | X-Control-Signature |
O control-plane signature é simétrico: a mesma função assina dos dois
lados, com o mesmo segredo (CONTROL_PLANE_SECRET). A wallet-control-api
usa isso pra provisionar um evento na região; a wallet-api usa o mesmo
esquema, na direção oposta, pra sincronizar o diretório de wallets.