Every week, men from my part of Pakistan pay money they cannot afford to lose for jobs that do not exist. A recruiter's ad looks official. The visa sounds real. The "processing fee" gets paid. Then the phone number goes dead.
I am a head teacher at a government primary school in Punjab. At night, I build software. When a former student showed me a fake Gulf job ad that had cost his cousin two months of savings, I stopped treating this as someone else's problem. The result is InGulfJobs, a job board I built solo and launched on 9 November 2026. Every listing shows the recruiter's licence number and permission number on the page, next to the job. If a recruiter cannot be checked, the job does not go up.
This is the honest build story: the stack, the two production bugs that taught me the most, and what I would tell anyone else who wants to build a job board website for a niche they actually understand.
The Problem Is Trust, Not Listings
There is no shortage of Gulf job ads. Facebook groups, WhatsApp forwards and newspaper classifieds carry hundreds of them for Saudi Arabia, the UAE, Qatar, Kuwait, Oman and Bahrain. What is missing is any way for a job seeker in a small town to answer one question: is this recruiter real?
Licensed recruiters in Pakistan operate under government permission numbers, and each overseas vacancy they advertise carries its own permission reference. That system exists. Almost nobody displays it where workers can see it. So I made it the core data model of the board: a job record on InGulfJobs is not complete until it carries the agency name, its licence, and the permission number for that specific vacancy. Verification is not a badge I paste on afterwards. It is a required field.
That one decision shaped everything else, including the workload. Verified jobs cannot be scraped in bulk. Each one has to be read, checked and entered with its numbers attached. My target for the first month is 30 to 35 verified jobs a day, and by the second day the board held 195 live jobs. That pace is the price of the trust promise, and it is also the moat. Anyone can copy a job board's design in a weekend. Copying a daily verification habit is harder.
The Stack: Workers, D1 and R2, Nothing Else
I build most of my projects on Cloudflare, including a free software downloads site, FileCobra, so I stayed with tools I already knew how to operate at 1 a.m.:
- Cloudflare Workers render every page on the server (SSR). Job pages must arrive as finished HTML because search engines and WhatsApp link previews do not wait around for client-side rendering.
- D1, Cloudflare's SQLite database, holds the jobs, agencies and categories. A job board is rows and relationships; it does not need anything fancier.
- R2 stores images: job photos, agency logos and the share posters.
- GA4 for analytics, and structured data on every job page, which gets its own section below.
No separate server, no container to patch, no database to babysit. The whole thing runs on bindings between three Cloudflare products, and my hosting bill for launch week was effectively zero. For a side project built by one person with a day job, boring infrastructure is a feature.
Bug One: The Category Count That Ate My Database
The first real production problem was my own query. Every page of the site shows category counts in the navigation — how many jobs in Saudi Arabia, how many in the UAE, and so on. I computed those counts the obvious way: a GROUP BY over the jobs table, run on every single page render.
With a few dozen jobs, it was fast. It was also a trap. D1 bills and limits you on rows read, and a full-table aggregate on every request means your rows-read grows with traffic multiplied by table size. The dashboard numbers climbed far faster than the traffic justified, and it took me an embarrassingly long hour to accept that the innocent little sidebar was the cause.
The fix was a small cache table in D1 itself, site_cache. The category counts and agency list are computed once, stored as a cached aggregate with a 15-minute TTL, and read from there on each render. The cache is invalidated the moment a job is inserted or updated, so the counts are never stale when it matters, which is right after new jobs go in. Rows read dropped back to sane levels immediately.
The lesson generalises beyond D1: any number you show on every page should be computed on write, not on read. I knew this rule. I still broke it, because with a small table the wrong way feels free. It is only free until it is not.
Bug Two: Google Emails You About Your Schema
A job board lives or dies on Google for Jobs. Jobs reach that carousel through JobPosting structured data (JSON-LD) on each job page, and I had shipped what I believed was complete markup: title, description, date posted, hiring organisation, location, salary where the ad stated one.
Then Google Search Console sent the email every developer of a jobs site eventually gets: structured data issues detected. My validThrough was a bare date, and Google wanted a full ISO datetime so it knows the exact moment a listing expires. My pages were also missing two recommended properties entirely, jobBenefits and qualifications — both sitting in my database as plain text (benefits and requirements are part of every verified ad I enter), just never mapped into the JSON-LD.
The fix took one deploy: build the expiry as a full datetime, and map the benefits and requirements fields I already had into the schema. Nothing new had to be collected. The data was there; the contract with Google was incomplete.
Two more pieces keep the listings flowing. A sitemap alone is slow for pages that expire in weeks, so new and updated jobs also go to Google through the Indexing API and to Bing and other engines through IndexNow. Job boards are perishable inventory. An indexing delay of ten days can be half a listing's life.
One honest limit: some genuine ads state no salary figure at all. My schema leaves salary out for those rather than inventing a range, and those listings are weaker for it. I accept that. A board whose entire pitch is "these numbers are real" cannot start estimating the numbers.
Distribution: WhatsApp First, Google Second
I did not launch to a cold start. For months I have run a WhatsApp jobs community that has grown to around 700 members, workers and job seekers who already trusted the posts there. InGulfJobs launched into that community first, and the site is built around how those users actually share things: on WhatsApp, in groups, from cheap Android phones.
That is why every job gets a generated share card, a 1200×630 poster-style image with the job title, salary, location and last date on it, served as the page's og:image. When a member pastes a job link into a group, the preview that appears is readable on its own, even for someone who never taps through. It is a small feature. It is also, so far, the main way jobs travel.
Search will matter more over time, and the schema and indexing work above is my bet on it. But a niche board's first thousand users rarely come from Google. They come from a community that already has the problem.
What I Would Do Differently
- Cache aggregates from day one. The
GROUP BY bug was predictable and predicted, by me, in other people's code reviews. Write-time aggregation for anything rendered site-wide.
- Treat the schema as a checklist, not a guess. I mapped the properties I knew Google required and stopped. Print the full recommended list and map every field you hold before launch.
- Plan the daily operations, not just the code. Thirty-plus verified jobs a day is a content operation wearing a software costume. The entry form, the permission-number fields, the poster generation — those matter as much as the framework.
Takeaways for Anyone Building a Niche Job Board
If you want to build a job board website for a community you know, my honest summary is:
- Pick the trust mechanism first. For InGulfJobs it is licence and permission numbers on every listing. Yours might be salary transparency, or verified employers, or manual review. The mechanism is the product.
- Choose boring infrastructure you can run alone at night. Workers + D1 + R2 has not given me a single operational surprise I did not cause myself.
- Compute site-wide numbers on write. Cache them. Invalidate on change.
- Ship complete
JobPosting data, push updates through the Indexing API and IndexNow, and read your Search Console email like it is a bug report. It is.
- Launch where the community already talks. For Gulf job seekers in Pakistan, that is WhatsApp, not a Product Hunt post.
InGulfJobs is a side project, built by a teacher after school hours, and it shows in the numbers: hundreds of jobs, not hundreds of thousands. But every one of them can be checked against a licence, and that is the whole point. If you are hiring for the Gulf, or looking for work there, the board is live at ingulfjobs.com. And if you are building something similar for your own community, I write up my other projects and what they taught me at rehmanahmad.com — steal the lessons, skip the bugs.