Skip to content
Gasperi

A HIPAA CRM, and 126,000 records that had to survive the move.

Client

Manors Medicaid

Year

2026

Role

Lead Engineer

Stack

  • Next.js 16
  • React 19
  • Supabase
  • PostgreSQL
  • TanStack Query
  • Zod
  • Tailwind v4

Manors Medicaid's staff file Medicaid, SNAP, TCA, ACA, and Medicare applications on behalf of clients across South Florida. Every field is protected health information. I built the CRM they run on and moved twelve years of legacy records into it without losing one.

126k

records migrated

0

migration errors

3 months

legacy CRM to production

01

PHI is the constraint

Every architectural decision in this system is downstream of one fact: every row is protected health information. That isn't a compliance checkbox bolted on at the end — it decides the auth model, the schema, and what's allowed to exist in memory.

TOTP multi-factor is enforced, not offered. Row-level security is the access model rather than application-layer checks, so a query bug can't leak another office's clients. Social security numbers are encrypted at rest and pass through a reveal-and-hide state machine where the plaintext lives only in local state and clears the moment it's hidden — never cached, never logged.

That last piece existed twice before I consolidated it, once for the client profile and once for household members, with two different masking rules. Two implementations of the most sensitive interaction in the app is how a leak eventually happens. It's now one module with injected ports, so both callers share the behavior and differ only in what they persist.

02

Moving 126,000 records without losing one

The legacy system was Vtiger, with twelve years of accumulated client history and 66,000 file attachments sitting on a disk beside the database. A CRM migration is easy to do approximately and very hard to do exactly, and 'approximately' isn't available when the records are someone's Medicaid application.

I wrote the migration as an idempotent script over TSV exports — 16,864 clients, 38,167 notes, 66,347 attachments, plus insurance and ACA records — each row keyed on its legacy identifier so re-running it converged instead of duplicating. Attachments were re-uploaded to object storage and re-linked, with every failure written to an error log rather than swallowed.

The error log came back empty. 126,000 records, zero failures, and staff logged into a system on day one that had everything the old one did.

03

One table for meaning

A CRM is mostly status. Case status, pipeline status, invoice status, task priority, staff role — and the moment those live on more than one screen, the same status starts rendering two different colors and the interface quietly stops being trustworthy.

So color isn't a decision components get to make. A status maps to a semantic tone — success, warning, danger, pending — through exactly one table, and the badge primitive maps tone to theme tokens. Raw hex exists in one CSS block and nowhere else. The same status cannot render two colors on two pages, because there is one mapping, not one per surface.

Program identity is deliberately kept separate: Medicaid, SNAP, TCA, and ACA each carry a brand hue that isn't a status meaning and doesn't go through the tone table. Conflating the two is what makes these systems drift.

Next project

Calendus

An appointment platform, built end to end and shipped.