Skip to content

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.

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/fhir package, 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.

The @openinsure/hipaa package maintains a registry of every database field that may contain PHI, mapped to HIPAA’s 18 identifiers:

HIPAA IdentifierOpenInsure Fields
Namesclaims.claimant_name, members.full_name, patients.name
Geographic dataclaims.claimant_address, members.address
Dates (birth, admission, discharge)members.date_of_birth, claims.treatment_start_date, claims.treatment_end_date
Phone numbersclaims.claimant_phone, members.phone
Email addressesclaims.claimant_email, members.email
Social Security Numbersmembers.ssn, claims.claimant_ssn
Medical record numbersclaims.medical_record_number
Account numbersmembers.health_plan_id
Certificate / license numbersmembers.provider_npi
IP addressesphi_audit_log.ip_address (not exposed in API responses)
Biometric identifiersNot stored in current schema
Full-face photographsStored in R2 with PHI-tagged metadata
Diagnosis codesclaims.diagnosis_codes[] (ICD-10)
Procedure codesclaims.procedure_codes[] (CPT)
Treatment notesclaims.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.

PHI protection is enforced at the storage layer:

DataStoragePHI
Policy recordsPlanetScale + CF D1 cacheNo
Claim records (non-PHI fields)PlanetScale + CF D1 cacheNo
Claim claimant informationPlanetScale onlyYes
Medical records / treatment notesPlanetScale onlyYes
Workers’ comp payroll dataPlanetScale onlyYes
FHIR resourcesPlanetScale only (FHIR schema)Yes
PHI audit logPlanetScale only (append-only)Meta
Session tokensCF KVNo
Rate tablesCF D1No

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.

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.

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.

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 performance
CREATE INDEX ON phi_audit_log (org_id, created_at);

Audit logs are retained for 7 years per HIPAA requirements (45 CFR § 164.530(j)).

Terminal window
GET /v1/compliance/:orgId/phi-audit?userId=usr_01J8...&after=2025-01-01
# Returns paginated audit entries for HIPAA compliance review

The redact() middleware in @openinsure/hipaa runs on every API response that may contain PHI:

packages/hipaa/src/phi.ts
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;
}
RolePHI AccessScope
producerNone — all PHI redacted
adjusterFull PHIClaims assigned to them only
claims_supervisorFull PHIAll org claims
underwriterAggregate stats onlyCohort-level, no individual PHI
compliance_officerAudit log onlyNo PHI values
adminFull PHIRequires MFA + session purpose declaration
actuarialAnonymized data onlySSN/name replaced with pseudonymous IDs

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.

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.

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:

RoleAllowed PHI Fields
admin / org_admin / system / clinical_adminAll PHI fields
underwriterfirstName, lastName, dob, dateOfBirth, ein, address
producerfirstName, lastName, email, phone
adjusterfirstName, lastName, treatmentDate
auditorfirstName, lastName
memberNo 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.

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 served
app.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' }));

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 an insert function 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.dev to 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”
VendorBAA AvailableNotes
PlanetScaleYesConfirm terms for your selected plan/provider
CloudflareYesAvailable via Cloudflare Data Processing Addendum
StripeYesStripe does not process PHI — BAA not required for billing

HIPAA’s Minimum Necessary rule requires that PHI is disclosed only to the extent necessary to accomplish the intended purpose. OpenInsure enforces this by:

  1. Field-level scope — API responses only include PHI fields that are necessary for the operation being performed. A GET /v1/claims/:id by a producer returns no claimant PHI, only coverage verification.
  2. Role scoping — Adjusters see only claims assigned to them.
  3. Export controls — Bulk PHI exports require admin approval and a documented business purpose, which is logged to the audit trail.