A Job Board That Remembers - Marcos Sánchez
I’ve put jobctl.net online. It’s a job board that<br>keeps the history other job boards throw away. Postings are polled,<br>reconciled against the last look, and every difference is written down:<br>appeared, changed, went missing, closed, came back. It’s small and it’s<br>opinionated. It exists because of two grudges and one curiosity.
Grudge one: they took RSS away
Browsers used to understand feeds. There was an icon in the address bar,<br>you clicked it, and from then on the site came to you. Firefox<br>removed live bookmarks in 2018.<br>Chrome never cared. Safari still has an RSS button, though I don’t think<br>anyone at Apple remembers why. The web didn’t stop publishing feeds. Most<br>of it still does. It just stopped telling you.
What replaced the feed was the tab. I had fifty of them open, each one a<br>site that would have pushed its changes to me if anyone had left the door<br>open. So the first thing jobctl is, under everything else, is a feed<br>reader that remembers. It polls, it keeps what it saw, and it hands the<br>result back as a feed. Every filter on /jobs<br>travels in the URL, and the URL is an<br>Atom feed. Paste it into any<br>reader and it stays current. No account, no app, no notification<br>permission dialog.
Grudge two: hunting for a job in 2026 is a tab marathon
This started when a couple of friends asked me to help them look. They’re<br>mid-level and junior, which is a separate post about who the industry has<br>decided it no longer hires. I said yes and found out what the procedure<br>actually is. You open Greenhouse for company A, Lever for company B, a<br>Rippling board for C, four aggregators that scrape each other (we’re no<br>different, I know), and two national boards behind a login wall. Then you<br>do it again on Thursday, because the one from Tuesday is gone and nobody<br>tells you when it went.
The postings are public and the APIs are documented.<br>Greenhouse alone<br>publishes a JSON board for every company that uses it. There’s no reason a<br>human should be the loop that walks them, so jobctl walks them: 91 sources<br>today, 57 Greenhouse boards, 28 feeds, plus Manfred, Rippling and<br>JobFluent, listed with their health on<br>/sources. The number goes up whenever I have<br>an evening, and there’s a suggest a source<br>page for that. If it’s a public feed or a documented API and its terms<br>allow it, it goes in. Anything that has to be tricked into responding<br>stays out. Conditional requests, a real User-Agent with a contact URL,<br>per-source rate limits and backoff. A failing source gets recorded as<br>failing, not hammered.
The curiosity: what do companies do with their postings?
This is the part I care about most and the part that’s least finished.
A job board shows you a snapshot: what’s open right now. It can’t tell you<br>that a posting has been reopened five times in eight months, that its<br>salary range vanished after week one, or that it’s been “urgently hiring”<br>since March. That information exists. It’s just never kept. Once you keep<br>it, a few questions that are currently folklore become answerable:
Which postings flap, close, reopen, close, reopen, and how often?
Which companies keep an evergreen “Senior Engineer” open for a year with<br>no visible change? That’s what CV harvesting looks like from outside.
Do salary ranges get narrowed after publication? Removed?
How long does a role at company X stay open, on median?
jobctl is built to make those queries possible later without lying now.<br>The core is an append-only event log: ItemObserved, ItemSeen,<br>ItemChanged, ItemMissing, ItemClosed, ItemReopened, plus<br>per-source fetch results. The vocabulary is about what I observed, not<br>about what a publisher did. A feed never says a job was deleted; it just<br>stops showing it. So “closed” is a conclusion reached after N consecutive<br>misses, N per source, never a fact anyone reported. I tried closing on<br>the first miss and got a timeline full of jobs dying and coming back that<br>had never gone anywhere, which is worse than no timeline at all. The<br>reconciler that decides this is a pure function,<br>(previous, snapshot) -> []Event, with no I/O, and that’s what lets me<br>property-test it: replay is deterministic, an item present in every<br>snapshot never closes, an item missing for exactly the threshold closes<br>once and only once, a reopening is always visible in the timeline.
Every observation also keeps the source’s original bytes. My<br>normalisation rules are wrong today in ways I haven’t found yet, and the<br>raw payload is the only thing that lets me re-derive them when I do.
Boring on purpose
Dan McKinley’s<br>Choose Boring Technology gives every<br>project a handful of innovation tokens. jobctl spends none of them on the<br>stack, so it can spend all of them on the one idea above.
It’s one Go binary and one Postgres. The event log is a table. The<br>current view and the...