# 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 ```text /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 ```text 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.