Files
b2bcall/docs/ARCHITECTURE.md
B2BCall Bootstrap a2898fa566 feat: add authentication and RBAC
- packages/database: schema Prisma (users/sessions/roles/permissions/
  user_roles/role_permissions/audit_logs/password_reset_tokens), migration
  inicial e seed (permissoes+perfis+bootstrap super_admin com senha
  aleatoria em FIRST_LOGIN.txt). Decisao de ORM (Prisma) documentada em
  docs/ARCHITECTURE.md
- packages/shared: catalogo de permissoes (fonte unica usada por seed e API)
- apps/api: NestJS 11 + Fastify
  - autenticacao: Argon2id, access JWT + refresh token opaco com rotacao,
    cookies HttpOnly/SameSite=Lax, change/forgot/reset password
  - rate limiting progressivo de login via Redis (bloqueio crescente por IP)
  - RBAC reforcado no backend (PermissionsGuard), protecao contra
    auto-elevacao de privilegio
  - auditoria (audit_logs) nas acoes sensiveis, com redacao de segredos
  - health checks reais (postgres+redis), swagger desabilitavel, logs
    estruturados JSON com request_id de correlacao, filtro global de
    excecoes sem vazar erro cru
- infrastructure/docker/api.Dockerfile: build multi-stage do monorepo pnpm
- docker-compose.yml: servico api na rede interna, sem porta publicada

Testado via containers reais: login, /me, refresh, change-password,
rate limit (7 tentativas -> 429), RBAC (nega/permite), bloqueio de
auto-elevacao (403), audit log populado, health checks, lint e testes
unitarios passando.
2026-08-27 12:23:02 -03:00

8.1 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.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.