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.
The Compliance Portal is intentionally isolated from the Admin portal (write controls) and the
Finance Portal (ledger data). A compliance_officer JWT cannot create or modify rate tables,
programs, or financial records. Write operations on compliance filings and licensing remain in the
Admin portal pending migration.
URL & Access
| Environment | URL |
|---|---|
| Production | https://compliance.openinsure.dev |
| Local dev | http://localhost:3007 |
Sign In
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
Roles & Authorization
| JWT role | SpiceDB org relation | Description |
|---|---|---|
compliance_officer | compliance | Standard compliance team access |
superadmin | — | Full access |
system | — | Machine-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.
Navigation
| Section | Page | Route |
|---|---|---|
| Overview | Dashboard | / |
| Licensing | Producers | /producers/licensing |
| Analytics | Compliance | /analytics/compliance |
| Analytics | Claims | /analytics/claims |
| Regulatory | Filings | /compliance/filings |
| Regulatory | Rules (view) | /rules |
| Audit | Activity Log | /audit |
Pages
Dashboard (/)
Three-pane layout (flex h-full overflow-hidden):
Left pane (w-64) — KPI summary cards:
- Expiring Licenses (30d) —
KPICardwithstatus='warn'if > 0 - Filings Due (30d) —
KPICardwithstatus='warn'if > 0 - Active Producers —
KPICardneutral - 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 KPIsGET /v1/compliance/:orgId/deadlines— center + right pane data
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 listGET /v1/producers/:id/licenses— license records per producerGET /v1/producers/:id/licenses/expiring— upcoming expirations
Compliance Analytics (/analytics/compliance)
Filing status breakdown by state and line of business. Data from GET /v1/analytics/compliance. Status categories: overdue → due_soon → on_track → filed.
Claims Analytics (/analytics/claims)
Aggregate claims metrics (frequency, severity, reserve development) used in regulatory reporting. Data from GET /v1/analytics/claims.
Filings (/compliance/filings)
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 withbg-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) withinitialDatafrom 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
penaltyPerDayfor 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)
Rules (/rules)
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.
Activity Log (/audit)
Immutable audit trail. Fetches from GET /v1/activities. Filterable by user, action type, and date range. Exportable to CSV for regulatory submission.
API Proxy Allowlist
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.
SpiceDB Permissions
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.
Environment Variables
| Variable | Description |
|---|---|
COMPLIANCE_JWT_SECRET | JWT signing secret — set via wrangler secret put |
API_URL | API worker base URL (e.g., https://api.openinsure.dev) |
NEXT_PUBLIC_APP_NAME | "Compliance Portal" — set in wrangler.toml [vars] |
# 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
Deployment
The Compliance Portal deploys as an OpenNext Cloudflare Worker (oi-sys-compliance):
# 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.
Sanctions Screening
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
Architecture
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:
| Backend | When active | Coverage |
|---|---|---|
| OpenSanctions hosted API (production) | OPENSANCTIONS_API_URL = https://api.opensanctions.org | OFAC 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 absent | 8 representative entries, no network call. All 35 unit tests pass without a live endpoint. |
Decision Model
| Score | Decision | Effect |
|---|---|---|
| ≥ 0.85 | block | HTTP 403/451, compliance.sanctions_hit queue event, KV cached for 10 years |
| 0.60–0.84 | review | Request allowed, compliance.sanctions_review queue event, KV cached 24h |
| < 0.60 | pass | Request continues, no event |
Fail-closed: if the OpenSanctions API is unreachable, the API returns HTTP 503. Transactions are never silently allowed through a failed screen.
OpenSanctions API
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 hasid,caption,score,match,datasets,properties - Pricing: Pay-per-query — see opensanctions.org/api for current rates
Self-Hosted VM (idle)
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
Secrets
# 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