Arquitetura

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)

🌐 Cliente / Internet Railway Edge (TLS) invoice-agent.up.railway.app 🔒 Rede privada Railway (interno apenas) api FastAPI + uvicorn :8000 POST /v1/process → 202 em <100ms GET /v1/health, /metrics 🐘 Postgres + pgvector invoice_jobs, audit_log, shadow_runs, embeddings 💾 postgres-volume (1 GB) 🟡 ClickHouse Warehouse OLAP (idle) CLICKHOUSE_ENABLED=false ativar para reports OLAP ⚙️ compat-worker Processa fila do legacy 🌗 shadow-worker Compara agent vs legacy Recursos externos 🧠 Anthropic API Claude Opus / Haiku 🔤 Voyage AI Embeddings RAG ☁️ AWS S3 / SNS Backup + notif status 🏛 RDS legacy SHADOW_LEGACY_DB_URL 🏛 Cortex / Provider CORTEX_DB_URL

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çaTipoAcessoPropó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:

Rede privada

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

1 Cliente POST /v1/process 2 api enfileira + retorna UUID 3 compat-worker SKIP LOCKED claim 4 Claude extração 10-30s 5 Persiste + SNS notify 6 (paralelo) shadow-worker compara agent vs invoice_old → métrica de paridade
  1. Cliente faz POST /v1/process/ com PDF.
  2. api zipa PDF → upload S3 backup → cria row em invoice_processes com status=TO_BE_PROCESSED → retorna UUID em <100 ms.
  3. compat-worker (em loop) detecta o row pendente e faz claim com SKIP LOCKED → marca PROCESSING.
  4. Claude extrai a fatura (multimodal Opus 4.7). Tempo médio: 10-30 s.
  5. Worker persiste em invoices / invoice_invoicex → marca PROCESSED → publica SNS → métricas no Prometheus.
  6. Em paralelo, shadow-worker compara o mesmo PDF processado pelo legacy invoice_old vs o agent. Grava diff em shadow_runs.
  7. Cliente consulta GET /v1/invoice/{uuid}/ ou GET .../report/<tipo>/ — api lê do Postgres e responde.

Recursos externos

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