Case 01 · Radar

A CRM of my own for the job search

I ran my job search like a sales pipeline. When the tool got too heavy, I built my own.

811 applications migrated, with history, notes and CVs From plan to a live system holding the whole history: 2 days 0 tables left in HubSpot after the migration
In four lines
Situation
A job search run like a sales pipeline: stages, a reason for every closed application, a note per contact. HubSpot held it for two months, then got heavier than the process. Too many clicks, missing fields, and nowhere for the fit score an AI agent was already producing per job.
Task
Replace the tool without losing the history, and without creating a system I would have to maintain after the search ends.
Action
Built a small CRM of my own: one process, SQLite on a volume, a written shutdown plan on day one. Migrated with item-by-item reconciliation instead of dual-write, put fit in as a system field, and had backups running before the second screen existed.
Result
811 applications migrated with 980 notes and 245 CVs, 0 tables left in HubSpot, the whole history live in 2 days. A low fit now closes a job before the CV.
The problem

Since August I have tracked applications in a real CRM, with stages, a reason for every closed one and a note per contact, because a serious search has the same problem as a sales pipeline: volume, dispersion and silence. HubSpot solved that for two months.

Then it stopped serving. Every application took too many clicks, the fields that mattered to me did not exist in the default, and the fit evaluation an AI agent was already doing for each job had nowhere to live.

What stood
Applications migrated811, with 980 notes and 245 PDFs verified
From plan to the whole history live2 days (October 12 to 13, 2026)
DeployA push to the main branch publishes to production
BackupDaily, server plus cloud, verified
What I decided
  • Separate and temporary. Radar lasts as long as the search does. It was born with a written shutdown plan on day one: export everything, take the last backup, remove the container. A system that knows when it will die is a system that never becomes debt.
  • Small on purpose. One process, a SQLite database on a volume, 200 MB of memory on a shared server. Postgres would have been one more container and one more backup to look after. An operating decision. One process, a SQLite database on a volume, 200 MB of memory on a shared server. Postgres would have been one more container and one more backup to look after. An operating decision, not a taste.
  • Migrate with reconciliation, never dual-write. HubSpot stayed as a mirror until switch day. The import was checked item by item: 811 deals became 793 applications and 18 merged duplicates, with 980 notes and 245 CVs in PDF.
  • Fit as a system field. Every job comes in with a 0 to 100 fit score, calculated by an agent that reads the description and applies my elimination criteria before any CV work. Radar keeps the number and the reason.
  • Gmail comes in, but never takes over. Google login restricted to one e-mail, read and send through OAuth. The system never moves, labels or deletes anything in the inbox.
  • New application from a link. Pasting the job URL fills in company, role, location, work mode and description. What the site does not expose as structured data, the system reads from the HTML.
  • Backup before features. A daily copy on the server and on B2, encrypted, with an integrity check. That was live before the second screen existed.
The human part

The previous CRM showed me something uncomfortable: I was applying to jobs outside my target, and I only noticed a month later, in an audit. Radar exists so that this shows up on the spot. A low fit closes the job before the CV. An elimination criterion closes it before the fit.

Funnel discipline is the same thing in a sales team and in a career. Only the person on the other side changes.

Next.jsReactSQLiteDockerNginxGitHub ActionsGoogle OAuthBackblaze B2Claude Code

The demo runs the same code as my real instance, with fictional companies and people. Anything you change is reset every hour.