An AI-powered campus placement and recruitment platform. Students apply to jobs, recruiters manage pipelines, and admins oversee the full hiring workflow — all behind role-based access control.
Built multi-role authentication with JWT and Supabase Row-Level Security, separating Student, Recruiter, and Admin data at the database layer.

Campus placement offices manage hundreds of student applications manually — spreadsheets, email threads, and no unified tracking system. Students have no visibility into their application status and recruiters have no structured way to compare candidates.
Vite + React with TypeScript. Protected routes using React Router v6. Each role gets its own route tree. Tailwind CSS for layout.
Node.js + Express REST API. Middleware validates JWT on every protected endpoint and extracts the role from the token payload before processing.
PostgreSQL on Supabase. Four core tables: users, companies, jobs, applications. Supabase RLS policies ensure database-level isolation per user.
Vite's code splitting keeps the initial bundle lean. Each role's dashboard is a separate route chunk, so a student's session never loads recruiter-specific code.
Passwords hashed with bcrypt. JWT contains role and userId — verified server-side on every request. Supabase RLS adds a second enforcement layer at the database.
The data has clear relationships: a job belongs to a company, an application links a student to a job. Relational constraints prevent orphaned records and make queries straightforward with JOINs.
Requires migration management and a fixed schema, which slows down early iteration compared to a document store.
If authorization logic lives only in the API layer, a bug could expose another recruiter's applicants. RLS enforces it at the database — even a direct DB connection respects the policy.
RLS policies can be difficult to debug and require learning Supabase's policy syntax carefully.
Authorization belongs at the data layer, not just the UI. Building RLS early meant I never had to audit the API for data leaks — the database simply refused unauthorized queries.