Skip to content

Compliance

This page covers SipSop's compliance posture: NJ/FCC regulatory obligations, data retention for CDR records and PII, Row Level Security (RLS) architecture, audit log coverage, and our SOC2-lite operational checklist.

Scope

SipSop is currently operated as a reseller_retail carrier (Twilio wholesale, SipSop retail). In this mode, Twilio remits government passthroughs (E911, FUSF). SipSop's direct regulatory obligations are limited to NJ Sales Tax collection/remittance and FCC 499 filing. This page will be updated when carrier mode changes.

NJ/FCC regulatory obligations

FCC Form 499

SipSop must file FCC Form 499-A annually as a telecommunications carrier/reseller. The 499 Filer ID is stored in billing_system_config.fcc_499_filer_id. Required even in reseller_retail mode.

Action required: File annually by April 1. The quarterly report (adminFees.quarterlyReport) provides the revenue figures needed for the 499 filing.

NJ Sales Tax (ST-50)

SipSop collects NJ Sales Tax (currently 6.625%) from tenants with jurisdiction_code = 'NJ'. This is reported and remitted quarterly to the NJ Division of Taxation:

  • Filing: NJ Form ST-50 (quarterly, online via NJ Tax portal)
  • Due dates: April 30, July 31, October 31, January 31
  • Amount: Sum of all invoice_line_items where code = 'SALES_TAX_NJ' for the quarter
  • Cert ID: Stored in billing_system_config.nj_dot_sales_tax_cert_id

To get the quarterly total:

trpc.adminFees.quarterlyReport({ period: 'Q1_2026' })
→ filter rows where code = 'SALES_TAX_NJ'
→ totalAmountCents / 100 = USD amount to remit

E911 (if applicable)

In reseller_retail mode, Twilio handles E911 surcharge remittance. If SipSop switches to reseller_wholesale, it becomes responsible for remitting the NJ 911 surcharge to NJ Division of Revenue.

VoIP privacy (CPNI)

Customer Proprietary Network Information (CPNI) rules under 47 CFR §64.2001 apply. CDR data (caller numbers, called numbers, duration) is CPNI. Key obligations:

  • No sharing with third parties without customer consent
  • Annual CPNI certification filing with FCC
  • Reasonable security measures for CPNI data (addressed by RLS + encryption at rest)

Data retention and PII

CDR records (cdr_records)

CDR data contains PII: caller phone number (src), called number (dst), timestamp, duration. Retention policy:

DataRetentionReason
CDR records7 yearsFCC record-keeping requirements for carriers
Billing invoices7 yearsUS tax law
Fee definition historyIndefiniteCompliance audit trail
Audit log7 yearsSOC2 / regulatory
Session tokensUntil expiry (max 30 days)Auth only

Hard deletion: CDR records are never hard-deleted except by explicit tenant offboarding, which requires:

  1. Written request from tenant
  2. Completion of final invoice and payment
  3. 30-day cooling-off period
  4. Manual SQL deletion after approval from MSP admin

PII in cdr_records

ColumnPII levelNotes
srcHigh — caller phone numberStored as-is from FreePBX
dstHigh — called phone numberStored as-is from FreePBX
calldateMedium — timestamp
billsecLowDuration only
dispositionNoneANSWERED/NO ANSWER/etc
costLowDerived calculation

Access: cdr_records has RLS. No application code reads CDRs for a tenant without SET app.current_tenant = '{id}' being executed first in the session.

PII in user table

Stores: name, email. No phone numbers or addresses in the user table. Billing addresses are in tenant_billing_config.billing_address (JSONB) and tenants.billing_address.

Right to deletion (GDPR-adjacent)

SipSop is not GDPR-regulated (US market only), but we follow a pragmatic privacy-by-design approach. If a tenant requests full data deletion:

  1. Set tenants.status = 'cancelled'
  2. Issue final invoice and confirm payment
  3. After 30-day retention window, delete: 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. In Better Auth: delete the organization row (cascades to member, invitation)
  5. user rows for client users: soft-delete by setting is_active = false
  6. Document deletion in audit_log with action tenant.offboard.pii_deletion

Row Level Security (RLS)

What RLS protects

Three tables have Postgres RLS policies enforced by a sipsop_app role (NOSUPERUSER):

TableRLS policy
cdr_recordsWHERE tenant_id = current_setting('app.current_tenant')::uuid
billing_periodsSame
invoicesSame
invoice_line_itemsSame

Tables without RLS (accessed via admin queries cross-tenant): tenants, plans, pbx_servers, fee_definitions, fee_definition_history, billing_system_config, audit_log, msp_role_definitions, commission_* tables.

How RLS is set

In apps/api/src/trpc/context.ts, every authenticated request runs:

ts
// middleware/tenant.ts pattern
await db.execute(sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`)

The true flag makes it transaction-local. After the request completes, the setting resets.

For admin routes (adminProcedure) that need to query across tenants (e.g., getGlobalOverview), the tenants table has no RLS, so cross-tenant queries work. However, billing_periods has RLS — this is a known limitation documented in admin.service.ts. Production fix: use a SECURITY DEFINER function or a dedicated admin DB role.

Applying RLS

RLS is applied via:

bash
# Local / dev — role + policies in one go (chained by db:push)
pnpm --filter @sipsop/db db:apply-rls

# Any environment, production included — policies only, no credentials touched
pnpm --filter @sipsop/db db:apply-rls-policies

# Initial provisioning of an environment (or credential rotation) — role only.
# Requires SIPSOP_APP_PASSWORD; it fails loudly if unset (there is no default).
SIPSOP_APP_PASSWORD='<password>' pnpm --filter @sipsop/db db:bootstrap-app-role

Policies and credentials are deliberately split across three scripts in packages/db/src/:

ScriptResponsibilityWhere it runs
apply-rls-policies.tsENABLE/FORCE ROW LEVEL SECURITY, policies, grants. IdempotentEvery API deploy (deploy-api.yml), CI, and on demand
bootstrap-app-role.tsCreates or rotates the password of the sipsop_app roleBy hand, once per environment
apply-rls.tsBoth, in orderLocal dev only

Running db:apply-rls against production would rotate the sipsop_app password and cut the API off from the database (its DATABASE_URL_APP comes from Key Vault). That is why the deploy pipeline runs apply-rls-policies.ts and never apply-rls.ts.

Audit log

The audit_log table (in packages/db/src/schema/fees.ts) is append-only. It captures sensitive operations:

ActionWhat triggered it
fee.createNew fee definition created
fee.updateFee metadata updated (no rate change)
fee.rate_changeRate change — new version created
fee.deactivateFee soft-deleted
carrier_mode.changeGlobal billing carrier mode switched
registration.updateRegulatory registration status updated

Additional audit events should be added for: tenant.status_change, tenant.offboard, impersonation.start, impersonation.end.

Reading audit events:

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

The commission_attribution_log table (in packages/db/src/schema/commissions/) provides an immutable audit trail of how commissions were attributed. Never modify this table — it is used to reconcile disputes.

Impersonation log

admin_impersonations table tracks every impersonation session:

  • Who initiated it (impersonator_user_id)
  • Whose account was impersonated (impersonated_user_id)
  • Start and end timestamps
  • IP address and user agent

When impersonating, write operations are blocked by impersonationAwareProcedure in trpc.ts. Impersonation sessions are read-only.

Secrets and environment

No secrets in code or .env files in production. All production secrets live in Azure Key Vault:

SecretKey Vault name
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

References are injected at runtime via Azure Container Apps environment variable bindings (Key Vault references).

SOC2-lite checklist

This is a pragmatic internal checklist, not a formal certification. Review quarterly.

Access control

  • [x] All admin operations require admin_msp role (adminProcedure enforces this)
  • [x] Billing clerk operations require billing_clerk MSP sub-role
  • [x] Carrier mode changes require super_admin sub-role
  • [x] Customers are isolated by org (Better Auth) and tenant (RLS)
  • [x] Impersonation is logged and write-protected
  • [ ] MFA for admin_msp users — planned, not yet implemented
  • [ ] Formal access review process — review quarterly who has admin_msp role

Audit logs

  • [x] Fee definition changes logged to audit_log
  • [x] Carrier mode changes logged with IP + user agent
  • [x] Commission attribution logged to commission_attribution_log
  • [x] Impersonation logged to admin_impersonations
  • [ ] Tenant status changes not yet in audit_log — add tenant.status_change events

Data encryption

  • [x] Encryption in transit: HTTPS enforced via Azure Container Apps (TLS 1.2+)
  • [x] Encryption at rest: Azure Database for PostgreSQL (transparent data encryption enabled)
  • [x] Redis: no PII stored in Redis (only session tokens and job queues)
  • [x] Stripe: payment data handled by Stripe, no card data stored in SipSop DB

Backup and recovery

  • [x] Postgres: Azure Database automated backups, 7-day retention
  • [ ] Point-in-time recovery: test restore quarterly
  • [ ] Disaster recovery runbook: documented in Runbooks

Vulnerability management

  • [x] Dependencies: pnpm audit run in CI (pnpm audit --prod)
  • [x] Container images: built from node:22-alpine (minimal attack surface)
  • [ ] Regular dependency updates: schedule monthly review
  • [ ] Penetration testing: not yet performed

Incident response

  • [x] Sentry error tracking for both API and portal
  • [x] BullMQ job failure monitoring (failed queue)
  • [ ] On-call rotation documented — see Runbooks
  • [ ] Incident post-mortem process not yet formalized

Key regulatory contacts

AuthorityContactPurpose
FCCfcc.gov/licensing/forms/499Annual 499-A filing
NJ Division of Taxationnjportal.com/taxationQuarterly ST-50 sales tax
NJ BPUnj.gov/bpuIf/when applying for CLEC cert
TwilioCompliance team via dashboardE911, FUSF passthrough questions

SipSop documentation. Product operated by Sopinf Tech LLC.