The problem

Every Friday, South Africa's Department of Public Service and Administration publishes the Public Service Vacancy Circular: every advertised government post in the country, as a single PDF of around 340 pages, with several hundred new posts each week. It is the only official source, and almost nobody reads it. Most jobseekers see one post as a screenshot forwarded on WhatsApp, often after it has already closed.

The people who need these posts most are mostly on low-cost Android phones with prepaid data. A 340-page PDF is the wrong format for them in every way: too big to download, impossible to search, and silent about which posts are still open. GovPosts exists to turn that PDF into something a person can actually use on the bus.

GovPosts is independent and not affiliated with the DPSA or any government department.

What it does

GovPosts parses each weekly circular into individual posts that are searchable and filterable by province, category, salary level, qualification and closing date. Every post shows its closing date with the time, the reference number the applicant must quote, and a deep link to the exact page of the original PDF so nobody has to take my word for it. Where the circular doesn't say something, the site says "Not stated in circular" rather than guessing. There is a weekly email alert with double opt-in, and the whole thing is free, with no account. Each post also carries JobPosting structured data so it can appear in Google for Jobs, and Google's Indexing API is told when a circular is published and when a post is withdrawn.

Key engineering decisions

The model may classify and rephrase, never originate a fact

An LLM normalises and categorises each post: it maps free text to a category, a salary band and a tier. But a wrong closing date can cost someone a job, so the model is never trusted with facts. Every date and rand amount it returns is checked to appear verbatim in the source text before it is stored. A value that fails the check is discarded and the post is flagged for human review instead of published. The trade-off is more manual review and a slower pipeline on messy circulars, in exchange for never showing a closing date the circular didn't print.

Posts have a lifecycle across circulars

Later circulars carry extensions, withdrawals and corrections to earlier posts, and re-advertisements supersede old ones. A notices parser extracts these, and a resolver matches each notice to an earlier post by reference number, department and title. It deliberately ignores the circular number printed in the notice, because those are sometimes wrong: circular 32 has cited "circular 20" for posts that ran in 31. Anything short of one clear match goes to a human with the candidates attached. Every applied change writes an audit row, and the job page shows that history in plain language ("The closing date moved from X to Y"), linked to the source page. Withdrawn posts keep their page instead of disappearing. The cost is a lot of code for what looks like a small feature, but it is the difference between a mirror of the PDF and a source people can trust.

Workflows, not Queues, for the ingest pipeline

Parsing a 300-page circular is a long, multi-step job: fetch the PDF from R2, extract text, parse posts and notices, enrich in batches through the model, validate, resolve lifecycle changes, publish. I chose Cloudflare Workflows over Queues because each step gets its own retry and backoff, a run survives a deploy in the middle of a parse, and the workflow can sleep for free while waiting on a batch of model calls. The trade-off is that Workflows is a newer, more opinionated primitive than a queue, with less community knowledge to lean on. For a weekly job where correctness matters more than throughput, durable steps won.

Built for prepaid data

The site is server-rendered with no client-side framework. Search and every filter are plain GET forms, so they work with JavaScript disabled, and there is an end-to-end test that runs the site with JavaScript off to keep it that way. The /jobs page has a documented first-load budget of 200 KB including fonts, and currently sits well under it. Closing states are computed in Africa/Johannesburg so a post never shows as open a day late for a reader in the country it applies to. The trade-off is that I gave up the interactivity most job boards have; in exchange, the site is fast on the phones it is actually used on.

Stack

Astro 5 in server mode on Cloudflare Workers, because the whole product is HTML over a database and Workers keeps it cheap and close to the user. D1 holds the posts with an FTS5 index (plus a trigram index for misspellings) for search. R2 stores the original circulars and the per-post extracted text. Cloudflare Workflows run the ingest and publish pipelines. Cloudflare Access protects the admin area, so there is no hand-rolled auth. Resend sends the double opt-in emails and the weekly digest. Claude handles the classification and normalisation, through the Batch API since nobody is waiting on it.

What's next

The parser has been measured against a small number of real circulars, so generalising to the layout quirks of future issues is the largest open risk; adding more fixtures is the most valuable thing I can do. Beyond that: better extraction of Western Cape reference numbers, and an application-compliance helper that checks a post's stated requirements against what the applicant is about to submit.