Friction
- ↗ATS platforms publish a feed per company and no index across them, so there is nothing to search.
- ↗A hand-built watchlist silently caps coverage at the companies you already know to name.
- ↗Everything a job aggregator shows you is filtered by its coverage before you ever see it.
My Role
- ↗Sole designer and engineer: product thinking, interaction design, and all of the code.
- ↗Designed the triage loop — what a dismissal is worth, and what the tool is allowed to do with it.
- ↗Built the discovery, sweep, and index pipeline on Next.js, Vercel Cron, Blob, and Neon Postgres.
Outcome
- ↗One searchable list across seven ATS platforms, refreshed on a three-day lap.
- ↗Board discovery that finds companies I would never have thought to add.
- ↗A feedback loop that turns "not relevant" into a specific, reversible change to the search.
Summary
Job boards search their own index, not the internet, and the hiring platforms underneath them have no cross-company search at all. Jobwatch sweeps roughly 15,900 company career pages across seven ATS platforms on a schedule and hands back one list — currently 2,626 postings from 1,166 companies — with the triage tools to get through it. I designed it and wrote every line of it.
The Problem
Every applicant tracking system publishes a public feed per company, and none of them publish an index across companies. There is no "every job on Greenhouse" endpoint. That makes a watchlist structurally required, and a hand-typed watchlist caps your search at the companies you happened to think of.
I found that out the expensive way: a role I would have applied for was live for weeks and invisible to me, because the company was not on a list I had written by hand. Aggregators have the same shape of problem one level up — you are searching their coverage, not the market.
The Solution
Two halves that only work together: a pipeline that assembles the list nobody publishes, and an interface built for getting through it rather than browsing it.
Discover boards, don't type them
Board tokens are derived from a crawl-backed dataset and a crawler of my own rather than a list I maintain. Nothing is ever removed automatically — a bad crawl must not be able to delete what a good one found.
Filter before the network
The sweep runs server-side and applies the title test where the data already is, turning ~13,000 postings a shard into a few hundred. The browser makes one request for the result instead of fifteen thousand for the raw material.
Make a dismissal worth something
Removing a posting is useful on its own. The reason is the other half: enough of them agreeing is a filter that is wrong, and the panel where filters live is where the evidence and the fix sit next to each other.
The Process
Exploration
The layout had one question to answer before anything else: can you judge a posting, act on it, and read it without ever leaving the frame you are scanning in?
Try it in the browser, and fail
The first version fetched every board from the page. It worked at a dozen companies and fell apart at a hundred — tens of megabytes to render a few hundred rows. That failure is what defined the real architecture.
Move the work to a schedule
A full pass does not fit in one serverless invocation, so the sweep is sharded across cron runs and each shard writes only its own file. Reading, merging, and rewriting one index looks fine and silently loses postings to CDN staleness.
Design the triage loop
Dismiss first, ask second. The dismissed pile became its own room rather than a filtered view, because a posting you want back has to still be findable after you tighten something.
Make the numbers describe the list
Every figure in the header was counted off the raw index while the list below showed a filtered subset. Two numbers that can never be reconciled by looking is worse than no numbers, so they all moved onto the list you actually get.
Shipped
The tool as it runs today, against a live index.