Estrutura do servidor
O invoice-agent roda no Railway como
um conjunto de 3 apps + 2 bancos. Esta página é um tour didático: o
que cada peça faz, como elas se conversam, e por que a arquitetura
é assim.
Visão geral (diagrama)
Caixas com borda branca = expostas publicamente. Caixas escuras = serviços internos (só atingíveis pela rede privada do Railway). Caixas pontilhadas = recursos externos fora do projeto.
O que cada peça faz
| Peça | Tipo | Acesso | Propósito |
|---|---|---|---|
| api | App | 🌐 público | Ponto de entrada HTTP. Recebe PDFs, devolve status e
reports. FastAPI + uvicorn na porta 8000. |
| compat-worker | App | 🔒 interno | Roda em loop, faz SELECT ... FOR UPDATE SKIP LOCKED
no DB legacy, claims processos pendentes e roda extração.
Sobrevive a kill -9 porque o estado é o DB. |
| shadow-worker | App | 🔒 interno | Polla o legacy de tempos em tempos, baixa o PDF original,
roda o extractor do agent e compara com o que o
invoice_old produziu. Gera métricas de paridade
que alimentam o gate de cutover por operadora. |
| Postgres | DB | 🔒 interno | Banco principal com extensão pgvector
habilitada. Guarda invoice_jobs,
invoice_records, audit_log,
shadow_runs e embeddings RAG. |
| ClickHouse | DB | 🔒 interno | Provisionado mas idle
(CLICKHOUSE_ENABLED=false). Banco analítico (OLAP)
para queries cross-fatura (ex.: "custo médio por operadora no
mês"). Não é onde dados transacionais ficam. Para ativar, ver
seção Ativar warehouse abaixo. |
| postgres-volume | Volume | — | Persistência do Postgres. Sobrevive a redeploys do service. |
Por que esta arquitetura
Separação API ↔ Workers
A API responde em <100 ms (return rápido); a extração via Claude
leva 10-30 s. Se a API fizesse a extração inline, o cliente esperaria
muito e teríamos timeout em pico de tráfego. Solução:
a API só enfileira (cria row em
invoice_processes com status=TO_BE_PROCESSED)
e workers processam em paralelo.
Múltiplos workers para paralelismo seguro
Hoje há 1 réplica de cada worker mas o design suporta N réplicas.
O SKIP LOCKED do Postgres garante que workers
concorrentes pegam rows diferentes sem conflito — não precisa de
Redis nem broker externo.
compat-worker vs shadow-worker
Dois workers porque resolvem problemas distintos:
compat-worker= produção, processa o trabalho real (extrai PDFs novos para clientes).shadow-worker= observabilidade, compara silenciosamente, ajuda a decidir quando confiar 100% no agent (gate de cutover por operadora).
Rede privada
Só api tem domínio público. Workers e DBs ficam
isolados na VPC do Railway — atacantes externos não conseguem se
conectar diretamente. Comunicação interna usa hostnames como
postgres.railway.internal + DNS interno.
Volumes
Dados de Postgres e ClickHouse ficam em
volumes managed pelo Railway (storage físico desacoplado do
container). Se o container morrer, o volume sobrevive e o próximo
container monta os mesmos arquivos. É o que permite migrations +
dados persistirem entre deploys.
Fluxo de uma extração ponta-a-ponta
- Cliente faz
POST /v1/process/com PDF. - api zipa PDF → upload S3 backup → cria row
em
invoice_processescomstatus=TO_BE_PROCESSED→ retorna UUID em <100 ms. - compat-worker (em loop) detecta o row pendente
e faz claim com
SKIP LOCKED→ marcaPROCESSING. - Claude extrai a fatura (multimodal Opus 4.7). Tempo médio: 10-30 s.
- Worker persiste em
invoices/invoice_invoicex→ marcaPROCESSED→ publica SNS → métricas no Prometheus. - Em paralelo, shadow-worker compara o mesmo PDF
processado pelo legacy
invoice_oldvs o agent. Grava diff emshadow_runs. - Cliente consulta
GET /v1/invoice/{uuid}/ouGET .../report/<tipo>/— api lê do Postgres e responde.
Recursos externos
- Anthropic API: Claude faz a extração propriamente dita (~$0.02 por fatura).
- Voyage AI: embeddings para o RAG (recupera glossários e exemplos por operadora).
- AWS S3: backup do PDF original (FP3) + bucket de PDFs do legacy.
- AWS SNS: notifica sistemas downstream quando
status muda (FP1, paridade
invoice_old). - AWS RDS legacy:
SHADOW_LEGACY_DB_URLaponta pra cá — onde estão os 25k+ PDFs já processados pelo sistema antigo. - Cortex / Provider DB:
CORTEX_DB_URL, dados de plano/cost-center que enriquecem reports analíticos.
Ativar warehouse (ClickHouse)
ClickHouse fica idle por default — agent funciona 100% sem ele.
Quando precisar de relatórios analíticos cross-fatura
(POST /v1/reports/run com tipos OLAP), ativar via:
railway service api
railway variable set --service api \
"CLICKHOUSE_ENABLED=true" \
"CLICKHOUSE_HOST=clickhouse.railway.internal" \
"CLICKHOUSE_PORT=8123" \
"CLICKHOUSE_USERNAME=${clickhouse.CLICKHOUSE_USER}" \
"CLICKHOUSE_PASSWORD=${clickhouse.CLICKHOUSE_PASSWORD}" \
"CLICKHOUSE_DATABASE=invoice_analytics"
# Repete pra compat-worker se quiser que ele escreva no warehouse
# Cria schema do warehouse
railway run --service api invoice-cli warehouse migrate
# Backfill dos dados históricos (opcional)
railway run --service api invoice-cli warehouse backfill
Depois disso o ClickHouse passa a receber dimensões + fatos.
Reuso do diagrama: linhas api → ClickHouse e
shadow-worker → ClickHouse ficam ativas.
Como ver no Railway
Dashboard: railway.com/project/invoice-agent — visualização gráfica dos services + logs em tempo real + variáveis por ambiente.
CLI (do repo local):
# Status dos services
railway status
# Logs em tempo real
railway logs --service api --tail 100
# Variáveis (sem expor valores)
railway variables --service api
# Conecta no Postgres
railway connect Postgres
Documentos relacionados
- Runbook — Setup Railway (procedimento passo-a-passo)
- Runbook —
Substituir
invoice_old(plano de cutover) - Arquitetura completa do código (componentes Python detalhados)