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_itemswherecode = '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 remitE911 (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:
| Data | Retention | Reason |
|---|---|---|
| CDR records | 7 years | FCC record-keeping requirements for carriers |
| Billing invoices | 7 years | US tax law |
| Fee definition history | Indefinite | Compliance audit trail |
| Audit log | 7 years | SOC2 / regulatory |
| Session tokens | Until expiry (max 30 days) | Auth only |
Hard deletion: CDR records are never hard-deleted except by explicit tenant offboarding, which requires:
- Written request from tenant
- Completion of final invoice and payment
- 30-day cooling-off period
- Manual SQL deletion after approval from MSP admin
PII in cdr_records
| Column | PII level | Notes |
|---|---|---|
src | High — caller phone number | Stored as-is from FreePBX |
dst | High — called phone number | Stored as-is from FreePBX |
calldate | Medium — timestamp | |
billsec | Low | Duration only |
disposition | None | ANSWERED/NO ANSWER/etc |
cost | Low | Derived 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:
- Set
tenants.status = 'cancelled' - Issue final invoice and confirm payment
- 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 = ? - In Better Auth: delete the
organizationrow (cascades tomember,invitation) userrows for client users: soft-delete by settingis_active = false- Document deletion in
audit_logwith actiontenant.offboard.pii_deletion
Row Level Security (RLS)
What RLS protects
Three tables have Postgres RLS policies enforced by a sipsop_app role (NOSUPERUSER):
| Table | RLS policy |
|---|---|
cdr_records | WHERE tenant_id = current_setting('app.current_tenant')::uuid |
billing_periods | Same |
invoices | Same |
invoice_line_items | Same |
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:
// 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:
# 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-rolePolicies and credentials are deliberately split across three scripts in packages/db/src/:
| Script | Responsibility | Where it runs |
|---|---|---|
apply-rls-policies.ts | ENABLE/FORCE ROW LEVEL SECURITY, policies, grants. Idempotent | Every API deploy (deploy-api.yml), CI, and on demand |
bootstrap-app-role.ts | Creates or rotates the password of the sipsop_app role | By hand, once per environment |
apply-rls.ts | Both, in order | Local 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:
| Action | What triggered it |
|---|---|
fee.create | New fee definition created |
fee.update | Fee metadata updated (no rate change) |
fee.rate_change | Rate change — new version created |
fee.deactivate | Fee soft-deleted |
carrier_mode.change | Global billing carrier mode switched |
registration.update | Regulatory registration status updated |
Additional audit events should be added for: tenant.status_change, tenant.offboard, impersonation.start, impersonation.end.
Reading audit events:
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:
| Secret | Key Vault name |
|---|---|
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 |
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_msprole (adminProcedureenforces this) - [x] Billing clerk operations require
billing_clerkMSP sub-role - [x] Carrier mode changes require
super_adminsub-role - [x] Customers are isolated by org (Better Auth) and tenant (RLS)
- [x] Impersonation is logged and write-protected
- [ ] MFA for
admin_mspusers — planned, not yet implemented - [ ] Formal access review process — review quarterly who has
admin_msprole
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— addtenant.status_changeevents
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
| Authority | Contact | Purpose |
|---|---|---|
| FCC | fcc.gov/licensing/forms/499 | Annual 499-A filing |
| NJ Division of Taxation | njportal.com/taxation | Quarterly ST-50 sales tax |
| NJ BPU | nj.gov/bpu | If/when applying for CLEC cert |
| Twilio | Compliance team via dashboard | E911, FUSF passthrough questions |