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.