HIPAA Compliance
HIPAA (Health Insurance Portability and Accountability Act) applies to any insurance operation that processes, stores, or transmits Protected Health Information (PHI). In the insurance context, this includes workers’ compensation claims with medical records, health captives, stop-loss insurance with individual claimant data, and FHIR-integrated health plan administration.
OpenInsure’s @openinsure/hipaa package implements privacy-by-design architecture: PHI is isolated, tagged, redacted, and audited at every layer of the stack.
HIPAA Applicability in Insurance
Section titled “HIPAA Applicability in Insurance”When HIPAA Applies
HIPAA applies to OpenInsure users in the following scenarios:
- Workers’ Compensation: Medical records, treatment notes, and disability determinations for injured workers.
- Health Captives: Any captive that insures employee health benefits (self-insured plans, stop-loss coverage with specific claimant data).
- FHIR Integration: If your platform connects to an EHR or health plan clearinghouse via the
@openinsure/fhirpackage, HIPAA applies to all FHIR resources received. - Stop-Loss Insurance: Aggregate stop-loss without individual claimant data is generally NOT subject to HIPAA. Specific stop-loss with individual run-out reports IS subject to HIPAA.
HIPAA does not apply to: pure commercial property/casualty lines (GL, Cyber, E&O) where no health information is involved.
Covered Entity vs. Business Associate
- Covered Entities (CEs): Health plans, healthcare clearinghouses, healthcare providers. A health captive administered on OpenInsure is typically a CE.
- Business Associates (BAs): Vendors who process PHI on behalf of a CE. OpenInsure (the platform) acts as a Business Associate.
- BAA: You must execute a Business Associate Agreement with OpenInsure (or your self-hosted operator) before processing PHI. See the BAA section below.
PHI Field Taxonomy
Section titled “PHI Field Taxonomy”The @openinsure/hipaa package maintains a registry of every database field that may contain PHI, mapped to HIPAA’s 18 identifiers:
| HIPAA Identifier | OpenInsure Fields |
|---|---|
| Names | claims.claimant_name, members.full_name, patients.name |
| Geographic data | claims.claimant_address, members.address |
| Dates (birth, admission, discharge) | members.date_of_birth, claims.treatment_start_date, claims.treatment_end_date |
| Phone numbers | claims.claimant_phone, members.phone |
| Email addresses | claims.claimant_email, members.email |
| Social Security Numbers | members.ssn, claims.claimant_ssn |
| Medical record numbers | claims.medical_record_number |
| Account numbers | members.health_plan_id |
| Certificate / license numbers | members.provider_npi |
| IP addresses | phi_audit_log.ip_address (not exposed in API responses) |
| Biometric identifiers | Not stored in current schema |
| Full-face photographs | Stored in R2 with PHI-tagged metadata |
| Diagnosis codes | claims.diagnosis_codes[] (ICD-10) |
| Procedure codes | claims.procedure_codes[] (CPT) |
| Treatment notes | claims.treatment_notes (free text) |
The full registry is in packages/hipaa/src/phi.ts. Adding a new PHI field requires a PR that updates this registry — enforced by a CI lint rule.
Storage Isolation Architecture
Section titled “Storage Isolation Architecture”PHI protection is enforced at the storage layer:
What Lives Where
Section titled “What Lives Where”| Data | Storage | PHI |
|---|---|---|
| Policy records | PlanetScale + CF D1 cache | No |
| Claim records (non-PHI fields) | PlanetScale + CF D1 cache | No |
| Claim claimant information | PlanetScale only | Yes |
| Medical records / treatment notes | PlanetScale only | Yes |
| Workers’ comp payroll data | PlanetScale only | Yes |
| FHIR resources | PlanetScale only (FHIR schema) | Yes |
| PHI audit log | PlanetScale only (append-only) | Meta |
| Session tokens | CF KV | No |
| Rate tables | CF D1 | No |
PHI fields are never written to CF D1 or CF KV. The Worker middleware that caches policy records to CF D1 automatically strips PHI-tagged fields before writing.
App-Level Tenant Isolation for PHI
Section titled “App-Level Tenant Isolation for PHI”PHI tables are protected by app-level tenant isolation (orgId scoping) that restricts access beyond the standard org_id filter. The API Worker enforces that queries to PHI tables are scoped to the authenticated user’s organization and, for claims data, further restricted to the assigned adjuster or supervisor.
Field-Level Encryption
Section titled “Field-Level Encryption”For the most sensitive PHI fields (SSN, DOB), OpenInsure uses field-level encryption at the application layer. The per-organization PHI encryption key is stored in Cloudflare Secrets and used by the Worker to encrypt/decrypt sensitive fields before writing to or reading from PlanetScale.
Audit Log Structure
Section titled “Audit Log Structure”Every read or write to a PHI-tagged field is logged to phi_audit_log. This table is append-only — no updates or deletes are permitted by any role.
CREATE TABLE phi_audit_log ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), org_id uuid NOT NULL, user_id uuid NOT NULL, role text NOT NULL, action text NOT NULL CHECK (action IN ('read', 'write', 'redact', 'export')), table_name text NOT NULL, record_id uuid NOT NULL, fields text[] NOT NULL, purpose text, -- HIPAA requires documenting the purpose ip_address inet, user_agent text, created_at timestamptz NOT NULL DEFAULT now());
-- Partition by month for performanceCREATE INDEX ON phi_audit_log (org_id, created_at);Audit logs are retained for 7 years per HIPAA requirements (45 CFR § 164.530(j)).
Querying the Audit Log
Section titled “Querying the Audit Log”GET /v1/compliance/:orgId/phi-audit?userId=usr_01J8...&after=2025-01-01
# Returns paginated audit entries for HIPAA compliance reviewRuntime Redaction Engine
Section titled “Runtime Redaction Engine”The redact() middleware in @openinsure/hipaa runs on every API response that may contain PHI:
export function redactPhi(data: unknown, userRole: string): unknown { if (!PHI_ACCESSIBLE_ROLES.has(userRole)) { return deepRedact(data, PHI_FIELDS, '[REDACTED]'); } // Log the access before returning auditLog.write({ action: 'read', fields: getPHIFields(data), ... }); return data;}Role-Based PHI Access Matrix
Section titled “Role-Based PHI Access Matrix”| Role | PHI Access | Scope |
|---|---|---|
producer | None — all PHI redacted | — |
adjuster | Full PHI | Claims assigned to them only |
claims_supervisor | Full PHI | All org claims |
underwriter | Aggregate stats only | Cohort-level, no individual PHI |
compliance_officer | Audit log only | No PHI values |
admin | Full PHI | Requires MFA + session purpose declaration |
actuarial | Anonymized data only | SSN/name replaced with pseudonymous IDs |
Enforcement Middleware
Section titled “Enforcement Middleware”The @openinsure/hipaa package includes Hono middleware that enforces PHI access logging and role-based redaction at the API layer. Both middleware functions run after the route handler, inspecting JSON responses before they reach the client.
PHI Access Logger
Section titled “PHI Access Logger”The phiAccessLogger middleware intercepts successful JSON responses, detects any PHI fields present in the response body, and writes an audit entry to a configurable store. Audit logging is fire-and-forget — failures never block the response.
import { phiAccessLogger } from '@openinsure/hipaa';import { DbAuditStore } from '@openinsure/hipaa';import { Hono } from 'hono';
const auditStore = new DbAuditStore({ insert: async (entry) => { await db.insert(phiAuditLog).values({ userId: entry.userId, userRole: entry.userRole, action: entry.action, entityType: entry.entityType, entityId: entry.entityId, fields: entry.phiAccessed, ipAddress: entry.ipAddress, userAgent: entry.userAgent, createdAt: entry.timestamp, }); },});
const app = new Hono();app.use('/v1/claims/*', phiAccessLogger({ store: auditStore }));app.use('/v1/members/*', phiAccessLogger({ store: auditStore, entityType: 'member' }));The middleware reads the authenticated user from c.get('user') (expecting { sub, role }) and extracts the client IP from the CF-Connecting-IP or X-Forwarded-For header. The audit action (read, write, delete, export) is inferred from the HTTP method and path.
In a Cloudflare Workers environment, the audit write is passed to executionCtx.waitUntil() so it survives beyond the response lifecycle.
PHI Redaction
Section titled “PHI Redaction”The phiRedaction middleware automatically applies minimum-necessary redaction to JSON responses based on the authenticated user’s role. Fields that are PHI but not permitted for the role are replaced with '[REDACTED]'.
import { phiRedaction } from '@openinsure/hipaa';
app.use('/v1/members/*', phiRedaction());app.use('/v1/claims/*', phiRedaction());The middleware uses the MINIMUM_NECESSARY mapping defined in the package to determine which PHI fields each role is allowed to see:
| Role | Allowed PHI Fields |
|---|---|
admin / org_admin / system / clinical_admin | All PHI fields |
underwriter | firstName, lastName, dob, dateOfBirth, ein, address |
producer | firstName, lastName, email, phone |
adjuster | firstName, lastName, treatmentDate |
auditor | firstName, lastName |
member | No PHI fields (fully redacted) |
If the user’s role grants access to all PHI fields, the middleware skips redaction entirely for performance. If no role is found on the context, the middleware defaults to the member role (most restrictive).
The middleware handles both flat JSON objects and { data: ... } wrapper patterns common in API responses.
Combining Both Middleware
Section titled “Combining Both Middleware”For production routes that serve PHI, apply both middleware together — redaction first (so only permitted fields are visible), then audit logging (so the log reflects what the user actually received):
import { phiAccessLogger, phiRedaction } from '@openinsure/hipaa';import { DbAuditStore } from '@openinsure/hipaa';import { Hono } from 'hono';
const app = new Hono();
// Redaction runs first, then audit logging records what was servedapp.use('/v1/claims/*', phiRedaction());app.use('/v1/claims/*', phiAccessLogger({ store: auditStore }));
app.use('/v1/members/*', phiRedaction());app.use('/v1/members/*', phiAccessLogger({ store: auditStore, entityType: 'member' }));Implementing PhiAuditStore for Production
Section titled “Implementing PhiAuditStore for Production”The PhiAuditStore interface requires a single method:
interface PhiAuditStore { write(entry: AuditEntry): Promise<void>;}The package provides two implementations:
InMemoryAuditStore— stores entries in an array. Suitable for tests and development only; entries are lost on process exit.DbAuditStore— delegates to aninsertfunction you provide, allowing integration with any database layer (Drizzle + PlanetScale, Prisma, raw SQL, etc.).
For production deployments, use DbAuditStore backed by the append-only phi_audit_log table described in the Audit Log Structure section above.
BAA (Business Associate Agreement) Obligations
Section titled “BAA (Business Associate Agreement) Obligations”Before processing PHI on OpenInsure, you must execute a Business Associate Agreement:
- OpenInsure Cloud: The BAA is included in the Enterprise subscription agreement. Contact
legal@openinsure.devto execute. - Self-Hosted: If you deploy OpenInsure on your own infrastructure, you are the BA processing PHI. You must execute BAAs with your database provider (PlanetScale), Cloudflare, and any other subprocessors.
Subprocessor BAAs for Self-Hosted Deployments
Section titled “Subprocessor BAAs for Self-Hosted Deployments”| Vendor | BAA Available | Notes |
|---|---|---|
| PlanetScale | Yes | Confirm terms for your selected plan/provider |
| Cloudflare | Yes | Available via Cloudflare Data Processing Addendum |
| Stripe | Yes | Stripe does not process PHI — BAA not required for billing |
Minimum Necessary Standard
Section titled “Minimum Necessary Standard”HIPAA’s Minimum Necessary rule requires that PHI is disclosed only to the extent necessary to accomplish the intended purpose. OpenInsure enforces this by:
- Field-level scope — API responses only include PHI fields that are necessary for the operation being performed. A
GET /v1/claims/:idby a producer returns no claimant PHI, only coverage verification. - Role scoping — Adjusters see only claims assigned to them.
- Export controls — Bulk PHI exports require admin approval and a documented business purpose, which is logged to the audit trail.