Skip to content

Jobsmith

Self-hosted job search tracker with an AI agent that drafts each application

Role
Creator & Sole Developer (Open Source)
Dates
Nov 2026
Status
Live
  • Next.js
  • NestJS
  • BullMQ
  • PostgreSQL
  • TypeScript
  • Prisma
  • Redis
  • Zod
  • Tailwind CSS
  • shadcn/ui
  • React PDF
  • Google Gemini API
  • Vitest
  • Docker
  • Turborepo
  • GitHub Actions
Try the live demo
Jobsmith

Summary

A working, self-hosted job search tracker with a seven-step application agent. You can use your own AI key or none. 92 tests pass in CI against real PostgreSQL and Redis.

The problem

I built Jobsmith while applying to dozens of jobs across the UK, Ireland and the EU. My spreadsheet fell apart. I lost track of which CV version went to which company and when to follow up. I couldn't tell which companies could even sponsor a visa. And I spent hours rewriting cover letters that all said the same thing. Jobsmith puts the whole search in one private place and hands the repetitive half of each application to an agent. It is open source, so any job seeker can run their own copy and keep their data to themselves.

My role

Solo project. I own the whole project. I wrote the PRD, feature list and decision notes, then built and deployed the web app, API, worker, agent and demo myself.

What I built

  • Wrote the PRD, a prioritised feature list, a nine-phase build plan (8 phases for version 1, plus 1 optional) and five decision notes.
  • Built the tracker: board with drag and drop, table, application page, contacts, reminders, calendar feed, CSV import and full export.
  • Built the CV builder: one profile, many versions, and three A4 templates whose live preview matches the exported PDF.
  • Built the seven-step agent as a BullMQ job, with Zod-checked outputs, live progress over Server-Sent Events and re-runs of single steps.
  • Built bring-your-own-key AI (Gemini, OpenAI, Anthropic or any OpenAI-compatible server), with keys encrypted using AES-256-GCM.
  • Built the SSRF-safe job fetcher, the daily sponsor register import, demo mode, Docker self-hosting and CI.

Architecture

A pnpm and Turborepo monorepo: - apps/web: Next.js (App Router), with the landing page, the app and the demo. It only displays data and forwards /api/* to the API. - apps/api: NestJS with Prisma on PostgreSQL. It checks the session on every request and scopes every query to the signed-in user. A separate worker entry runs the BullMQ queues. - packages/shared: Zod schemas and pure logic used by both apps: keyword match, salary rules, the CV document builder and company name matching. - packages/pdf: React templates for CV and cover letter PDFs (@react-pdf/renderer). - prompts/ and evals/: versioned prompt files, and an evaluation set that runs those same files. Redis holds the queues, caching and rate limits. Agent runs, PDFs and the register import each have their own queue, which runs one job at a time. Agent progress streams to the browser over Server-Sent Events. AI comes from the user's own key: Gemini (default), OpenAI, Anthropic or an OpenAI-compatible server. docker compose runs the web app, API, worker, PostgreSQL and Redis.

Key decisions and trade-offs

Chose: the agent as seven fixed steps in one BullMQ job. Each step's output has a Zod schema and is saved separately. Only reading the job and the two drafts call the AI. Because: cost and time are predictable, outputs can be checked, and the user can watch progress. It still works without a key. An invalid answer is retried once, then shown as an error. The agent can't send, submit or fetch anything on its own. Chose: job advert text goes into prompts inside labelled untrusted_job_posting tags. Any such tags inside the text are removed, and every prompt says the content is data only. Because: an advert that says "ignore previous instructions" must not change what the agent does. The evaluation set has an injection case that tests this. Chose: my own fetcher for job pages. It allows http and https on ports 80 and 443 only, and checks every DNS answer against private, loopback, link-local, CGNAT and cloud metadata ranges. It connects to the checked address, follows at most 3 redirects (each checked again), and stops at 10 seconds or 2 MB. Because: a "paste a link" box on a self-hosted server is an SSRF risk. The same check guards user-chosen OpenAI-compatible endpoints. Chose: bring your own key, with Gemini as the default. Keys are encrypted with AES-256-GCM and are write-only: only the last 4 characters are ever shown. Because: the project has no AI bill. Bring your own key: Gemini, OpenAI, Anthropic or any OpenAI-compatible API. Everything except the writing steps works with no key, and the demo never uses the host's key.

Hardest problem and how I solved it

Worker memory: over 1 GB → about 250 MB (batched SQL import, 160 MB heap cap) The daily import of the Home Office sponsor register ran the shared server out of memory. The register has about 143,000 rows. At first the worker read the whole CSV into an array before inserting it. I changed it to stream rows straight from the download in batches of 1,000, still inside one transaction, so badges never see a half-loaded register. That was not enough. Measured on the real register, Prisma's createMany grew the process by about 6 MB per 1,000 rows, over 1 GB in total. I replaced it with one fixed INSERT ... SELECT FROM unnest(...) per batch, with each column passed as an array. That keeps the retained heap near 60 MB and loads the whole register in about 3 seconds. V8 frees memory lazily, so I also capped the worker's heap well under its 350 MB restart limit. With a 192 MB heap the import peaked at 349 MB on the server; with 160 MB it stays near 250 MB. Heavy jobs got their own one-at-a-time queues, and the import no longer runs when the worker starts. The demos now run on their own server, away from client sites.

Testing and quality

92 tests pass: 83 API tests in 13 files (Vitest and Supertest) and 9 shared-logic tests. The API tests run against real PostgreSQL and Redis with a fake AI provider, so they never call a real AI. They cover login and refresh-token rotation, user data isolation on every record route, SSRF protection, job advert extraction, sponsor matching and register batching, salary rules by date, CV snapshots that can't change, reminders firing once, API key encryption, agent step validation and demo key expiry. CI runs a secret scan, typecheck, lint, tests, build and pnpm audit. A second job builds the Docker images, starts the whole stack and checks health through the web proxy, the API and the worker. AI quality is checked separately with an evaluation set of 4 made-up cases, including prompt injection, run by hand with your own key.

Results

  • 92 tests in CI against real PostgreSQL and Redis
  • 4 of 7 agent steps are plain code and never call the AI
  • 1 GB+ → ~250 MB worker memory (batched SQL import, 160 MB heap cap)

Version 1 is built: all 8 version 1 phases of the nine-phase plan are done (the optional ninth is not started), and CI passes on main. It is open source under the AGPL-3.0 licence and runs with one docker compose command. Try the live demo: press "Start the demo" for a private copy of a made-up job search. No email or password is needed, and it is deleted after 24 hours.

Key features

  • Board and table from Saved to Offer, with days in stage and sponsor badges.
  • Save a job from a link, a pasted advert or a bookmarklet.
  • One profile, many CV versions, and three ATS-friendly A4 PDF templates.
  • Seven-step agent: read the job, sponsor check, salary check, CV pick, cover letter, message and follow-up date.
  • UK sponsor register status for every company, updated daily from GOV.UK.
  • Salary check against Skilled Worker thresholds, with GOV.UK sources (a guide only).
  • Dashboard with a funnel, reply rates by channel and CV version, and a weekly target.
  • Reminders, a calendar with a private iCal feed, CSV import and a full export.
  • Bring your own AI key, or use it with none.

Out of scope

Version 1 leaves out auto-applying to jobs, scraping job boards, LinkedIn or Indeed integration, inbox sync, a browser extension, native apps and team features. The salary check doesn't yet check going rates per occupation, because those need SOC codes.