~/austinmula
mambo-hr-new-swiss
Web ApplicationLive

Mambo HR

A production recruitment platform for a Nairobi HR consultancy — public job board, candidate portal, and a role-gated admin CMS with analytics. Server Components read Postgres directly and typed Server Actions handle every write, with auth enforced in middleware and re-checked per action, and Zod schemas as the single validation contract across client and server.

a public marketing site and job board, a candidate portal, and a full admin CMS with an analytics dashboard.

Architecture at a glance

(marketing) public site + job board + blog + events ← static / ISR
(auth) login · signup ← credentials + bcrypt
/portal candidate area ← any signed-in user
/admin CMS + analytics dashboard ← role-gated in middleware
/api (auth, forms, uploads)

Key decisions

  1. Server Components by default; no API layer where none is needed. Reads query Postgres directly inside Server Components. Writes go through typed Server Actions instead of REST endpoints. Client components are the exception: the two interactive charts, the forms, the mobile nav.
  2. Auth guards at both layers. Middleware protects the route via Auth.js's authorized callback (role promoted into the JWT at sign-in), so redirects happen before a page renders — no client-side flash of protected content. Every Server Action independently re-checks the session before mutating, so a leaked action ID still can't write.
  3. Zod is the single validation contract. Six schemas in lib/validations are the source of truth: React Hook Form resolves against them client-side for instant feedback, and the Server Action parses the same schema before touching the database. The inferred type is the form type is the action's argument type. Client validation is a UX affordance; the server never trusts it.
  4. Status lives in the database, not in string conventions. Five native Postgres enums (job_status, blog_status, event_status, event_type, user_role) across 12 tables. A draft job can't leak onto the public board because of a typo in a filter, and a column rename surfaces as a compile error in every page that reads it.
  5. Charts with zero dependencies. Trend lines, stacked columns, sparklines, bar lists and part-to-whole bars rendered from ~850 lines of hand-written SVG. No Recharts, no D3. The categorical palette is validated against the render surface for colourblind separation, chroma floor and 3:1 contrast — and every chart ships a table view, so no value is reachable only through colour.
  6. Numbers that don't lie. The dashboard suppresses period-over-period deltas when the prior period was zero (a "+100%" off a base of nothing is a fabricated statistic) and attaches no percentage to running totals, which are point-in-time counts rather than period measures.
  7. Rendering strategy chosen per route. 18 routes prerender static; events use ISR at 60s; anything reading the session is dynamic by necessity. SEO is done with the framework rather than around it — generated sitemap.ts / robots.ts, per-route generateMetadata, JSON-LD for the organization and job postings.
  8. No runtime dependency on third-party hosts. Fonts self-hosted through next/font; a render-blocking Google Fonts icon stylesheet and a remotely-loaded texture were both removed. Uploads are compressed in the browser before they hit the network, then recorded in media_uploads so the gallery stays queryable.