All use-cases

Bulk import with dry-run preview

Upload a file, auto-map columns, validate every row, preview a zero-write dry-run — then commit, deduplicate, audit, and notify.

The whole flow, end to end

Mail = email sent Bell = in-app alert Amber diamond = automatic check Amber card = blocked / rejected path
2 · VALIDATE & COMMIT 1 · UPLOAD & MAP Upload file CSV / spreadsheet · drag-drop Size + MIME OK? maxBytes · allow-list Magic-byte matches? real header vs Content-Type Detect entity + headers RFC 4180 · quoted commas · CRLF safe All required columns mapped? alias auto-suggest Validate every row required + type checks → row-indexed errors Rows valid? any row-indexed errors? Dry-run preview same staging path · zero writes Duplicate / conflict? naturalKey + contentHash Commit resolved rows created + skipped recorded Import complete audit row · email + in-app result pass pass valid new / changed Rejected — too large / wrong type 413 / 415 · BLOCKED BLOCKED fail Rejected — spoofed file header ≠ Content-Type · 422 BLOCKED fail Fix mapping map the missing required column no re-map Fix file + re-import row-indexed errors listed errors Duplicate skipped unchanged contentHash — no-op duplicate mapped then

The dry-run and the commit call the SAME pure stageRows function — no side effects, no writes. Natural-key + content-hash dedup makes re-importing the same file a guaranteed no-op.

What the import guarantees

  • Headers map to canonical fields with alias auto-suggest — a missed required column blocks the import, not corrupts it.
  • Validation is row-indexed — every error names its row and field; nothing fails silently.
  • Commit writes one immutable audit row with created + skipped counts; the result is emailed and shown in-app.

Live demo

app/import
Bulk import wizard — upload, map, dry-run preview, commit — screen recording
Recorded from the running app. Upload → auto-map headers → row-indexed validation → dry-run preview → commit — the same pure path the diagram describes.