[{"data":1,"prerenderedAt":148},["ShallowReactive",2],{"project:job-finder":3},{"id":4,"title":5,"body":6,"demo":111,"description":112,"docs":111,"draft":113,"extension":114,"highlights":115,"image":125,"language":126,"meta":127,"navigation":128,"order":129,"path":130,"repo":111,"seo":131,"site":111,"stack":132,"stem":140,"tagline":141,"tags":142,"__hash__":147},"projects\u002Fprojects\u002Fjob-finder.md","job·finder",{"type":7,"value":8,"toc":103},"minimark",[9,22,27,30,33,37,40,43,47,50,57,60,64],[10,11,12],"blockquote",{},[13,14,15,16,21],"p",{},"Built on ",[17,18,20],"a",{"href":19},"\u002Fprojects\u002Fgstack","GStack",", which is the point: the starter existed\nso that this could be about the product instead of about auth, tenancy and\nbilling. First commit to a working multi-tenant application in 13 days.",[23,24,26],"h2",{"id":25},"the-problem","The problem",[13,28,29],{},"Job boards optimise for volume. You scroll for forty minutes to find one posting\nworth reading, and the ones that match you best are buried under hundreds that\ndo not. The work is not applying. The work is finding the three postings a week\nthat justify the effort.",[13,31,32],{},"job·finder inverts it. It reads the boards on a schedule, scores every posting\nagainst your actual CV and preferences, and stays silent unless something clears\nyour threshold.",[23,34,36],{"id":35},"no-scraping-on-purpose","No scraping, on purpose",[13,38,39],{},"Every posting comes from a public JSON API that companies publish deliberately:\nGreenhouse, Lever, Ashby, plus a handful of open feeds. Nine adapters, each a\npure function - raw payload in, normalised postings out, no database access, a\nfixture test attached.",[13,41,42],{},"No headless browser, no anti-bot evasion, no proxy rotation. That is a decision\nrecorded as an ADR, not a shortcut. A scraper wins the demo and loses the year:\nit breaks on a renamed CSS class and turns the product into an adversarial\nsystem you maintain forever. The cost of the choice is real - Workday and\nLinkedIn are simply unreachable - and it was worth paying.",[23,44,46],{"id":45},"where-the-llm-is-and-where-it-is-not","Where the LLM is, and where it is not",[13,48,49],{},"Scoring runs on Gemini 2.5 Flash Lite, server-side only, with the rubric and\nprofile in a cached system instruction and the posting as the payload. Structured\noutput is validated against a schema on the way back, and a response that does\nnot parse leaves the row unscored for the next run rather than storing something\nplausible and wrong.",[13,51,52,56],{},[53,54,55],"strong",{},"The model does not do arithmetic."," It scores four dimensions independently;\napplication code computes the final number. Language models are good at\njudgement and unreliable at consistent multiplication, so it is only asked for\nthe part it is good at.",[13,58,59],{},"The whole pipeline is off behind a flag until the rubric is calibrated. An\nuncalibrated scorer that runs automatically is just an expensive random number\ngenerator.",[23,61,63],{"id":62},"operationally","Operationally",[65,66,67,74,85,91,97],"ul",{},[68,69,70,73],"li",{},[53,71,72],{},"Multi-tenant from the first migration"," - one account is one tenant, RLS\nscoped, with its own profile documents, tracked boards, settings and quota",[68,75,76,79,80,84],{},[53,77,78],{},"Scheduled in the database"," - ingestion, scoring, digest and retention run on\n",[81,82,83],"code",{},"pg_cron"," rather than external workflow runners, so the schedule lives with the\ndata it operates on",[68,86,87,90],{},[53,88,89],{},"Idempotent ingestion"," - a re-run can never overwrite a status, score or\ngenerated letter, enforced at the upsert",[68,92,93,96],{},[53,94,95],{},"Alerts that respect quiet hours"," - Telegram for anything above threshold, a\ndaily email digest for the rest, at most once each",[68,98,99,102],{},[53,100,101],{},"It never applies on your behalf."," A human reviews and sends, always. That is\na product boundary, not a missing feature.",{"title":104,"searchDepth":105,"depth":105,"links":106},"",2,[107,108,109,110],{"id":25,"depth":105,"text":26},{"id":35,"depth":105,"text":36},{"id":45,"depth":105,"text":46},{"id":62,"depth":105,"text":63},null,"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.",false,"md",[116,119,122],{"value":117,"label":118},"13","days from empty repo to a working multi-tenant product",{"value":120,"label":121},"9","ATS adapters, each a pure function with a fixture test",{"value":123,"label":124},"766","tests - 737 unit, 29 end-to-end","\u002Fprojects\u002Fjob-finder.webp","TypeScript",{},true,1.5,"\u002Fprojects\u002Fjob-finder",{"title":5,"description":112},[133,126,134,135,136,83,137,138,139],"Nuxt 4","Supabase","PostgreSQL","Gemini","Playwright","Telegram","Resend","projects\u002Fjob-finder","LLM-scored job discovery, built on my own starter",[143,144,145,146],"Product","AI","Multi-tenancy","Architecture","hCsDPWUsK5P7Ikh-GUOcFb5qxh6Dfb6OKc0-oJJfs60",1786623948919]