A modern insurance-claim-check / mortgage loss-draft disbursement portal — built as an opinionated reaction to how brittle and dead-end the real ones are.
The headline feature: a homeowner can't get trapped by a typo. If they enter a check with the wrong cents, the portal won't let them silently re-submit a duplicate, and gives them a real correction path with a full audit trail.
Codespaces and StackBlitz run the seeded SQLite demo with no extra config — they pick up
.devcontainer/devcontainer.jsonandstackblitz.jsonrespectively.
| Persona | Path | What you can do |
|---|---|---|
| Homeowner | /dashboard |
See claim status, funds release stages, documents, activity, and submitted checks |
| Homeowner | /dashboard/checks/new |
Submit a new check with currency-safe input + review screen |
| Homeowner | /dashboard/checks/[id]/correct |
File a correction request on an existing check |
| Servicer admin | /admin |
Review pending corrections, see flagged duplicate attempts, release funds |
| Compliance | /audit |
Append-only audit feed of every create, update, correction, and release |
The seed script pre-loads:
- One homeowner (Alex Rivera) and one servicer admin (Jordan Park)
- One mortgage loan (
LN-44820137) on a Texas property - One hailstorm claim (
HC-2026-00913) with three release stages - One submitted check
#123456for $28,000.82 — the typo, 82¢ instead of 02¢ - One blocked duplicate-submission attempt at $28,000.02
- One pending correction request sitting in the admin queue
- Open
/dashboard/checks/new. - Use check number
123456and any amount, payees, etc. - Click through the review screen and submit.
- The duplicate-block modal appears with View existing / File correction.
- Hop to
/adminand approve the seeded correction. Watch the check amount update. - Open
/auditto see the paired audit entries with the old/new diff.
git clone https://github.com/Beanstown/ClaimFlow
cd ClaimFlow
npm install
cp .env.example .env
npm run setup # prisma db push + seed demo data
npm run dev # localhost:3000Or one-shot via the helper script:
bash scripts/init.sh- Next.js 16 App Router + TypeScript
- Tailwind CSS with shadcn-style primitives (no UI vendor lock-in — owned in
src/components/ui) - Prisma ORM + SQLite for self-contained demo
- Server actions for mutations, no separate API layer
- Zod + React Hook Form for validation
- Sonner for toasts, Radix UI for accessible dialogs/tabs
To regenerate: bash scripts/screenshots.sh (requires Google Chrome; expects dev server
on port 3037 — pass URL_BASE=http://localhost:3000 to override).
The data model treats a check number as unique within the (loan, claim) tuple:
model Check {
// ...
loanNumber String
claimNumber String
checkNumber String
@@unique([loanNumber, claimNumber, checkNumber])
}loanNumber and claimNumber are denormalized onto the Check row specifically so
the unique constraint is enforced at the DB level — not just by app code. SQLite, Postgres,
and MySQL all back this with a real UNIQUE INDEX.
The submit flow (src/app/actions/checks.ts) does not rely
on catching P2002 after-the-fact. It looks up the conflict first and surfaces a
useful payload:
const existing = await prisma.check.findUnique({
where: { loanNumber_claimNumber_checkNumber: { ... } },
});
if (existing) {
// Log the attempt for the admin queue.
await prisma.duplicateAttempt.create({ ... });
await prisma.auditLog.create({ ... });
return { ok: false, kind: "duplicate", existingCheck, attemptedAmountCents };
}The client form (new-check-form.tsx) treats the duplicate response as a non-error
state and opens the DuplicateBlockModal with
two clear next actions: view the existing check or file a correction with the new
amount pre-filled. No dead-end error toast.
This is why loanNumber and claimNumber are duplicated on the Check row — Prisma's
composite unique constraint must reference scalar fields on the same model. We trade
a tiny bit of denormalization for a guarantee the database itself enforces.
- Homeowner files a correction via
/dashboard/checks/[id]/correct. The check itself is not mutated — aCorrectionRequestrow is created withstatus: "pending", plus anAuditLogentry recordingoldValueandnewValue. - Stacking is blocked. Only one pending correction per check at a time (server-side
check in
fileCorrection). - Admin reviews at
/admin. Approve or reject with an optional reviewer note. - Approval runs in a single Prisma transaction:
- update the check's
amountCents - flip the correction to
approved - write a
check.updateaudit entry with old/new values - write a
correction.approveaudit entry
- update the check's
- Rejection writes a
correction.rejectentry and leaves the check untouched. The homeowner can then file a new correction with more detail.
Every state-changing action goes through the same AuditLog table. Each entry captures:
actorId+actorEmail— who did itaction—check.create,check.duplicate_blocked,check.update,correction.request,correction.approve,correction.reject,funds.releaseentityType+entityId— what was acted onfieldName,oldValue,newValue— the diff (when applicable)reason— free text for context (correction reason, reviewer note, etc.)
The log is rendered at /audit with old → new diffs and entity references. It's
append-only by convention; there's no UI surface to edit or delete entries.
All currency values are stored as integer cents (Int in the schema). The form
input (CurrencyInput) clamps user input to two
decimal places, formats on blur, and emits cents to the form state. This is the same
reason the typo bug exists in the real world — bad currency UX combined with float
math — and it's intentional that ClaimFlow treats it as a first-class concern.
The demo runs on SQLite, which is ideal for local dev and Codespaces but not for a shared production deploy. To put this in front of users:
- Vercel + Turso (LibSQL) — cheapest path. Swap the Prisma datasource to LibSQL
and Turso's free tier handles persistence. Requires
@libsql/clientadapter. - Vercel + Postgres — change
provider = "postgresql"and pointDATABASE_URLat Vercel Postgres / Neon / Supabase. - Fly.io / Railway — keep SQLite, mount a persistent volume for
prisma/dev.db.
The code itself is provider-agnostic; only prisma/schema.prisma and DATABASE_URL
change.
prisma/
schema.prisma # data model + unique constraints
seed.ts # the typo-and-duplicate demo scenario
src/
app/
page.tsx # landing
dashboard/ # homeowner views
admin/ # servicer admin queue
audit/ # compliance audit log
actions/ # server actions: checks, corrections, funds
components/
ui/ # shadcn-style primitives, owned in-repo
site-header.tsx
duplicate-block-modal.tsx
currency-input.tsx
check-status-pill.tsx
lib/
db.ts # Prisma singleton
validation.ts # Zod schemas
utils.ts # formatCurrency, parseCurrencyToCents, etc.
session.ts # demo "session" (no auth in prototype)
docs/screenshots/ # README assets
scripts/
init.sh # one-shot setup
screenshots.sh # regenerate README screenshots
.devcontainer/ # GitHub Codespaces config
stackblitz.json # StackBlitz config
Prototype. Original work — not affiliated with any real loss-draft platform or insurance
brand. Auth is stubbed out (a real deploy would drop in NextAuth or Clerk and gate
/admin behind a servicer role).
MIT. See LICENSE.




