Stack
Why this was the right shape
Djob is the kind of project where the public website is only the visible edge. The harder product is the admin and matching layer behind it: jobs, candidates, applications, CV-derived statements, embeddings, score gates, snapshot tables, and workflows for contacting matched people.
That is why the timeline was closer to six months. A two-sided platform is not one user flow; it is two user flows plus the operating system that connects them.
Key build decisions
Quick apply and full candidate records coexist
The public job card supports quick apply without a CV, while the modal still tries to find or create a candidate record so the admin side does not lose the applicant in a disconnected application table.
Embeddings are built from structured statements
Jobs and candidates are synced into statement-part tables and embedding tables, rather than embedding one large unstructured blob per record.
Matching uses gates, not only one score
The matching service computes title score, required score, optional score, time gates, pass/fail reasons, and final totals so the UI can explain why a match passed or failed.
Snapshot tables keep the UI usable
Candidate and job match views read from snapshot tables for ranked results instead of recalculating every match live every time a recruiter opens a page.
What shipped
- Public job board with job cards, categories, regions, dates, external links, and quick apply.
- Candidate creation, job creation, and admin editing flows.
- Candidate-to-job and job-to-candidate matching views.
- OpenAI embedding service using text-embedding-3-small.
- Daily rebuild scripts and snapshot tables for match results.
- Controls for email/WhatsApp follow-up tracking on matched roles.
What got tricky
- A single match score is not enough in recruiting. A candidate can be semantically close to a role but fail a hard requirement or time requirement, so the system needed visible gates and reasons.
- The first matching implementation had to evolve into a snapshot model because recruiter screens need fast, repeatable rankings.
- Job and candidate data comes from different shapes and levels of completeness, so the sync layer has to validate, normalize, and rebuild safely.
What we would improve next time
- Expose a plain-English explanation of match reasons on more screens so recruiters can trust the ranking without opening debug views.
- Move more of the threshold configuration into admin settings once enough real usage shows which defaults are too strict or too loose.
- Add analytics around which matches are emailed, WhatsApped, ignored, or converted so the matching model can be tuned from outcomes, not just scores.
Want a teardown like this for your own workflow?
Send the current process, the tools involved, and what keeps breaking. We can usually find the smallest useful build before anyone writes code.
Talk through a project