Skip to content

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_items donde code = '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 remitir

E911 (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:

DatosRetenciónMotivo
Registros CDR7 añosRequisitos de record-keeping de FCC para carriers
Facturas de billing7 añosLey impositiva de EE.UU.
Historial de fee definitionsIndefinidoAudit trail de compliance
Audit log7 añosSOC2 / regulatorio
Session tokensHasta 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:

  1. Solicitud escrita del tenant
  2. Finalización de última factura y pago
  3. Período de enfriamiento de 30 días
  4. Borrado manual en SQL después de aprobación del MSP admin

PII en cdr_records ​

ColumnaNivel PIINotas
srcAlto — número de teléfono del llamanteAlmacenado tal como viene de FreePBX
dstAlto — número de teléfono llamadoAlmacenado tal como viene de FreePBX
calldateMedio — timestamp
billsecBajoSolo duración
dispositionNingunoANSWERED/NO ANSWER/etc
costBajoCá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:

  1. Setear tenants.status = 'cancelled'
  2. Emitir factura final y confirmar pago
  3. 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 = ?
  4. En Better Auth: borrar la fila de organization (cascade a member, invitation)
  5. Filas user de client users: soft-delete seteando is_active = false
  6. Documentar la eliminación en audit_log con action tenant.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):

TablaPolítica RLS
cdr_recordsWHERE tenant_id = current_setting('app.current_tenant')::uuid
billing_periodsIgual
invoicesIgual
invoice_line_itemsIgual

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:

ts
// 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:

bash
# 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-role

Las policies y las credenciales están separadas a propósito en tres scripts de packages/db/src/:

ScriptResponsabilidadDónde corre
apply-rls-policies.tsENABLE/FORCE ROW LEVEL SECURITY, policies, grants. IdempotenteCada deploy del API (deploy-api.yml), CI y a demanda
bootstrap-app-role.tsCrea o rota la password del rol sipsop_appA mano, una vez por entorno
apply-rls.tsLos dos, en ordenSolo 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ónQué la dispara
fee.createNueva fee definition creada
fee.updateMetadata de fee actualizada (sin cambio de rate)
fee.rate_changeCambio de rate — nueva versión creada
fee.deactivateFee soft-deleted
carrier_mode.changeCarrier mode de facturación global cambiado
registration.updateEstado 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:

sql
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:

SecretoNombre en Key Vault
DATABASE_URLsipsop-database-url
BETTER_AUTH_SECRETsipsop-auth-secret
STRIPE_SECRET_KEYsipsop-stripe-key
STRIPE_WEBHOOK_SECRETsipsop-stripe-webhook-secret
FREEPBX_DB_PASSWORDsipsop-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 (adminProcedure lo 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 eventos tenant.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 --prod ejecutado 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 ​

AutoridadContactoPropósito
FCCfcc.gov/licensing/forms/499Filing anual 499-A
NJ Division of Taxationnjportal.com/taxationST-50 sales tax trimestral
NJ BPUnj.gov/bpuSi/cuando se aplique para cert CLEC
TwilioEquipo de compliance via dashboardPreguntas sobre passthrough de E911, FUSF

Documentación de SipSop. Producto operado por Sopinf Tech LLC.