Compliance
Esta página cubre la postura de compliance de SipSop: obligaciones regulatorias de NJ/FCC, retención de datos para registros CDR y PII, arquitectura de Row Level Security (RLS), cobertura del audit log, y nuestro checklist operacional SOC2-lite.
Alcance
SipSop opera actualmente como carrier reseller_retail (Twilio wholesale, SipSop retail). En este modo, Twilio remite los passthroughs gubernamentales (E911, FUSF). Las obligaciones regulatorias directas de SipSop se limitan a cobro/remittance de NJ Sales Tax y filing de FCC 499. Esta página se actualizará cuando cambie el carrier mode.
Obligaciones regulatorias NJ/FCC
FCC Form 499
SipSop debe presentar el FCC Form 499-A anualmente como carrier/reseller de telecomunicaciones. El FCC 499 Filer ID está almacenado en billing_system_config.fcc_499_filer_id. Requerido incluso en modo reseller_retail.
Acción requerida: Presentar antes del 1ro de abril cada año. El reporte trimestral (adminFees.quarterlyReport) provee las cifras de ingresos necesarias para el filing del 499.
NJ Sales Tax (ST-50)
SipSop cobra NJ Sales Tax (actualmente 6.625%) a los tenants con jurisdiction_code = 'NJ'. Se reporta y remite trimestralmente a la NJ Division of Taxation:
- Filing: NJ Form ST-50 (trimestral, online via portal NJ Tax)
- Vencimientos: 30 de abril, 31 de julio, 31 de octubre, 31 de enero
- Monto: Suma de todos los
invoice_line_itemsdondecode = 'SALES_TAX_NJ'del trimestre - ID de cert: Almacenado en
billing_system_config.nj_dot_sales_tax_cert_id
Para obtener el total trimestral:
trpc.adminFees.quarterlyReport({ period: 'Q1_2026' })
→ filtrar filas donde code = 'SALES_TAX_NJ'
→ totalAmountCents / 100 = monto en USD a remitirE911 (si aplica)
En modo reseller_retail, Twilio maneja la remittance del surcharge E911. Si SipSop cambia a reseller_wholesale, se vuelve responsable de remitir el surcharge NJ 911 a la NJ Division of Revenue.
Privacidad VoIP (CPNI)
Las reglas de Customer Proprietary Network Information (CPNI) bajo 47 CFR §64.2001 aplican. Los datos CDR (números de llamante, números llamados, duración) son CPNI. Obligaciones clave:
- Sin compartir con terceros sin consentimiento del cliente
- Filing anual de certificación CPNI con FCC
- Medidas de seguridad razonables para datos CPNI (cubierto por RLS + cifrado en reposo)
Retención de datos y PII
Registros CDR (cdr_records)
Los datos CDR contienen PII: número de teléfono del llamante (src), número llamado (dst), timestamp, duración. Política de retención:
| Datos | Retención | Motivo |
|---|---|---|
| Registros CDR | 7 años | Requisitos de record-keeping de FCC para carriers |
| Facturas de billing | 7 años | Ley impositiva de EE.UU. |
| Historial de fee definitions | Indefinido | Audit trail de compliance |
| Audit log | 7 años | SOC2 / regulatorio |
| Session tokens | Hasta expiración (máx 30 días) | Solo auth |
Hard deletion: Los registros CDR nunca se borran permanentemente excepto por offboarding explícito del tenant, que requiere:
- Solicitud escrita del tenant
- Finalización de última factura y pago
- Período de enfriamiento de 30 días
- Borrado manual en SQL después de aprobación del MSP admin
PII en cdr_records
| Columna | Nivel PII | Notas |
|---|---|---|
src | Alto — número de teléfono del llamante | Almacenado tal como viene de FreePBX |
dst | Alto — número de teléfono llamado | Almacenado tal como viene de FreePBX |
calldate | Medio — timestamp | |
billsec | Bajo | Solo duración |
disposition | Ninguno | ANSWERED/NO ANSWER/etc |
cost | Bajo | Cálculo derivado |
Acceso: cdr_records tiene RLS. Ningún código de aplicación lee CDRs de un tenant sin que se haya ejecutado SET app.current_tenant = '{id}' primero en la sesión.
PII en tabla user
Almacena: name, email. Sin números de teléfono ni direcciones en la tabla de usuarios. Las direcciones de facturación están en tenant_billing_config.billing_address (JSONB) y tenants.billing_address.
Derecho a eliminación (orientado a GDPR)
SipSop no está regulado por GDPR (mercado US únicamente), pero seguimos un enfoque pragmático de privacy-by-design. Si un tenant solicita eliminación completa de datos:
- Setear
tenants.status = 'cancelled' - Emitir factura final y confirmar pago
- Después del período de retención de 30 días, borrar:
cdr_records WHERE tenant_id = ?,billing_periods WHERE tenant_id = ?,invoices WHERE tenant_id = ?,invoice_line_items WHERE tenant_id = ?,tenant_billing_config WHERE tenant_id = ? - En Better Auth: borrar la fila de
organization(cascade amember,invitation) - Filas
userde client users: soft-delete seteandois_active = false - Documentar la eliminación en
audit_logcon actiontenant.offboard.pii_deletion
Row Level Security (RLS)
Qué protege RLS
Tres tablas tienen políticas RLS de Postgres enforced por un rol sipsop_app (NOSUPERUSER):
| Tabla | Política RLS |
|---|---|
cdr_records | WHERE tenant_id = current_setting('app.current_tenant')::uuid |
billing_periods | Igual |
invoices | Igual |
invoice_line_items | Igual |
Tablas sin RLS (accedidas via consultas admin cross-tenant): tenants, plans, pbx_servers, fee_definitions, fee_definition_history, billing_system_config, audit_log, msp_role_definitions, tablas commission_*.
Cómo se setea RLS
En apps/api/src/trpc/context.ts, cada request autenticado ejecuta:
// Patrón de middleware/tenant.ts
await db.execute(sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`)El flag true lo hace transaction-local. Después del request, el setting se resetea.
Para rutas admin (adminProcedure) que necesitan consultar cross-tenant (ej: getGlobalOverview), la tabla tenants no tiene RLS, así que las consultas cross-tenant funcionan. Sin embargo, billing_periods tiene RLS — esto es una limitación conocida documentada en admin.service.ts. Fix de producción: usar una función SECURITY DEFINER o un rol DB dedicado para admin.
Aplicar RLS
RLS se aplica via:
# Local / dev — rol + policies de una (lo encadena db:push)
pnpm --filter @sipsop/db db:apply-rls
# Cualquier entorno, producción incluida — solo policies, sin tocar credenciales
pnpm --filter @sipsop/db db:apply-rls-policies
# Aprovisionamiento inicial de un entorno (o rotación de credencial) — solo el rol.
# Requiere SIPSOP_APP_PASSWORD; falla ruidosamente si no está (no hay default).
SIPSOP_APP_PASSWORD='<password>' pnpm --filter @sipsop/db db:bootstrap-app-roleLas policies y las credenciales están separadas a propósito en tres scripts de packages/db/src/:
| Script | Responsabilidad | Dónde corre |
|---|---|---|
apply-rls-policies.ts | ENABLE/FORCE ROW LEVEL SECURITY, policies, grants. Idempotente | Cada deploy del API (deploy-api.yml), CI y a demanda |
bootstrap-app-role.ts | Crea o rota la password del rol sipsop_app | A mano, una vez por entorno |
apply-rls.ts | Los dos, en orden | Solo dev local |
Correr db:apply-rls contra producción rotaría la password de sipsop_app y dejaría a la API sin acceso a la base (su DATABASE_URL_APP sale de Key Vault). Por eso el pipeline de deploy corre apply-rls-policies.ts y nunca apply-rls.ts.
Audit log
La tabla audit_log (en packages/db/src/schema/fees.ts) es append-only. Captura operaciones sensibles:
| Acción | Qué la dispara |
|---|---|
fee.create | Nueva fee definition creada |
fee.update | Metadata de fee actualizada (sin cambio de rate) |
fee.rate_change | Cambio de rate — nueva versión creada |
fee.deactivate | Fee soft-deleted |
carrier_mode.change | Carrier mode de facturación global cambiado |
registration.update | Estado de registro regulatorio actualizado |
Eventos de auditoría adicionales que deberían agregarse: tenant.status_change, tenant.offboard, impersonation.start, impersonation.end.
Leer eventos de auditoría:
SELECT
to_char(timestamp, 'YYYY-MM-DD HH24:MI:SS') AS ts,
action,
entity_type,
entity_id,
u.email AS changed_by,
payload
FROM audit_log al
LEFT JOIN "user" u ON u.id = al.user_id
ORDER BY timestamp DESC
LIMIT 100;Commission attribution log
La tabla commission_attribution_log (en packages/db/src/schema/commissions/) provee un audit trail inmutable de cómo se atribuyeron las comisiones. Nunca modificar esta tabla — se usa para reconciliar disputas.
Log de impersonation
La tabla admin_impersonations registra cada sesión de impersonation:
- Quién la inició (
impersonator_user_id) - De quién fue la cuenta impersonada (
impersonated_user_id) - Timestamps de inicio y fin
- IP address y user agent
Al impersonar, las operaciones de escritura son bloqueadas por impersonationAwareProcedure en trpc.ts. Las sesiones de impersonation son de solo lectura.
Secretos y entorno
Sin secretos en código ni archivos .env en producción. Todos los secretos de producción viven en Azure Key Vault:
| Secreto | Nombre en Key Vault |
|---|---|
DATABASE_URL | sipsop-database-url |
BETTER_AUTH_SECRET | sipsop-auth-secret |
STRIPE_SECRET_KEY | sipsop-stripe-key |
STRIPE_WEBHOOK_SECRET | sipsop-stripe-webhook-secret |
FREEPBX_DB_PASSWORD | sipsop-freepbx-password |
Las referencias se inyectan en runtime via bindings de variables de entorno de Azure Container Apps (Key Vault references).
Checklist SOC2-lite
Este es un checklist interno pragmático, no una certificación formal. Revisar trimestralmente.
Control de acceso
- [x] Todas las operaciones admin requieren rol
admin_msp(adminProcedurelo enforcea) - [x] Operaciones de billing clerk requieren sub-rol MSP
billing_clerk - [x] Cambios de carrier mode requieren sub-rol
super_admin - [x] Los clientes están aislados por org (Better Auth) y tenant (RLS)
- [x] Impersonation está logueada y protegida contra escritura
- [ ] MFA para usuarios
admin_msp— planificado, no implementado aún - [ ] Proceso formal de revisión de accesos — revisar trimestralmente quién tiene rol
admin_msp
Audit logs
- [x] Cambios en fee definitions logueados en
audit_log - [x] Cambios de carrier mode logueados con IP + user agent
- [x] Atribución de comisiones logueada en
commission_attribution_log - [x] Impersonation logueada en
admin_impersonations - [ ] Cambios de estado de tenant aún no están en
audit_log— agregar eventostenant.status_change
Cifrado de datos
- [x] Cifrado en tránsito: HTTPS enforced via Azure Container Apps (TLS 1.2+)
- [x] Cifrado en reposo: Azure Database for PostgreSQL (transparent data encryption habilitado)
- [x] Redis: sin PII almacenada en Redis (solo session tokens y colas de jobs)
- [x] Stripe: datos de pago manejados por Stripe, sin datos de tarjeta en DB de SipSop
Backup y recuperación
- [x] Postgres: backups automáticos de Azure Database, retención de 7 días
- [ ] Point-in-time recovery: testear restore trimestralmente
- [ ] Runbook de disaster recovery: documentado en Runbooks
Gestión de vulnerabilidades
- [x] Dependencias:
pnpm audit --prodejecutado en CI - [x] Imágenes de containers: construidas desde
node:22-alpine(mínima superficie de ataque) - [ ] Actualizaciones regulares de dependencias: programar revisión mensual
- [ ] Penetration testing: aún no realizado
Respuesta a incidentes
- [x] Sentry error tracking para API y portal
- [x] Monitoreo de fallos en jobs BullMQ (cola failed)
- [ ] Rotación de oncall documentada — ver Runbooks
- [ ] Proceso de post-mortem aún no formalizado
Contactos regulatorios clave
| Autoridad | Contacto | Propósito |
|---|---|---|
| FCC | fcc.gov/licensing/forms/499 | Filing anual 499-A |
| NJ Division of Taxation | njportal.com/taxation | ST-50 sales tax trimestral |
| NJ BPU | nj.gov/bpu | Si/cuando se aplique para cert CLEC |
| Twilio | Equipo de compliance via dashboard | Preguntas sobre passthrough de E911, FUSF |