Hirred: A Job Board That Treats Auth, Roles, and Data Access as One System
A close look at how a modern React app uses Clerk metadata, Supabase JWTs, and a thin data layer to serve recruiters and candidates without building a heavy backend.
- Hirred’s real idea is not the job board, but the way it turns identity into database access without a custom auth service.
- Clerk metadata and a Supabase JWT template let the app keep role logic at the edges while Row Level Security does the enforcement work.
- The app stays thin because its fetch layer, onboarding flow, and storage upload path all lean on platform primitives instead of bespoke backend code.
- That simplicity makes Hirred fast to prototype and easy to reason about, but also more dependent on third-party assumptions.
Why Hirred Feels Bigger Than a Job Board
Hirred looks like a marketplace app, but its real subject is coordination. The codebase is organized around a simple premise: a recruiter and a candidate should live inside one product, yet see very different data and actions. That problem is usually solved with a custom backend, a user table, and a lot of glue. Hirred trims that down to identity metadata, token exchange, and database rules.
That is why the repo feels instructive. It is not showing off a flashy UI. It is showing a small-team way to build something that still behaves like a serious SaaS product.
The Hidden Core: Clerk Metadata Meets Supabase RLS
The smartest part of Hirred is how little ceremony it uses to define who a user is. Roles are stored in Clerk metadata, onboarding pushes the user into the right state, and Supabase receives a Clerk-issued token that lets Row Level Security decide what the user can actually touch. The app does not need a separate auth database to remember the basics.
| Layer | Classic backend-heavy marketplace | Hirred |
|---|---|---|
| Auth and roles | Custom user table plus role logic in server code | Clerk metadata with onboarding-driven role assignment |
| Permission enforcement | App checks plus server checks duplicated in multiple places | Supabase RLS as the final enforcement layer |
| Backend surface area | Dedicated API, session management, and auth plumbing | Thin API modules and a small fetch wrapper |
| Data access | Manual scoping in server endpoints | Token-scoped access passed through Supabase |
| Maintenance | More code paths to keep in sync | Fewer moving parts, more reliance on platform primitives |
That separation matters. Clerk handles identity state, while Supabase handles database truth. Hirred sits in the middle and keeps its own code deliberately small.
Onboarding as Authorization
Hirred treats onboarding as part of access control, not just setup. If a user has no role in Clerk metadata, ProtectedRoute sends them to onboarding. Once they choose recruiter or candidate, the app writes that choice back to unsafeMetadata through Clerk’s user update flow.
// onboarding.jsx
const handleRoleSelection = async (role) => {
await user.update({
unsafeMetadata: { role }
});
};
// protected-route.jsx
if (!user?.unsafeMetadata?.role) {
return <Navigate to="/onboarding" replace />;
}
That is a clean product decision. It avoids a separate profile table whose only job would be to say whether the user is a recruiter or a candidate. It also makes the route guard and the stored metadata agree with each other, which keeps the system easy to debug.
The Fetch Layer Is the Real Infrastructure
The quiet backbone of the app is use-fetch.jsx. It wraps async callbacks, tracks loading and error state, and fetches a Clerk token using the Supabase template before any database call goes out. That means page components do not have to know how the token is acquired or where it gets attached.
const { loading, data, error } = useFetch(async (token) => {
return await getJobs(token);
});
// inside the hook
const token = await session.getToken({ template: "supabase" });
await cb(token);
The payoff is structural. Authentication concerns stay inside one reusable layer, while the pages stay focused on rendering and state transitions. For a small team, that is exactly the sort of boring infrastructure that prevents later chaos.
How a Candidate Applies Without the App Becoming a Mess
The application flow shows why thin API modules work here. A candidate uploads a resume to Supabase Storage, the app generates a public URL, and then it inserts the application row with that file reference. The whole path stays understandable because each step maps to a single platform primitive.
| Step | What happens | Why it stays manageable |
|---|---|---|
| Upload | Resume goes to Supabase Storage | The file system is externalized instead of reinvented |
| Link | A public URL is generated | The database stores a pointer, not a blob |
| Insert | Application row is created with the resume URL | The record stays small and queryable |
| Enforcement | Token-scoped access applies through RLS | The backend does not need a custom permission layer |
This is the pattern Hirred repeats well. Use the platform for the hard part, keep the application code thin, and let the data model stay legible.
Why the Job Listings Feel Fast
The listing experience gets its speed from local filtering. In job-listing.jsx, search, location, and company filters update against fetched or fallback data in real time. The result feels responsive without requiring a more elaborate search service for a small catalog.
That choice fits the rest of the repo. The app is not trying to outgrow its data model before it needs to. It uses simple client-side state where the user experience benefits most.
This Stack Is the Real Product
The stack is almost the point of the project: React 19, Vite 6, Tailwind v4, Clerk, Supabase, React Hook Form, Zod, and Shadcn UI. None of that is novel by itself. The combination matters because it reduces friction in the exact places a solo builder feels pain first: auth, forms, validation, styling, storage, and protected data access.
| Stack choice | Why it helps Hirred | Trade-off |
|---|---|---|
| React 19 + Vite 6 | Fast app shell with a modern build path | Less opinionated structure than a full framework |
| Tailwind v4 | Simpler styling pipeline | Requires discipline to stay consistent |
| Clerk | Identity and metadata without custom auth code | Vendor dependency for user state |
| Supabase | Storage, Postgres, and RLS in one place | Access patterns depend on Supabase conventions |
| Zod + React Hook Form | Predictable validation and form state | Another layer to learn for simple forms |
| Shadcn UI | Composable components without heavy abstraction | Visual consistency still needs curation |
Hirred reads like a blueprint for a modern small-team SaaS starter. It is optimized for shipping a credible marketplace without building the infrastructure that a larger company might need later.
What Hirred Gets Right, and What It Still Signals About Maturity
The repo is strong where architecture clarity matters most. It makes roles explicit, keeps data access scoped, and avoids the common trap of spreading auth logic across the app. That is a good sign whether you see it as a portfolio piece or the base of a real product.
There are also hints of beta maturity. Dummy data appears in fallback paths, which suggests the dynamic layer is still being filled in. That does not weaken the architecture story. It simply marks the difference between a strong system design and a fully finished marketplace.
If the dynamic paths continue to replace the fallback ones, Hirred becomes a credible product foundation. If not, it still works as a sharp example of how far you can get when identity, routing, and database permissions behave like one system.