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.