Skip to content

Compliance Portal

The Compliance Portal (apps/compliance-portal, oi-sys-compliance) is a dedicated Next.js application for the Compliance / Regulatory team. It provides read-only access to producer licensing, state filing workflows, compliance analytics, and the full audit log — all scoped behind the compliance_officer JWT role.

EnvironmentURL
Productionhttps://compliance.openinsure.dev
Local devhttp://localhost:3007

Use the standard OpenInsure sign-in flow. The Compliance Portal sets the oi_compliance_token cookie, verified against COMPLIANCE_JWT_SECRET.

GET https://auth-dev.openinsure.dev/api/auth/sign-in/microsoft
?callbackURL=https://compliance.openinsure.dev/api/auth/callback
JWT roleSpiceDB org relationDescription
compliance_officercomplianceStandard compliance team access
superadminFull access
systemMachine-to-machine (M2M)

The compliance SpiceDB org relation grants read permissions across org resources but explicitly does not grant manage_fiduciary — compliance officers cannot alter financial records.

The compliance_officer role is enforced in apps/compliance-portal/lib/auth.ts. Unauthenticated requests redirect to /login.

SectionPageRoute
OverviewDashboard/
LicensingProducers/producers/licensing
AnalyticsCompliance/analytics/compliance
AnalyticsClaims/analytics/claims
RegulatoryFilings/compliance/filings
RegulatoryRules (view)/rules
AuditActivity Log/audit

Three-pane layout (flex h-full overflow-hidden):

Left pane (w-64) — KPI summary cards:

  • Expiring Licenses (30d)KPICard with status='warn' if > 0
  • Filings Due (30d)KPICard with status='warn' if > 0
  • Active ProducersKPICard neutral
  • Navigation links to /producers/licensing, /compliance/filings, /audit

Center pane (flex-1) — Compliance Calendar:

  • 4-pill count strip: Overdue / Due Soon / On Track / Filed
  • Scrollable deadline cards sorted by urgency (overdue → due soon → on track → filed)
  • Empty state if org has no deadlines

Right pane (w-72) — Urgent Items:

  • Overdue filings listed in red (with penalty/day if applicable)
  • Due-soon filings listed in amber
  • If no urgent items: green checkmark (“All filings on track”)
  • Quick links to licensing, filings, and audit pages

API calls (parallel via Promise.all):

  • GET /v1/compliance/:orgId/summary — left pane KPIs
  • GET /v1/compliance/:orgId/deadlines — center + right pane data

Producers / Licensing (/producers/licensing)

Section titled “Producers / Licensing (/producers/licensing)”

Table of all producers with their license status, expiry dates, and appointment counts by state. Links through to individual producer detail pages at /producers/:id.

Fetches from:

  • GET /v1/producers — paginated producer list
  • GET /v1/producers/:id/licenses — license records per producer
  • GET /v1/producers/:id/licenses/expiring — upcoming expirations

Compliance Analytics (/analytics/compliance)

Section titled “Compliance Analytics (/analytics/compliance)”

Filing status breakdown by state and line of business. Data from GET /v1/analytics/compliance. Status categories: overduedue_soonon_trackfiled.

Aggregate claims metrics (frequency, severity, reserve development) used in regulatory reporting. Data from GET /v1/analytics/claims.

Three-pane layout with URL-driven status filter:

Left pane (w-56) — Filter sidebar:

  • Status filter links (?status=overdue, ?status=due_soon, etc.) — no JavaScript, pure URL params
  • Active state derived from searchParams.status — highlighted with bg-primary/10
  • Count badge next to each filter (computed server-side)
  • Info note: “Filings data updates daily”

Center pane (flex-1)FilingsTable client component:

  • TanStack Query polling (refetchInterval: 60_000) with initialData from server render
  • DataTable (TanStack Table) with columns: Filing Name, Due Date, Days Remaining, Status badge, Penalty/Day, Action
  • Client-side name search (pre-filtered before DataTable — the component does not filter internally)
  • “Mark Filed” dialog (shadcn Dialog): filedAt date input, confirmation number, notes — validates with Zod schema, POST /api/v1/compliance/filings, invalidates query on success

Right pane (w-64) — Server-computed summary stats:

  • Penalty at Risk — sum of penaltyPerDay for overdue filings
  • Due Next 7 Days — count of unfiled filings with daysRemaining ≤ 7
  • Oldest Overdue — max |daysRemaining| among overdue filings
  • Link to compliance analytics for marking filings complete

Source: apps/compliance-portal/app/(dashboard)/compliance/filings/filings-table.tsx (client island)

Read-only view of the underwriting rules engine. Compliance officers can inspect rules for regulatory review but cannot create, edit, or delete rules — those operations require an Admin login.

Immutable audit trail. Fetches from GET /v1/activities. Filterable by user, action type, and date range. Exportable to CSV for regulatory submission.

The Compliance Portal proxies API requests through apps/compliance-portal/app/api/[...path]/route.ts, gated by lib/proxy-allowlist.ts:

GET /v1/analytics/compliance
GET /v1/analytics/claims
GET /v1/producers
GET /v1/producers/:id
GET /v1/producers/:id/licenses
GET /v1/producers/:id/licenses/expiring
GET /v1/producers/:id/appointments
GET /v1/activities
GET /v1/rules
GET /v1/compliance
GET /v1/compliance/:orgId/summary
GET /v1/compliance/:orgId/deadlines
GET /v1/compliance/filings
POST /v1/compliance/filings

Any request not in the allowlist returns 403 Forbidden.

The compliance org relation in SpiceDB (packages/auth/spice/schema.zed) grants:

relation compliance: user

Permissions derived:

  • Granted: view_org, view_submissions, view_policies, view_claims, view_producers, view_audit_log, manage_filings
  • Denied: manage_org, manage_rate_tables, manage_programs, manage_fiduciary, bind_policy

This ensures that a compliance_officer with elevated API access cannot accidentally trigger write operations on financial or underwriting resources.

VariableDescription
COMPLIANCE_JWT_SECRETJWT signing secret — set via wrangler secret put
API_URLAPI worker base URL (e.g., https://api.openinsure.dev)
NEXT_PUBLIC_APP_NAME"Compliance Portal" — set in wrangler.toml [vars]
Terminal window
# Set the JWT secret (production)
wrangler secret put COMPLIANCE_JWT_SECRET --name oi-sys-compliance
# Local dev (.env.local)
COMPLIANCE_JWT_SECRET=9d79c38aa7d57bd24a1afe213848b2b935519afb08a3923ac112aed71fd5bc21
API_URL=http://localhost:8787
DEMO_MODE=true

The Compliance Portal deploys as an OpenNext Cloudflare Worker (oi-sys-compliance):

Terminal window
# Build + deploy
cd apps/compliance-portal
npx opennextjs-cloudflare deploy -- --keep-vars
# Or via pnpm filter
pnpm --filter @openinsure/compliance-portal deploy

CI deploys automatically from master when apps/compliance-portal/ or shared packages change. See CI/CD for the full pipeline.

OFAC / international sanctions screening is enforced at the API layer via packages/compliance/src/sanctions.ts. Applied at three enforcement points:

  • Submission bind (POST /v1/submissions/:id/bind) — screens insured name before creating a policy
  • Producer onboarding (POST /v1/producers) — screens legal name before inserting the record
  • Disbursements (POST /v1/disbursements) — screens payee name with mandatory DB audit log
Request
└─► KV cache check (sanctions:v2:{name}:{country})
├─ HIT → return cached decision (block → 403/451, review/pass → continue)
└─ MISS → live API call → cache result → emit event if block/review

Two backends:

BackendWhen activeCoverage
OpenSanctions hosted API (production)OPENSANCTIONS_API_URL = https://api.opensanctions.orgOFAC SDN + EU FSF + UN 1267 + UK FCDO + SECO + 40 other lists. Nomenklatura cross-dataset deduplication — the same entity across multiple lists returns one canonical result with a confidence score.
Static SDN list (dev / CI)OPENSANCTIONS_API_URL absent8 representative entries, no network call. All 35 unit tests pass without a live endpoint.
ScoreDecisionEffect
≥ 0.85blockHTTP 403/451, compliance.sanctions_hit queue event, KV cached for 10 years
0.60–0.84reviewRequest allowed, compliance.sanctions_review queue event, KV cached 24h
< 0.60passRequest continues, no event

Sanctions matching runs through the OpenSanctions hosted API at api.opensanctions.org. This is the same yente-based matching engine used by OpenSanctions — the API call format and response schema are identical to self-hosted yente.

  • Endpoint: POST https://api.opensanctions.org/match/sanctions
  • Auth: Authorization: ApiKey <OPENSANCTIONS_API_KEY>
  • Request: FTM schema body (Company / Person / Vessel / Aircraft)
  • Response: responses.main.results[] — each result has id, caption, score, match, datasets, properties
  • Pricing: Pay-per-query — see opensanctions.org/api for current rates

A self-hosted yente instance (openinsure-opensanctions) is deployed on Fly.io with Elasticsearch 8 for future migration to the OpenSanctions bulk delivery plan:

  • Region: iad (US East), performance-2x, 4 GB RAM, 10 GB persistent volume
  • Image: Custom Docker — ES 8 + yente (supervisord), infra/opensanctions/
  • Status: Running, no data — awaiting OPENSANCTIONS_DELIVERY_TOKEN (bulk license)
  • Upgrade: Purchase bulk license at opensanctions.org/account/bulk, then fly secrets set OPENSANCTIONS_DELIVERY_TOKEN=<token> -a openinsure-opensanctions && cd infra/opensanctions && fly deploy
Terminal window
# Already set on oi-sys-api Worker
wrangler secret put OPENSANCTIONS_API_URL # https://api.opensanctions.org
wrangler secret put OPENSANCTIONS_API_KEY # from opensanctions.org/account
# Local dev — leave OPENSANCTIONS_API_URL unset
# Static SDN fallback activates automatically; all 35 unit tests pass without a live endpoint
  • 31 CFR Parts 500–598 (OFAC regulations)
  • Executive Order 13224 (terrorism-related sanctions)
  • International Emergency Economic Powers Act (IEEPA)

Finance Portal

TigerBeetle ledger, AR aging, statutory reports, and report builder.

Authentication

JWT roles, portal cookies, and SpiceDB authorization.