Schema-enforced anonymization: what we changed in the Kalivar rebuild
The Kalivar team
6/7/2026
The original Kalivar was a Laravel application. It worked. Cases moved through it, physicians wrote opinions on it, and the platform's central contract — that no expert ever sees the attorney who published a case, and no attorney ever sees an expert's identity until they pay for an introduction — held. It held through good practice: anonymization checks in controllers, mail templates that scrubbed names, a media table that wasn't queryable from any UI an expert could reach.
It held, but it held by convention. Nothing in the data model knew the contract existed.
The rebuild we shipped yesterday starts from a different premise: the schema has to enforce the contract, because conventions decay every time someone adds a feature.
What "schema-enforced" means in practice
Four changes followed from that decision. Each one moved a guarantee from the application layer to a place it can't be quietly undone.
1. Anonymous expert handles, generated when archetype is set
Every physician on the platform now has a User.anonymousUsername field: a stable pseudonym shown next to their opinions instead of their name. Generation happens once, on the same transaction that writes the user's archetype. The handle is themed by archetype, drawn from role-specific keyword pools in packages/marketplace/src/anonymous/keywords.ts: Astute-Aristion-4123 for a physician, Resolute-Iustinor-8421 for an attorney, Sapphire-Falcon-2034 for an administrator.
The mechanism is what matters. Previous platforms in this space tend to either (a) generate handles client-side at the moment a case is rendered, which means the same physician shows up under different handles across cases, or (b) leave the slot blank, which forces attorneys to pay for an introduction before they can recognize prior work from a familiar expert. Both fail the same way: they make the relationship between identity and reputation accidental.
Our generator is invoked from a single helper, setUserArchetype, that every archetype-writing path must call. The auth layer strips anonymousUsername and archetype from any client-driven update, so the only way an archetype lands in the database is through the helper that also produces the themed handle. There is no "set archetype but skip handle" code path.
2. PHI detection at submission, not at publication
The original platform's anonymization control was at the user-interface level: a checkbox an attorney clicked to confirm the narrative had been anonymized. The new platform runs a detection pass on every case narrative as it's submitted. Patterns flagged include patient name candidates, MRN-shaped numeric sequences, hospital identifiers, and faces in imaging uploads. If anything is flagged, the attorney gets a revision prompt before the case is published.
The detection is not a replacement for the attorney's obligation under Terms §8. It's a safety net under it. The reason for moving detection from publication to submission is asymmetric cost: a leak after publication is high-cost and unrecoverable, a false-positive at submission is a 30-second revision. The two are not comparable, so the cheaper failure mode should absorb the volume.
3. The 140-row NUCC sub-specialty list, replacing an internal taxonomy
Specialty matching is what determines which experts see which cases. The original platform used an internal sub-specialty list that grew by request: when a physician onboarded, if their sub-specialty wasn't in the list, support added it. Years of that produced a list with duplicates ("Pediatric Cardiology" and "Pediatric Cardio"), gaps (no entry for several established subspecialties), and no relationship to the taxonomies used elsewhere in healthcare.
The rebuild uses the 140-row sub-specialty list derived from the National Uniform Claim Committee (NUCC) provider taxonomy code set. It's the same list hospital credentialing systems and federal programs work from. The legacy importer fuzzy-matches each legacy sub-specialty against the NUCC list; anything that doesn't map cleanly is logged and skipped, and the physician picks the correct row on first sign-in.
This is the single most consequential change for the matching algorithm. Cases now get matched against the same vocabulary the experts are credentialed in. Borderline matches fall, and the matches that do come through are tighter.
4. FREE_LINK on follow-on introductions, retroactively
Once an attorney has paid for an introduction with a physician, every subsequent case-link with that physician costs nothing. The rule is enforced at the introduction-request layer: if an (attorneyUserId, physicianUserId) pair has any prior Introduction with status = COMPLETED, the new case-scoped request is created with kind = FREE_LINK and introductionFee = 0, skipping the payment preflight entirely.
The change applies retroactively. Every introduction that was ever paid for on the original Kalivar now unlocks free follow-ons for the attorney who paid. The mechanism the rebuild gives us is precise: a single findFirst on the Introduction table, filtered on the pair plus status = COMPLETED. The decision to apply it retroactively is a values decision, not a technical one. The introduction fee is a fee for trust formation. Once trust exists, charging for the connection is a tax on the relationship the platform was supposed to make possible.
What this rebuild does not change
It does not make legal-medical expert exchange easy. It does not eliminate the judgment a physician applies when reading a case, or the judgment an attorney applies when deciding to publish one. It does not relieve the attorney of the obligation to anonymize before submission. It does not migrate passwords; the new platform uses a stronger hashing scheme than the Laravel default, and every existing user resets their password once with a unique link sent to the email on file.
Where to look in the codebase
For readers who care: the anonymous handle generator and its role-specific keyword pools live in packages/marketplace/src/anonymous/. The setUserArchetype helper is at packages/auth/lib/set-user-archetype.ts. The legacy importer that re-creates handles for migrated users is at packages/database/scripts/migrate-legacy/01-import-users.ts. The platform is built on Next.js, Prisma, and Better Auth on top of the supastarter base.
— The Kalivar team