Skip to content
All builds
Operations

RapportTiny CRM

What it does for you

Remember every client, deal, and follow-up.

A private, in-browser CRM with contacts, a weighted deal pipeline, follow-ups, smart views and CSV import, built on a fully tested logic core.

Rapport screenshot

Who it's for

Freelancers and tiny shops that need to remember who they talked to and what they promised. Nothing leaves your browser, so client details stay off someone else's server.

Screenshots

Rapport screenshot 1
Rapport screenshot 2
Rapport screenshot 3

In more detail

Rapport is a small, private CRM that runs in your browser: people, companies, deals, follow-ups, and tasks, with nothing sent to a server. It's for freelancers and tiny shops who need to remember who they talked to and what they promised, without putting client details on someone else's server. It keeps the useful 20 percent of a CRM and leaves the rest.

Honest limits

Deals were moved through the per-card stage menu in the automated runs, not a real mouse drag. Desktop notifications never went through a real permission prompt or an actual OS notification. Naming a smart view still uses a browser prompt(). None of that is hammered; it runs and the core is tested.

How I built it

An earlier Claude Code session wrote the logic core, crm.js, and a 316-assertion test file, then the connection dropped before any UI existed. All that was left was an empty index.html. In this session I pointed Claude Code at that core with a brief that forced small writes. Instead of loading the 76 KB core, it read the API with grep and small line ranges, then built the UI as 17 small ES modules on top: dashboard, contacts, companies, a six-stage kanban pipeline, follow-ups, tasks, import and tools, and a Ctrl+K command palette.

To click through the real app, Claude wrote two throwaway browser harnesses that drove it inside an iframe: palette, fuzzy search, smart views, the query builder, stage moves, merge, CSV import, snooze, and a v1 to v3 storage migration. They passed 18 of 18 and 7 of 7 and were then deleted, because one of them clears localStorage. I set the scope, the no-AI rule, the fictional sample data (40 made-up contacts in demo mode), and the one-file-per-screen layout.

What I learned

  • A pure core that takes "now" as an argument pays off twice. The UI pass had almost no logic bugs, and the one real bug (CSV import) was a shape mismatch at the seam: parseCSV returns {headers, rows} and the UI expected an array. That seam now has a test watching it.
  • Re-rendering each view into a fresh container is a cheap way to avoid stacking event listeners in vanilla JS. My first version bound handlers to a persistent <main> and would have fired them twice after one re-render.
  • Utility class names leak across features. .t-email meant chart color in one file and a timeline row in another, and the chart rule painted my timeline gold. Prefix per feature.
  • An em dash guard written with a literal em dash fails on its own test file. Building the character from its code point fixed it.

What carried forward

This is the "one pure logic module, imported by both the browser and verify.mjs" pattern from #9, #10, and #11, stretched to a much bigger core: filters, fuzzy scoring, dedupe, merge, CSV, vCard folding, recurrence, snooze, forecast, and schema migration all live in one DOM-free file. The iframe harness trick for clicking through a no-build app with headless Chrome also shows up in #41 and #50 from this same batch.

If I took it further

I'd build the UI in the same session as the core, in small files from the start, so a dropped connection costs one file instead of the whole interface. Then I'd swap the prompt() for an inline field and test drag and drop and notifications by hand.

#042 Built with Claude Code September 23, 2026 381 automated checks

Want one like this?