← All posts

Build walkthrough · Regulated Industries

Building HIPAA-Grade Pharmacy Routing With a 7-Year Audit Log

Inside the build: failover-safe prescription routing and an immutable seven-year audit trail, verified epic by epic.

Asaasin EngineeringPublished August 24, 20268 min read

In short

A missed prescription-routing failover in a pharmacy platform is not a bug ticket. It is a compliance liability, the kind that shows up in an audit finding, not a sprint retro. We built product-level pharmacy routing, dual prescriber paths, and a seven-year immutable audit log for a compounding-pharmacy network, and verified every phase against a numbered spec before it shipped.

Key numbers

  • 11 epics shipped behind a spec-first verification gate
  • 490+ unit tests, strict TypeScript typecheck green
  • 7-year immutable audit log covering every access and change
  • Patient-facing screens passing WCAG 2.1 AA
  • Stack: Next.js 15, React 19, TypeScript strict, Prisma/Postgres, jose + bcrypt, Vitest

The problem with treating compliance as a defect queue

Most SaaS teams patch compliance gaps the way they patch anything else: find it, ticket it, fix it next sprint. That model works for a broken button. It does not work for a pharmacy platform.

A compounding-pharmacy network came to us running a white-label platform across clinic, patient, and platform-admin surfaces. The system routes real prescriptions to real pharmacies. If a routing failover silently drops a script, or if the audit log has a gap when a regulator asks who touched a record and when, that is not a bug to patch later. It is a liability the moment it happens. The build had to treat every phase as something that either meets a written requirement or does not ship, because "we'll harden it in v2" is not a sentence a compliance officer accepts.

That framing set the entire build process: spec-first, with a verification gate that does not trust the code that wrote the code.

The architecture: three surfaces, one routing engine, one audit spine

The platform has three distinct user surfaces on a single design system: clinic staff managing patients and prescriptions, patients handling consent and status, and platform admins overseeing the whole network. All three sit on top of one product-level pharmacy-routing engine, which is where the actual compliance weight lives.

The routing engine owns four responsibilities that cannot fail quietly:

  1. Failover logic. If a primary pharmacy cannot fill or process a routed prescription, the system has to detect that and reroute, not stall silently.
  2. Dual prescriber paths. Prescriptions can originate from more than one type of prescriber relationship, and each path has its own validation and audit requirements.
  3. Consent and e-sign. Every patient action that matters legally gets a signed, timestamped consent record, not an implied checkbox.
  4. KYC. Identity verification runs before a prescription enters the routing pipeline, not after.

Every one of those four surfaces writes to the same place: an append-only, seven-year retention audit log that records who accessed what, what changed, and when, across all three user surfaces.

Compounding-pharmacy platform: routing and audit spine Clinic surface staff, chart, orders Patient surface consent, e-sign, status Platform-admin surface network oversight Pharmacy-routing engine failover logic, dual prescriber paths, consent/e-sign, KYC checks Primary pharmacy fill or fail Failover pharmacy rerouted on failure Prescriber path dual validation 7-year immutable audit log (append-only) every access, every change, across all three surfaces

Every arrow in that diagram is a write path into the audit spine, not just the routing engine. Access to a patient record from the clinic surface writes an entry. A consent sign-off from the patient surface writes an entry. A configuration change from platform-admin writes an entry. Nothing reads or mutates protected data without leaving a row behind.

What "append-only" means in the schema, not just in the pitch

An audit log that can be edited is not an audit log, it is a log-shaped table someone can quietly clean up. The schema pattern enforces immutability at the data layer, not just in application logic, so a compromised service account still cannot rewrite history.

// prisma/schema.prisma (representative pattern, not verbatim)

model AuditLogEntry {
 id String @id @default(cuid())
 actorId String
 actorRole Role
 action AuditAction
 resourceType String
 resourceId String
 beforeState Json?
 afterState Json?
 surface Surface // CLINIC | PATIENT | PLATFORM_ADMIN
 ipAddress String
 createdAt DateTime @default(now())

 // No updatedAt. No delete cascade. No soft-delete flag.
 // Retention is enforced by policy (7 years), never by app-level mutation.

 @@index([resourceType, resourceId])
 @@index([actorId, createdAt])
}

enum AuditAction {
 VIEW
 CREATE
 UPDATE
 ROUTE_PRESCRIPTION
 FAILOVER_TRIGGERED
 CONSENT_SIGNED
 KYC_VERIFIED
}

Notice what is missing: no updatedAt, no delete path, no soft-delete boolean that a future migration could quietly flip. Every write to a protected resource, including a plain read, produces a new row. The pattern also constrains the database grants the application runs under, so append and read are the only operations available to it in the first place. Retention is a policy enforced at the infrastructure and access-control layer, not a field an engineer could edit under deadline pressure.

Session verification runs on the same discipline. Every authenticated request checks a signed token before it ever reaches a route handler that touches patient data.

// Simplified illustration of a jose-based session-verification middleware.
// Pattern only, not the client's implementation.

import { jwtVerify } from "jose";
import type { NextRequest } from "next/server";

const secret = new TextEncoder().encode(process.env.SESSION_SECRET);

export async function verifySession(req: NextRequest) {
 const token = req.cookies.get("session")?.value;
 if (!token) {
 return { authenticated: false as const };
 }

 try {
 const { payload } = await jwtVerify(token, secret, {
 algorithms: ["HS256"],
 });

 return {
 authenticated: true as const,
 userId: payload.sub as string,
 role: payload.role as string,
 surface: payload.surface as string,
 };
 } catch {
 // Expired, tampered, or malformed token: treat as unauthenticated.
 // Never fall through to a default-allow path.
 return { authenticated: false as const };
 }
}

The failure mode matters here as much as the success path. A malformed or expired token returns "not authenticated," full stop. There is no default-allow branch, no fallback role, nothing that treats an unverifiable session as a lower-privilege guest. In a regulated system, the safe default is always "no access," never "reduced access."

Spec-first, adversarial verifier: what actually happened before merge

The phrase "spec-first" gets used loosely. Here it meant something specific: every phase of the build was written against a numbered requirements document, and nothing merged until a separate, automated adversarial verifier checked the shipped code against those numbered requirements and tried to find gaps.

That is a different gate than code review. A code reviewer asks "does this look right and does it follow our patterns." An adversarial verifier asks a narrower, harder question: "does this satisfy requirement 4.2, and can I construct an input or a state transition that breaks it." It is looking for the edge case the implementation missed, not the style violation.

The client's numbered requirements are theirs, so the specifics stay with them. The shape of what a verifier gate asks of a routing engine looks like this:

  • Does every failover path actually trigger on the documented failure conditions, or only the ones covered by the happy-path test.
  • Does the audit log capture the access event even when the underlying request errors out partway through.
  • Does the dual prescriber path reject a malformed submission from either path, not just the more common one.
  • Does a consent record actually block downstream routing when consent has not been signed, or does the block only apply in the UI.

Each of those is a failure mode a code reviewer skimming a pull request could plausibly miss, especially under deadline pressure. The verifier does not get tired and does not skim. It runs the same numbered checklist against every phase, every time, and a phase that fails a checklist item does not merge until it passes.

This is the same discipline we apply across any AI-assisted build: a pull request in the client's own repository, reviewed by the named engineer who owns it, typed contracts, tests in CI. The adversarial verifier is an added layer specific to a regulated build where "looks right" is not the bar. Our security posture and process are built around that same idea: nothing ships on trust that can instead ship on a check.

What eleven epics behind a spec gate actually produced

The build shipped in eleven separate epics, each one gated the same way: numbered spec, implementation, adversarial verification, merge. That discipline produced measurable, checkable outcomes rather than a claim in a sales deck.

OutcomeWhat it means in practice
490+ unit testsRouting logic, failover triggers, and audit-write paths are exercised by test, not by hope
Strict TypeScript typecheck, greenNo any escape hatches hiding a routing or consent bug at runtime
WCAG 2.1 AA, patient-facing screensPatients using assistive technology can complete consent and status flows
7-year immutable audit log, liveEvery access and change across all three surfaces is recorded, not sampled

None of these numbers describe intent. They describe what merged. A 490+ count of unit tests is a count of assertions that ran and passed in CI on this codebase, not a target the team was aiming for. Strict typecheck green means the compiler enforced type safety across the whole codebase, not just the files someone remembered to annotate.

That is the difference between a compliance posture that is promised in a deck and one that is proven in code. A deck can claim anything. A test suite either passes or it does not, and a strict TypeScript build either compiles clean or it throws.

Where "HIPAA-compliant" claims usually go wrong

We are careful about this phrase because most vendors are not. There is no certification called "HIPAA certified." HIPAA does not issue one, and any vendor claiming it is either confused or overselling. The honest, checkable claims are: we sign a Business Associate Agreement, and we operate controls aligned with the HIPAA Security Rule and Privacy Rule, backed by evidence like an immutable audit log, access controls tied to role and surface, and a SOC 2 Type II report available under NDA.

For a compounding-pharmacy network specifically, the audit log is the artifact a regulator actually wants to see: not a policy document describing intent, but a queryable record of who accessed a given prescription, when, and what changed. Seven years of retention matches the kind of recordkeeping period a pharmacy network needs to defend against an audit years after the fact, not just at go-live.

If your team is evaluating vendors on this exact question, our longer breakdown of what HIPAA-compliant software actually requires covers the Security Rule requirements in more depth than fits in a single build walkthrough. For dental practices weighing similar infrastructure decisions, dental IT services covers the adjacent ground: patient data handling, imaging pipelines, and practice-management integration under the same regulatory pressure.

A build like this does not tolerate a slow ramp. The failover logic, the audit schema, and the verifier gate all had to exist together from early in the project, because none of them are safely retrofitted onto a system already handling live prescriptions. That is the case for staff augmentation over a standard hiring cycle in a regulated build: a matched pod with a pod lead and an engineer bench starts within days, not the 3-6 months a typical senior hire takes to source, interview, and onboard.

The engineering discipline itself, spec-first development with an adversarial verification gate, is not something we reserve for pharmacy platforms. It is closer to the standard for any regulated or data-heavy build we take on, because the cost of a missed requirement in those domains is not a bug report, it is an audit finding.

The short version

A pharmacy-routing platform cannot treat a failover gap or an audit-log hole as a bug to fix later, because the liability lands the moment it happens, not when someone notices. We built this one spec-first: eleven epics, each verified against numbered requirements by an adversarial verifier before merge, on Next.js 15, React 19, strict TypeScript, Prisma/Postgres, and Vitest. The result is checkable, not promised: 490+ unit tests, a clean strict typecheck, WCAG 2.1 AA patient screens, and a seven-year immutable audit log recording every access and change across clinic, patient, and platform-admin surfaces.

Frequently asked questions

What does "HIPAA-aligned controls" mean if there is no HIPAA certification to hold?
HIPAA does not issue a certification, so any vendor claiming to be "HIPAA certified" is describing something that does not exist. The honest claim, and the one we make, is that we sign a Business Associate Agreement on request and operate controls aligned with the HIPAA Security and Privacy Rules: access controls, audit logging, encryption, and deployment inside the client's own cloud account or VPC.
How is an adversarial verifier different from a normal code review?
A code reviewer checks whether a pull request looks correct and follows team conventions. An adversarial verifier is a separate, automated check that tries to break the implementation against a numbered spec: constructing edge cases, failure states, and malformed inputs the happy-path tests might not cover, and blocking merge until those cases pass.
Why does the audit log need to be append-only instead of just access-controlled?
Access control limits who can read or write a table. Append-only removes the write paths that could alter history in the first place, so even a compromised credential with write access cannot edit or delete a past entry. For a system that has to defend its record years later in an audit, that distinction is the difference between a log and a log that can be trusted.
How many unit tests and epics did this build actually ship?
Eleven epics shipped behind the spec-first verification gate, backed by 490+ unit tests, a strict TypeScript typecheck that passes clean, and patient-facing screens verified against WCAG 2.1 AA. Those are counts of what merged and passed in CI, not targets the team described as aspirational.
Does a pod like this replace an in-house compliance or security hire?
No. A pod builds the system and the controls inside it, but a compliance officer role, ongoing risk assessments, and organizational policy are separate functions we do not replace. Our [security](/security) page details exactly what we sign and provide (BAAs on request, a SOC 2 Type II report under NDA) versus what remains the client's responsibility.

Sources

Get in touch.

Thirty minutes to map your problem to a plan and a timeline. You will leave the call with scope, price, and a start date.

What happens on the call
01You describe the outcome you need.
02We map it to scope, price, and a start date.
03You decide whether to proceed to a free prototype.
Schedule a 30-minute call