Files
b2bcall/docs/ARCHITECTURE.md
B2BCall Bootstrap 6273b32214 feat: add frontend, nginx reverse proxy and monitoring/reports extras
- apps/frontend: Next.js 15 (App Router) + Tailwind v4 + componentes estilo
  shadcn/ui sobre Radix UI + TanStack Query. Tema light/dark, logo
  processada. Menu completo (secao 52) com gating por permissao real.
  Todas as telas do checklist de aceite (secao 90) conectadas a endpoints
  reais (nao mockup): login, usuarios, perfis/permissoes, ramais/troncos,
  dialplan, filas/agentes, console do agente, campanhas (CPS/CSV/
  iniciar/pausar), monitoramento ao vivo (polling, nao WebSocket real),
  TME/TMA, busca/export de chamadas, administracao do Asterisk, auditoria
- infrastructure/nginx: reverse proxy colocando frontend+API na mesma
  origem (porta 80), antecipado da Fase 9 pois a API nao publica porta
  propria
- apps/api: GET /api/monitoring/agents (estado corrente real via
  agent_state_events em aberto) e filtro queueId em GET /api/reports/calls

Pendencia registrada: tela de Callbacks nao implementada (schema existe
desde a Fase 6, mas nunca houve controller/service — construir a tela sem
API real seria mockup). Verificacao visual em navegador nao foi possivel
neste ambiente headless; validado via tsc/eslint/next build limpos + curl
reproduzindo as chamadas do navegador (middleware de auth, 24 paginas
protegidas via Nginx, endpoints de dados com cookie de sessao).
2026-08-27 17:35:34 -03:00

9.9 KiB

B2BCall — Arquitetura

1. Contexto do servidor (levantado em 2026-08-27)

Item Valor
SO Debian 13 (trixie)
Kernel 6.12.94+deb13-amd64
CPU 2 vCPU
RAM 1.9 GiB (+ 1.6 GiB swap)
Disco 29 GiB (26 GiB livres)
IP 10.10.32.142/24 (privado, sem IP público)
Hostname Lab-FSCore-API

Alerta de capacidade: 1.9 GiB de RAM é pouco para rodar simultaneamente Postgres + Redis + Asterisk + API (NestJS) + worker + asterisk-events + scheduler + frontend (Next.js) + Nginx. Decisões desta arquitetura foram tomadas para caber neste ambiente de laboratório:

  • Build do frontend fora do container em produção real (ou build multi-stage com node --max-old-space-size limitado) — builds Next.js podem estourar RAM em VMs de 2 GiB.
  • docker-compose.yml define mem_limit/deploy.resources.limits conservadores por serviço.
  • Antes de operação real de discagem em produção, recomenda-se aumentar a VM para pelo menos 4 vCPU / 8 GiB RAM. Este ambiente serve para desenvolvimento e testes funcionais (inclusive DIALER_SIMULATION=true), não para carga real de discador preditivo com volume.

2. Estrutura do monorepo

/opt/b2bcall
├── apps
│   ├── frontend          # Next.js (App Router) + Tailwind + shadcn/ui
│   ├── api                # NestJS (Fastify adapter) - REST + WebSocket gateway
│   ├── dialer-worker       # PredictiveDialerEngine, CPS limiter, retry engine
│   ├── asterisk-events     # Conector AMI dedicado -> normaliza eventos -> Postgres/Redis/WS
│   └── scheduler           # Callbacks, horários de campanha, reconciliação, cron jobs
├── packages
│   ├── database            # Prisma/Drizzle schema + migrations + client
│   ├── shared               # utils comuns (logger, result types, error classes)
│   ├── types                 # tipos TS compartilhados (DTOs, enums de estado)
│   ├── ui                     # componentes de UI compartilhados (design system)
│   └── telephony               # TelephonyProvider (interface) + AsteriskTelephonyProvider (AMI/ARI)
├── infrastructure
│   ├── asterisk                 # pjsip.conf, extensions, odbc, realtime, queues
│   ├── nginx                     # reverse proxy, websocket upgrade, TLS
│   ├── postgres                   # init scripts, schemas (application / asterisk)
│   └── docker                      # Dockerfiles por serviço
├── scripts                          # install.sh, update.sh, backup.sh, restore.sh, healthcheck.sh
├── docs
├── docker-compose.yml
├── .env.example
├── .gitignore
└── README.md

3. Decisões técnicas

3.1 Rede Docker / Asterisk

  • Asterisk roda em network_mode: host — RTP usa uma faixa grande de portas UDP (10000-20000 por padrão); mapear porta a porta em bridge network é inviável em produção. Impacto documentado: o container do Asterisk enxerga a rede do host diretamente (perde isolamento de rede do Docker); todas as portas do Asterisk (SIP 5060/5061, RTP, AMI 5038, ARI 8088) ficam sujeitas apenas ao firewall do host (nftables), não ao Docker networking. Por isso as regras de nftables (seção 8) são a única linha de defesa e devem ser tratadas como obrigatórias, não opcionais.
  • Todos os demais serviços (api, frontend, worker, asterisk-events, scheduler, postgres, redis, nginx) ficam em uma rede Docker privada (b2bcall-net, bridge), sem publicação de portas para o host exceto Nginx (80/443).
  • Postgres e Redis não publicam portas no host; acessíveis somente dentro de b2bcall-net.

3.2 Topologia SIP

INTERNET → [OpenSIPS] → rede SIP privada → [Asterisk] → [B2BCall]
Asterisk → rede privada → Troncos SIP (operadoras)

O Asterisk nunca terá IP público. OpenSIPS e troncos são acessados via IP privado. AMI/ARI nunca expostos além do host local (bind em 127.0.0.1 ou rede interna Docker quando possível).

3.2.1 Asterisk (host network) ↔ Postgres/Redis (rede Docker interna)

Como o Asterisk usa network_mode: host, ele não enxerga a rede Docker interna (b2bcall-net) por nome — DNS de containers só funciona para quem está anexado à mesma rede definida pelo usuário. Solução adotada: o Postgres publica a porta 5432 somente em 127.0.0.1 do host (127.0.0.1:5432:5432, nunca 0.0.0.0), e o Asterisk se conecta via ASTERISK_DB_HOST=127.0.0.1 (variável própria, distinta de POSTGRES_HOST=postgres usada pelos demais serviços na rede interna). Isso mantém o Postgres inacessível pela LAN/Internet, alcançável apenas por processos no próprio host — incluindo um container em host network. Redis não precisa disso: só a aplicação (bridge network) fala com Redis, nunca o Asterisk diretamente.

3.3 Asterisk Realtime

  • PJSIP objects (ps_endpoints, ps_auths, ps_aors, ps_contacts, ps_endpoint_id_ips, ps_registrations) via res_odbc + res_config_odbc, schema dedicado asterisk no mesmo Postgres.
  • Domínio de aplicação (campanhas, leads, agentes, filas, etc.) vive no schema public/application. Nunca misturar as duas responsabilidades na mesma tabela.

3.4 Camada de telefonia

  • packages/telephony: interface TelephonyProvider (Originate, Hangup, QueuePause, QueueAdd/Remove, ExtensionState, DeviceState, QueueStatus, PJSIPShowEndpoints/Contacts, reload) + implementação AsteriskTelephonyProvider (AMI para controle/eventos administrativos, ARI apenas quando controle fino de canais/bridges for necessário). Nenhum comando AMI é chamado diretamente de controllers da API.

3.5 Eventos em tempo real

  • apps/asterisk-events mantém conexão persistente ao AMI, normaliza eventos, persiste os relevantes e publica no apps/api (WebSocket gateway) via Redis pub/sub. Frontend nunca faz polling do Asterisk.

3.6 Motor do discador preditivo

  • apps/dialer-worker: PredictiveDialerEngine — decide continuamente quantas chamadas originar, usando EWMA de answer rate, TMA, abandono, CPS disponível (token bucket coordenado via Redis) e capacidade de agentes. Detalhado em docs/PREDICTIVE_DIALER.md (a ser criado na fase correspondente).
  • Reserva de leads via SELECT ... FOR UPDATE SKIP LOCKED no Postgres — transição READY → RESERVED → DIALING atômica, com timeout de reservation.
  • Lock distribuído por campanha via Redis (dialer:campaign:{id}:lock, TTL + renewal + ownership token) para impedir dois workers controlando a mesma campanha.

3.6.1 ORM / migrations (decisão técnica)

Escolhido Prisma (packages/database) em vez de Drizzle/TypeORM: migrations versionadas e testáveis nativamente (prisma migrate), schema declarativo único como fonte de verdade, e client tipado que reduz erro humano no domínio RBAC/auditoria (muitas tabelas de relacionamento). Trade-off aceito: o engine binário do Prisma adiciona overhead de build/memória, mitigado por rodar prisma generate uma vez por build de imagem (não em runtime) e por ser um ambiente com swap disponível. Consultas de alta performance do motor do discador (ex.: SELECT ... FOR UPDATE SKIP LOCKED) usam $queryRaw do Prisma em vez de tentar modelar lock otimista via ORM.

3.7 Banco de dados

  • PostgreSQL 17.
  • Migrations obrigatórias (nenhuma alteração manual de schema).
  • Particionamento temporal de calls, call_events, queue_events, audit_logs avaliado quando o volume justificar — não implementado prematuramente, mas índices e chaves são desenhados para permitir particionamento futuro sem migração destrutiva.

3.7.1 Frontend e reverse proxy (Fase 8)

  • apps/frontend: Next.js 15 (App Router), React 19, Tailwind CSS v4, componentes estilo shadcn/ui escritos à mão sobre Radix UI (sem depender do CLI interativo do shadcn), TanStack Query para data-fetching/cache. SPA client-rendered atrás de RBAC (permissões vêm de /api/auth/me, cada tela usa RequirePermission além de já esconder itens de menu sem a permissão — defesa em profundidade, a autorização real é sempre do backend).
  • Build com output: "standalone" (Next.js) — a imagem de runtime (infrastructure/docker/frontend.Dockerfile) não precisa de pnpm/instalação nenhuma, só copia a árvore podada gerada pelo build. Atenção: em monorepo pnpm os node_modules do standalone contêm symlinks relativos para o pnpm store (../../../node_modules/.pnpm/...) — a imagem copia a árvore apps/frontend inteira sem achatar diretórios, senão os symlinks apontam para fora do container.
  • Nginx (infrastructure/nginx/nginx.conf, imagem oficial nginx:alpine) é o único serviço publicado para a LAN (porta 80, agente.md seção 87) — faz proxy de /api/* para o container api e do restante para o frontend, ambos na rede Docker interna sem portas publicadas individualmente. Isso coloca frontend e API na mesma origem do ponto de vista do navegador, eliminando CORS e problemas de cookie cross-origin (o cliente da API em apps/frontend/src/lib/api-client.ts usa por padrão a base relativa /api). HTTPS/TLS ainda não configurado — ver seção 87 do agente.md e pendências na Fase 9 do TODO.md.
  • next.config.ts tem eslint: { ignoreDuringBuilds: true } — o lint roda como etapa separada da pipeline de qualidade (pnpm --filter @b2bcall/frontend lint), não durante o build de produção.

3.8 Segurança desde a arquitetura

  • Segredos apenas em .env (nunca commitado). Credenciais de trunk SIP criptografadas em repouso (AES-256-GCM, master key fora do banco).
  • RBAC completo (users/roles/permissions/user_roles/role_permissions), reforçado no backend independentemente do frontend.
  • nftables com política default-deny para serviços administrativos/telefônicos; SSH nunca bloqueado durante instalação.

4. Status

Ver TODO.md para o checklist vivo de implementação.