- Role
- Product, design and engineering
- Platform
- Web app · Next.js 16, LangGraph
- Status
- Working prototype
- Year
- 2026 · Independent project
The problem
Customer lists never match the CRM.
They arrive as spreadsheets: E-mail in one, Kontakt in the next, countries spelled three ways, websites without https://, the same company twice, and now and then a cell that Excel would run as a formula.
Every one of these has a right answer, and most of them do not need a model to find it.
- Header “Kontakt”Email fieldMapped by the AI, approved by you
- UK · GBRUnited KingdomApplied automatically
- DeutschlandGermanyApplied automatically
- N/A(empty)Applied automatically
- acme.comhttps://acme.comSuggested, you accept
- sales(at)northwind-packaging.co.uknot a valid email addressFlagged for you
- GB123456789, twicepossible duplicate, never mergedFlagged for you
The flow
Four steps, and it works on a phone.
- 01 · Import
CSV or XLSX, parsed in the browser. Duplicate and empty headers are fixed and reported. Nothing is uploaded.
- 02 · Map
Each column gets a suggested field, a confidence bucket and a one-line reason. Accept, change or ignore.
- 03 · Validate
Safe changes are applied and marked; meaning-changing ones are one-click fixes. Edit cells, exclude rows, review duplicates.
- 04 · Export
CSV in schema order, formula-injection safe, plus a JSON audit report of every decision and change.
AI suggests, a person approves, deterministic code executes. The model has exactly one job: propose how columns map to fields. Everything after the approval is plain, tested TypeScript.
What the model sees
A column profile, never the rows.
The file is parsed in the browser and stays there. The server gets each header plus statistics, such as how many values look like email addresses or countries. Up to three sample values per column are sent only if you switch sharing on.
The privacy claim is simple to check: open the network tab, and there is no file in it.
- 1Samples are opt-in: at most 3 values per column, 60 characters each.
- 2Confidence is shown as High, Medium or Low, labelled as a model estimate.
- 3Every suggestion comes with a one-line reason and a preview of the values.
Map · in demo mode the suggestions come from heuristics, and the screen says so.
Architecture
A real graph: retries, a fallback and a person in the loop.
Model output is parsed with Zod and repaired by domain rules. Garbage gets one retry with the validation errors; after that, or if the provider fails, heuristics stand in and the interface says so. The graph then stops and waits for a person, and an approved mapping that is still missing a required field goes back for another review.
Decision · no database
Serverless, and honest about it.
It runs on Vercel without a database. Graph threads live in a bounded in-memory saver, and when a review lands on a cold instance the route rebuilds the thread from what the browser echoes back, then re-validates it with the same rules. Production would use a shared Postgres checkpointer, with persistence and recovery verified across instances.
306
Tests
Unit, integration and component, from header parsing to the LangGraph retry paths.
5
End-to-end flows
Playwright on a production build, with axe accessibility checks on every step.
0
Bytes of the file uploaded
Parsing, validation and export all run in the browser.
Data quality rules
Every change has a from and a to.
The source file is never modified. Corrections and fixes are overlays you can revert, missing data is never invented, and the schema’s Zod contract has the final word on each record.
| Field | Applied and shown | Suggested, you accept | Flagged |
|---|---|---|---|
| All text | Trim, collapse whitespace, N/A · null · - to empty | – | – |
| Company name | – | – | Missing is an error |
| Lowercase domain, strip mailto: | – | Invalid is an error | |
| Website | – | acme.com → https://acme.com | Not http(s) or no domain is an error |
| Country | UK, GBR, Deutschland, U.S.A. to the canonical name | – | Unrecognised is a warning; NA is ambiguous |
| Tax ID | – | Remove spaces and punctuation, uppercase | – |
| Phone | – | – | Letters or fewer than 6 digits is a warning |
| Duplicates | – | – | Same tax ID, email, or name and country. Never merged. |
Decision · formula-safe export
A CSV that spreadsheets can’t run.
A cell starting with =, @, a tab or a carriage return gets a leading quote. Cells starting with + or - are escaped only when they hold more than a number, so phone numbers and negatives survive.
- -12.5stays
- +44 20 7946 0000stays
- +Plus Supplies Ltd'+Plus Supplies Ltd
- -cmd|' /C calc'!A0'-cmd|' /C calc'!A0
CWE-1236 · next to the CSV comes a JSON report of every decision, change, open issue and excluded row.
Built with
- Next.js 16
- React 19
- TypeScript 6
- LangGraph.js
- OpenRouter
- Zod 4
- TanStack Table
- Zustand
- Tailwind CSS 4
- shadcn/ui
- Papa Parse
- read-excel-file
- Vitest
- Playwright
- axe-core
Without an API key it runs in demo mode on deterministic heuristics, which also makes it reviewable in CI.









