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.

8 min read • View on GitHub • More from parvej1362

A wide editorial scene of a recruiter and a candidate working on opposite sides of the same marketplace. A central control panel stands between them, suggesting that identity and permissions, not the job cards themselves, are what organize the product. The image explains Hirred as a two-sided system with one shared access layer.
Hirred is less a job board than a permissions pipeline with a marketplace UI on top.
Key Takeaways

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.

A close-up mechanical scene shows a sealed envelope labeled as an identity token being passed through a narrow slot into a vault. Inside, small gates open only for specific drawers, representing role-based access control. The image explains how Clerk-issued identity becomes Supabase permission context.
The interesting part is the handoff. Identity becomes a token, then a token becomes a permission path.

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.

One role field is enough when the token and the database rules do the rest.

LayerClassic backend-heavy marketplaceHirred
Auth and rolesCustom user table plus role logic in server codeClerk metadata with onboarding-driven role assignment
Permission enforcementApp checks plus server checks duplicated in multiple placesSupabase RLS as the final enforcement layer
Backend surface areaDedicated API, session management, and auth plumbingThin API modules and a small fetch wrapper
Data accessManual scoping in server endpointsToken-scoped access passed through Supabase
MaintenanceMore code paths to keep in syncFewer 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.

StepWhat happensWhy it stays manageable
UploadResume goes to Supabase StorageThe file system is externalized instead of reinvented
LinkA public URL is generatedThe database stores a pointer, not a blob
InsertApplication row is created with the resume URLThe record stays small and queryable
EnforcementToken-scoped access applies through RLSThe 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 choiceWhy it helps HirredTrade-off
React 19 + Vite 6Fast app shell with a modern build pathLess opinionated structure than a full framework
Tailwind v4Simpler styling pipelineRequires discipline to stay consistent
ClerkIdentity and metadata without custom auth codeVendor dependency for user state
SupabaseStorage, Postgres, and RLS in one placeAccess patterns depend on Supabase conventions
Zod + React Hook FormPredictable validation and form stateAnother layer to learn for simple forms
Shadcn UIComposable components without heavy abstractionVisual 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.