Skip to content
All projects
TypeScript·LLM-scored job discovery, built on my own starter

job·finder

A multi-tenant job-discovery product that reads company ATS boards, scores every posting against your CV with an LLM, and prepares tailored application material. Built on GStack in 13 days.

Private repository
13
days from empty repo to a working multi-tenant product
9
ATS adapters, each a pure function with a fixture test
766
tests - 737 unit, 29 end-to-end

Stack

Nuxt 4TypeScriptSupabasePostgreSQLGeminipg_cronPlaywrightTelegramResend

Built on GStack, which is the point: the starter existed so that this could be about the product instead of about auth, tenancy and billing. First commit to a working multi-tenant application in 13 days.

The problem

Job boards optimise for volume. You scroll for forty minutes to find one posting worth reading, and the ones that match you best are buried under hundreds that do not. The work is not applying. The work is finding the three postings a week that justify the effort.

job·finder inverts it. It reads the boards on a schedule, scores every posting against your actual CV and preferences, and stays silent unless something clears your threshold.

No scraping, on purpose

Every posting comes from a public JSON API that companies publish deliberately: Greenhouse, Lever, Ashby, plus a handful of open feeds. Nine adapters, each a pure function - raw payload in, normalised postings out, no database access, a fixture test attached.

No headless browser, no anti-bot evasion, no proxy rotation. That is a decision recorded as an ADR, not a shortcut. A scraper wins the demo and loses the year: it breaks on a renamed CSS class and turns the product into an adversarial system you maintain forever. The cost of the choice is real - Workday and LinkedIn are simply unreachable - and it was worth paying.

Where the LLM is, and where it is not

Scoring runs on Gemini 2.5 Flash Lite, server-side only, with the rubric and profile in a cached system instruction and the posting as the payload. Structured output is validated against a schema on the way back, and a response that does not parse leaves the row unscored for the next run rather than storing something plausible and wrong.

The model does not do arithmetic. It scores four dimensions independently; application code computes the final number. Language models are good at judgement and unreliable at consistent multiplication, so it is only asked for the part it is good at.

The whole pipeline is off behind a flag until the rubric is calibrated. An uncalibrated scorer that runs automatically is just an expensive random number generator.

Operationally

  • Multi-tenant from the first migration - one account is one tenant, RLS scoped, with its own profile documents, tracked boards, settings and quota
  • Scheduled in the database - ingestion, scoring, digest and retention run on pg_cron rather than external workflow runners, so the schedule lives with the data it operates on
  • Idempotent ingestion - a re-run can never overwrite a status, score or generated letter, enforced at the upsert
  • Alerts that respect quiet hours - Telegram for anything above threshold, a daily email digest for the rest, at most once each
  • It never applies on your behalf. A human reviews and sends, always. That is a product boundary, not a missing feature.

Let’s build something that lasts

Hiring a senior engineer or architect for a remote team? I work across EU and US time zones - tell me what you're building.

Newsletter

I write about software architecture, PHP, Vue, TypeScript, and developer experience. No spam, unsubscribe anytime.

Copyright © 2026. All rights reserved.