TDPBX Logo
arrow_backVolver a la landing
DOCUMENTACIÓN TÉCNICA

Cómo funciona TDPBX, de punta a punta.

Referencia generada desde el código real: api/graph/schema.graphql, proto/agent/v1/*.proto, migrations/ y docs/runbooks/. Sin cifras de flota inventadas: lo medible vive en tu consola, no en esta página.

01 // FUNDAMENTO

Qué es TDPBX#

TDPBX es el panel de control de la flota Talkadillo. No es un PBX y no cursa llamadas: centraliza intención declarativa, telemetría y aprovisionamiento. Talkadillo —nodo SIP escrito en Rust que habla el protocolo backplane directamente— procesa las llamadas.

Reemplaza, no administra

Sustituye consolas aisladas de FreePBX/VitalPBX. El soporte Asterisk se eliminó del código (agente Go + modo observer retirados); los importadores en tools/ solo sirven para migrar configuración real al modelo declarativo.

Herramienta interna

No se vende ni se licencia a terceros. La jerarquía platform → resellers → customers modela la estructura real de clientes de Talkadillo, con scoping a nivel repositorio y aislamiento por cuenta.

02 // SISTEMA

Arquitectura y stack#

Monolito modular en Go con fronteras internas limpias (gateway, graph, config, provisioner, cdr, store). Separación estricta control/data: el backplane guarda intención, cada nodo la materializa.

CAPAS // SEPARACIÓN CONTROL / DATOS
CAPA 01 · INTENT — consola Next.js + API GraphQL schema-first (gqlgen). Versiones inmutables, RBAC por jerarquía.
CAPA 02 · TRANSPORT — un stream gRPC bidireccional por nodo sobre mTLS. Coordinación entre instancias vía Redis. Los nodos llaman a casa; el backplane nunca inicia TCP hacia ellos.
CAPA 03 · EXECUTION — nodos Talkadillo con snapshot local: siguen cursando llamadas aunque el panel esté caído.
API + GATEWAY
Go + gqlgen · gRPC mTLS · zerolog · /metrics Prometheus
DATOS
PostgreSQL (primario) · ClickHouse (CDR) · Redis (pub/sub)
CONSOLA
Next.js + TypeScript + Apollo · paginación Relay · subscriptions live
MEDIA WORKER
Binario separado: la imagen API es distroless y no puede forkar ffmpeg
AUTH STAFF
OIDC (Authentik) · JWT httpOnly tdpbx_session, idle 30 días
MIGRACIONES
SQL ordenados, tracking en schema_migrations
03 // NODOS

Nodos, enrollment y protocolo#

Enrollment dirigido por operador: se crea el nodo en la consola, se genera un token de un solo uso y se ejecuta el install script en la máquina (descarga el agente de la ruta /downloads, se enrola en el primer arranque systemd y reconecta con su certificado). La emisión del certificado es la autorización.

  • →rpc Connect(stream AgentEnvelope) returns (stream GatewayEnvelope) — un stream por nodo, envelopes con correlation_id.
  • →Handshake declara node_software, capabilities[] y config_schema. Un nodo que declara nada nunca se policea contra baseline (los nodos existen antes de su primer handshake).
  • →Heartbeat reporta llamadas activas, carga, memoria/disco, registros y active_config_version — lo que el nodo dice correr es autoritativo.
  • →ConfigPush / ConfigAck con resultados APPLIED · REJECTED · DUPLICATE · SUPERSEDED · DEFERRED más findings[] (WARN = aplicó con matices, ERROR = rechazó).
  • →CDRs por batch con ack (el agente solo avanza su cursor tras el ack), RegistrationSync por deltas, EventBatch idempotente por event_id, renovación de certs y rollouts de binario del agente.
Nodos OBSERVER existen para inventario durante migraciones: reportan pero nunca reciben pushes. El aprovisionamiento cloud automático es aspiracional (hoy solo se listan regiones).
04 // INTENCIÓN DECLARATIVA

Configuración y versiones#

La consola y la API operan sobre intención estructurada — jamás archivos crudos de PBX. El engine (config.Build) compila el NodeConfig desde un snapshot y omite bloques que Talkadillo no necesita (p. ej. todo SIPSettings).

Extensiones — PJSIP/SIP/WebRTC, DND, desvíos (all/busy/no-answer), voicemail + PIN + saludo, max contacts, ubicación E911.
Troncales — PJSIP/SIP/IAX2/DAHDI, registro o IP-auth, codecs, límite de canales, troncal de failover, TLS/SRTP.
Rutas — entrantes por patrón DID con prioridad; salientes con patrones + secuencia de troncales + transformación (strip_digits/prepend).
IVR · Ring groups · Colas — IVR con reintentos y destinos inválido/timeout; ring groups y colas con 6 estrategias cada uno, miembros con penalidad/pausa.
Time conditions · MOH · Prompts — reglas con timezone; MOH single-asset para Talkadillo; librería recordings siempre sin comprimir (Opus sería irreproducible a 8 kHz).
Feature codes — *97 voicemail, *8 pickup, *70 park, *1 automon.
VERSIONADO. Cada push escribe una fila inmutable en node_config_versions (JSONB). Rollback = re-aplicar una versión anterior. Auditoría completa siempre.
DRIFT ADVISORY. Si active_config_version del heartbeat difiere de la última aplicada, se marca drift y decide el operador (re-push o adoptar) — jamás auto-repush, para no flapear un dial plan en producción.
CAPABILITIES. PushNodeConfig rechaza antes de escribir si el nodo no puede honrar la config; los matices warn llegan en el ack y quedan en la auditoría.
05 // VOZ

Telefonía avanzada y E911#

Vive en nodes.settings.telephony (ahí lo lee el engine) con API tipada y validada encima: parqueo con órbitas y retorno al que parqueó, conferencias con PIN de moderador, paging unidireccional/intercom con política ante ocupado, grupos de pickup y set de prompts de buzón por nodo.

E911. Ubicación despachable por extensión (street / detail / city / region / postalCode / country) + número de callback: ambos o ninguno. Política de emergencia por nodo con números y lista de notificación. nodes.settings.emergency.notify alimenta webhooks de emergencia.
06 // ANALÍTICA

CDR y analytics#

CDRs en ClickHouse, no en Postgres: tabla ReplacingMergeTree, partición mensual, TTL 7 años. Entrega at-least-once con dedup determinista (UUID5 de la línea cruda) y lecturas con FINAL. Incluye topología de forking, causa SIP, app/route de dialplan y diagnóstico por pata.

  • →cdrs (log paginado), callSummary (totales del mismo filtro), callBreakdown(dimension:) (12 dimensiones: caller, destino, nodo, cuenta, hora, día, outcome, cause, troncal, app, route) y callTrend.
  • →Export GET /api/v1/cdrs.csv en streaming con el mismo filtro que la UI; un export truncado termina en línea # TRUNCATED, nunca pasa por respuesta completa.
  • →Los agregados exigen ventana temporal: un GROUP BY sin filtro sería un scan de años de flota.
07 // AUDIO

Archivo, compresión y transcripción#

Dos mundos con la misma palabra: recordings es la librería de prompts (panel → nodo, por gRPC) y media_assets es el archivo de llamadas y buzones (nodo → object storage, nunca dentro de una config).

SUBIDA POR URL PRESIGNADA. El audio no viaja por gRPC: el nodo pide grant, hace PUT directo al bucket (10 min, una key, un método) y reporta completitud.
LA FILA PRECEDE A LOS BYTES. El row se escribe al otorgar; un objeto huérfano en el bucket sería una factura inexplicable. Solo el estado verified (HEAD + tamaño + digest) autoriza al nodo a borrar. Una negativa nunca es permiso de borrar.
OPUS 24 KBPS. ~115 MB/h → ~11 MB. Estéreo preservado (pata A izquierda, B derecha). El original se conserva con grace period. Cola media_jobs con lease FOR UPDATE SKIP LOCKED.
Políticas de storage por cuenta con herencia root-first (el secreto no se mergea, se hereda entero), cifrado con TDPBX_STORAGE_ENCRYPTION_KEY dedicada. Transcripción Whisper local/API sobre la misma interfaz OpenAI; sin proveedor fallback (fallar ruidoso antes que filtrar audio al proveedor equivocado) y con transcripts.provider como auditoría.
08 // ENDPOINTS

Tatu, portal self-service y físicos#

Tres identidades separadas: User (staff, solo OIDC), PortalUser (empleado del cliente, login + TOTP) y Extension (registro SIP sin login). Un portal user puede tener varias extensiones y varios devices Tatu (móvil, escritorio y web).

  • →Tatu: bootstrap REST (login/TOTP, rotación de refresh, QR de onboarding por staff, pairing entre devices), perfil myProvisioningProfile (líneas + policy efectiva + profileVersion para polling barato) y reporte de calidad (MOS/jitter) a ClickHouse.
  • →Portal /portal: mis llamadas, buzones y ajustes propios. El sujeto se deriva, nunca se suministra. La reproducción de buzón otorga una sola URL por escucha (network-only) con check de pertenencia; los campos editables son un set fijo vía statement dedicado, no el update general.
  • →Sesiones: staff cookie tdpbx_session (30 días idle, re-emitida al día, revocación inmediata por session_version); portal cookie tdpbx_portal verificada contra la fila del device en cada request; Tatu access token de 15 min.
  • →Físicos: plantillas Yealink/Grandstream/Fanvil (librería global) y auto-provisión por /provisioning/{token}/{mac}.cfg — la URL es bearer y nunca se loguea.
Policies de softphone en JSONB con herencia por árbol de cuentas + override por usuario; el staff ve capas propias, efectivas y contribuyentes.
09 // EVENTOS

Webhooks salientes#

La única forma en que el panel avisa hacia afuera. Una entrega es una deuda: la fila solo se retira con 2xx. Un hecho cubierto sin suscripción genera una fila undeliverable visible en vez de silencio verde.

  • →La transición se decide por WRITE (UpdateNodeStatus retorna lo reemplazado): dos instancias jamás anuncian dos veces el mismo outage.
  • →Idempotencia desde la transición: call.emergency usa el event_id del nodo; node.config_drift clavea (nodo, esperado, ejecutado, fecha UTC) para no notificar 2.880 veces al día.
  • →Si la emergencia no se puede encolar, se acorta el ack del EventBatch para que el nodo reintente; si el payload no se puede construir, se salta para no atascar el stream. Firmado HMAC con TDPBX_WEBHOOK_ENCRYPTION_KEY dedicada; dispatcher con lease, batch = slots libres y drain en shutdown.
10 // CONFIANZA

Seguridad y RBAC#

  • →mTLS bilateral (RequireAndVerifyClientCert contra la CA propia). Sin puertos inbound abiertos en el borde; el audio RTP nunca sale de los nodos.
  • →Roles + cuentas, independientes: viewer < accounting < engineer < admin < super_admin como piso por mutación; la cuenta filtra cada lectura/escritura por subárbol. El "manager de plataforma" y el "manager de cuenta" son el mismo rol sobre distintas cuentas.
  • →User management anti-escalada: dentro del subárbol propio, sin actuar sobre superiores ni otorgar rol mayor al propio, nunca sobre uno mismo, y nunca dejando la plataforma sin super_admin activo. Entrar es OIDC: crear usuario no emite credencial, el primer login reclama la fila por email.
  • →Auditoría: cada cambio de config deja snapshot + hash; secretos SIP visibles solo con rol engineer y lectura auditada.
  • →Firewall observable, no gestionado: lo reportado (observed_sip) vs lo observado desde fuera (sonda sentinel TCP: RST = sin firewall, DROP = filtrado). Advisory siempre: nunca bloquea un push ni cambia estado. Rechaza loopback/RFC1918 y solo usa TCP.
11 // NOC

Monitoreo y operación#

  • →Tiempo real: subscriptions nodeStatus · nodeConfigStatus · nodeAgentUpdateStatus · nodeWssCertStatus · fleetStatus (vía Redis) + snapshot de operador (llamadas vivas, presencia) plegado desde eventos.
  • →Liveness desconfiado: sweep a 3 intervalos de heartbeat que se niega a marcar offline si el nodo aún tiene stream local o si voltearía a la mayoría del fleet. Emite control_plane.degraded y métricas de supresión/fallos de escritura.
  • →Rechazos visibles: node_connection_rejections agrega por (identidad, motivo, IP) con conteos — un nodo retirado que sigue llamando se ve en /connections en vez de perderse en logs.
  • →Rollouts: updates de binario del agente por flota con concurrencia acotada y verificación SHA256; certs WSS vía ACME DNS-01; inventario cloud como snapshot con timestamp (nunca lectura viva).
12 // INTEGRACIÓN

API GraphQL#

Schema-first: api/graph/schema.graphql es la fuente de verdad y gqlgen genera los tipos Go. Convenciones: paginación cursor Relay (Connection/Edge), mutaciones con payload ({ recurso, errors }) y subscriptions para estado live.

70+ QUERIES
Cuentas, usuarios, nodos, extensiones, troncales, rutas, IVR, colas, CDRs, media, softphones, webhooks, portal self-service.
80+ MUTACIONES
CRUD de telefonía, push/rollback de config, policies, credenciales de storage, pairing Tatu, provisioning físico, webhooks.
RUTAS HTTP
GET /api/v1/cdrs.csv (export), /provisioning (teléfonos), /downloads (agente), /tatu/onboard.
13 // OPERACIÓN

Runbooks#

Procedimientos en docs/runbooks/ del repo:

node-onboarding — token de un solo uso → install script → cert en primer arranque.
node-down — respuesta ante liveness sweep y guards proporcionales.
node-firewall — lectura de veredictos sentinel y alertas.
user-management — las 4 reglas anti-escalada aplicadas.
cert-rotation — rotación de CA/certs sin tirar la flota.
backup-restore — Postgres + ClickHouse.
deploy-rollback — releases versionados con auto-rollback por health check.
tatu-onboarding — QR staff, pairing y políticas.
webhooks / media-storage — deuda de entregas y ciclo del archivo.
webrtc / phone-provisioning — certs ACME y plantillas por vendor.
14 // HONESTIDAD

Gaps conocidos#

Matriz verificada contra nodo lab (docs/PARITY.md). Talkadillo hoy no expresa: toggles DND *78/*79, desvío a móvil/externo, conditional forward ni miembros externos en ring groups. El aprovisionamiento cloud (crear instancias) y el relay de media por gRPC están especificados pero no implementados. La transcripción nunca hace fallback entre proveedores por diseño.