A production-grade job board application built with the MERN stack and modern tooling. Recruiters can post and manage job listings, job seekers can browse, search, and apply — all with role-based access control, Google OAuth, and a clean responsive UI.
- Features
- Tech Stack
- Project Structure
- Getting Started
- API Reference
- Authentication
- Role System
- Screenshots
- Docker
- Roadmap
- https://layerhire.up.railway.app [Deployed using docker containers]
- https://layerhire-front.onrender.com
- Browse and search all active job listings
- Filter by job title, type, experience level, salary, and location
- Sort by newest, oldest, salary high and low
- View full job details with requirements and salary range
- Can upload resume on profile page
- Apply with a cover letter, CV attach automatically — one application per job enforced
- Track all applications and their status in real time
- Seeker will get push notifications on job application status change
- Post new job listings with full details and skill tags
- Manage all posted jobs (edit, delete, toggle active)
- View all incoming applications in a dashboard along with CVs
- Update application status — pending, reviewing, accepted, rejected
- Application stats overview with live counts
- When recruiter changes application status, push notifications will be sent to the seeker
- Email/password registration and login
- Google OAuth — single click sign in and sign up
- Github OAuth — single click sign in and sign up
- Role-based access control throughout the app
- Fully responsive UI across all screen sizes
- Persistent sessions via secure cookies
| Technology | Purpose |
|---|---|
| React 19 + TypeScript | UI framework |
| Vite + Bun | Build tool and dev server |
| React Router v7 | Client-side routing |
| Redux Toolkit | Global state management |
| TailwindCSS v4 | Utility-first styling |
| Shadcn/ui + Radix | Accessible component library |
| Lucide React | Icon library |
| Axios | HTTP client |
| BetterAuth (client) | Session management |
| Firebase Push Notification | Firebase Push Notification |
| Technology | Purpose |
|---|---|
| Express + TypeScript | REST API server |
| Bun | JavaScript runtime and package manager |
| MongoDB + Mongoose | Database and ODM |
| BetterAuth | Authentication (email + Google OAuth) |
| MongoDB Native Driver | BetterAuth database adapter |
| Firebase Push Notification | Firebase Push Notification |
| Technology | Purpose |
|---|---|
| Docker + Docker Compose | Containerization |
| ESModules | Module system throughout |
LayerHire/
├── backend/ # Express + TypeScript API server
│ └── src/
│ ├── config/ # DB connection, env config, Firebase/Supabase init, Better Auth
│ │ ├── auth.ts # BetterAuth instance and config
│ │ ├── db.ts # MongoDB connection
│ │ ├── env.ts # Environment variable validation
│ │ └── supabase.ts # Supabase initialization and config
│ ├── middleware/ # Auth guards, error handlers, protected routes middlewares etc.
│ │ ├── protect.middleware.ts # Auth guard + role guard + protected routes middleware
│ │ └── error.middleware.ts # Global error handler
│ ├── modules/
│ │ ├── jobs/ # Job module
│ │ │ ├── jobs.model.ts
│ │ │ ├── jobs.types.ts
│ │ │ ├── jobs.service.ts
│ │ │ ├── jobs.controller.ts
│ │ │ └── jobs.routes.ts
│ │ ├── applications/ # Applications module
│ │ │ ├── applications.model.ts
│ │ │ ├── applications.types.ts
│ │ │ ├── applications.service.ts
│ │ │ ├── applications.controller.ts
│ │ │ └── applications.routes.ts
│ │ ├── recruiter/ # Recruiter module
│ │ │ ├── recruiter.model.ts
│ │ │ ├── recruiter.types.ts
│ │ │ ├── recruiter.service.ts
│ │ │ ├── recruiter.controller.ts
│ │ │ └── recruiter.routes.ts
│ │ ├── notifications/ # Notifications module
│ │ │ ├── notifications.service.ts
│ │ │ ├── notifications.controller.ts
│ │ │ └── notifications.routes.ts
│ │ └── resume/ # Resume module
│ │ ├── resume.controller.ts
│ │ └── resume.routes.ts
│ ├── utils/
│ │ ├── asyncHandler.ts # Async route wrapper
│ │ └── apiResponse.ts # Consistent response helpers
│ └── server.ts # App entry point
│
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ ├── ui/ # Shadcn generated components
│ │ │ └── shared/ # Navbar, Layout, ProtectedRoute, JobCard
│ │ │ ├── Footer.tsx
│ │ │ ├── JobCard.tsx
│ │ │ ├── Layout.tsx # Main layout
│ │ │ ├── Navbar.tsx
│ │ │ ├── ProtectedRoute.tsx # Protected layout
│ │ │ └── ResumeUpload.tsx # Resume upload component
│ │ ├── features/
│ │ │ ├── auth/ # Login, Register, auth slice
│ │ │ │ ├── auth.slice.ts
│ │ │ │ ├── LoginPage.tsx
│ │ │ │ └── RegisterPage.tsx
│ │ │ ├── jobs/ # Jobs slice and types
│ │ │ │ ├── EditJobModal.tsx
│ │ │ │ ├── jobs.slice.ts
│ │ │ │ └── jobs.types.ts
│ │ │ └── applications/ # ApplyModal, applications slice
│ │ │ ├── applications.slice.ts
│ │ │ ├── applications.types.ts
│ │ │ └── ApplyModal.ts # ApplyModal component
│ │ ├── hooks/ # useAuth, useJobs, useApplications
│ │ │ ├── useApplications.ts # useApplications hook
│ │ │ ├── useAuth.ts # useAuth hook
│ │ │ └── useJobs.ts # useJobs hook
│ │ ├── lib/ # Axios instance, auth client
│ │ │ ├── auth-client.ts # Auth client
│ │ │ ├── axios.ts # Axios instance
│ │ │ ├── resume.api.ts # Resume upload API
│ │ │ └── utils.ts # Axios interceptors
│ │ ├── pages/ # JobsPage, JobDetailPage, PostJobPage, etc.
│ │ │ ├── AboutPage.tsx
│ │ │ ├── BlogPage.tsx
│ │ │ ├── CareersPage.tsx
│ │ │ ├── ContactPage.tsx
│ │ │ ├── DashboardPage.tsx
│ │ │ ├── JobDetailPage.tsx
│ │ │ ├── JobsPage.tsx
│ │ │ ├── MyApplicationsPage.tsx
│ │ │ ├── PostJobPage.tsx
│ │ │ ├── PrivacyPolicyPage.tsx
│ │ │ ├── RecruiterDashboard.tsx
│ │ │ ├── SeekerDashboard.tsx
│ │ │ └── TermsOfServicePage.tsx
│ │ ├── store/ # Redux store
│ │ │ └── index.ts
│ │ ├── types/ # Shared TypeScript interfaces
│ │ │ ├── auth.types.ts
│ │ │ └── index.ts
│ │ ├── App.tsx # Router setup
│ │ └── main.tsx # App entry point with providers
│ ├── public/
│ │ ├── firebase-messaging-sw.js # Firebase push notifications service worker
│ │ └── sw-env.js # Environment variables for firebase-messaging-sw.js
├── docker-compose.yml
├── .env.example
└── README.md
Make sure you have these installed:
- Bun v1.0 or higher
- MongoDB running locally or a MongoDB Atlas URI
- Docker (optional — for containerized setup)
- A Google Cloud Console project with OAuth credentials
- Clone the repository
git clone https://github.com/yourusername/job-board.git
cd job-board- Install backend dependencies
cd backend && bun install- Install frontend dependencies
cd ../frontend && bun installBackend — create backend/.env from the example:
cp .env.example backend/.env# Server
PORT=3000
MONGO_URI=mongodb://localhost:27017/jobboard
CLIENT_URL=http://localhost:5173
# BetterAuth
BETTER_AUTH_SECRET=your_32_character_secret_here
BETTER_AUTH_URL=http://localhost:3000
# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Github OAuth
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Supabase (Phase 5 — resume uploads)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your_anon_key
# Firebase (Phase 6 — push notifications)
FIREBASE_PROJECT_ID=your_project_id
FIREBASE_PRIVATE_KEY=your_private_key
FIREBASE_CLIENT_EMAIL=your_client_email
FIREBASE_API_KEY=your_api_key
FIREBASE_AUTH_DOMAIN=your_auth_domain
FIREBASE_STORAGE_BUCKET=your_storage_bucket
FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id
FIREBASE_APP_ID=your_app_idFrontend — create frontend/.env:
cp .env.example frontend/.envVITE_API_URL=http://localhost:3000
VITE_APP_URL=http://localhost:5173
VITE_FIREBASE_API_KEY=your_firebase_api_key
VITE_FIREBASE_APP_ID=your_firebase_app_id
VITE_FIREBASE_AUTH_DOMAIN=your_firebase_auth_domain
VITE_FIREBASE_MESSAGING_SENDER_ID=your_firebase_messaging_sender_id
VITE_FIREBASE_PROJECT_ID=your_firebase_project_id
VITE_FIREBASE_STORAGE_BUCKET=your_firebase_storage_bucket
In firebase console, Web Push certificates -> Key pair value is VAPID
VITE_FIREBASE_VAPID- firebase-messaging-sw.js is already added to frontend/public folder
- Add below scripts in package.json
"build:env": "echo \"self.apiKey = '$VITE_FIREBASE_API_KEY'; self.authDomain = '$VITE_FIREBASE_AUTH_DOMAIN'; self.projectId = '$VITE_FIREBASE_PROJECT_ID'; self.storageBucket = '$VITE_FIREBASE_STORAGE_BUCKET'; self.messagingSenderId = '$VITE_FIREBASE_MESSAGING_SENDER_ID'; self.appId = '$VITE_FIREBASE_APP_ID';\" > public/sw-env.js",
"build": "tsc -b && npm run build:env && vite build",- Go to Google Cloud Console
- Create a new project or select an existing one
- Navigate to APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID
- Set Authorized JavaScript origins to
http://localhost:5173 - Set Authorized redirect URIs to
http://localhost:3000/api/auth/callback/google - Copy the Client ID and Client Secret into your
backend/.env
# Terminal 1 — start the backend
cd backend && bun dev
# Terminal 2 — start the frontend
cd frontend && bun dev| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:3000 |
| Health check | http://localhost:3000/api/health |
All API responses follow this shape:
{
"success": true,
"message": "Success",
"data": {}
}| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/jobs |
Public | Get all jobs (supports filters) |
GET |
/api/jobs/:id |
Public | Get job by ID |
POST |
/api/jobs |
Recruiter | Create a new job |
PUT |
/api/jobs/:id |
Recruiter | Update a job |
DELETE |
/api/jobs/:id |
Recruiter | Delete a job |
GET |
/api/jobs/recruiter/my-jobs |
Authenticated | Get current recruiter's jobs |
Query parameters for GET /api/jobs:
| Param | Type | Description |
|---|---|---|
search |
string | Full-text search on title, company, description |
type |
string | full-time | part-time | contract | internship | remote |
experienceLevel |
string | entry | mid | senior | lead |
location |
string | Partial match on location |
page |
number | Page number (default: 1) |
limit |
number | Results per page (default: 10) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/applications |
Seeker | Apply to a job |
GET |
/api/applications/my |
Seeker | Get my applications |
GET |
/api/applications/check/:jobId |
Seeker | Check if already applied |
GET |
/api/applications/recruiter |
Recruiter | Get all applications for my jobs |
GET |
/api/applications/stats |
Recruiter | Get application status counts |
PATCH |
/api/applications/:id/status |
Recruiter | Update application status |
This project uses BetterAuth for authentication.
- Email/Password — standard register and login flow
- Google OAuth — one-click sign in and sign up
- Sessions — stored in MongoDB, persisted via HTTP-only cookies
- Session duration — 7 days, refreshed if older than 1 day
BetterAuth automatically creates and manages these MongoDB collections:
users email, name, role, emailVerified, image
sessions token, userId, expiresAt, userAgent
accounts OAuth provider data (Google)
verifications email verification tokens
Every user is assigned a role at registration:
| Role | Access |
|---|---|
seeker |
Browse jobs, apply, track applications |
recruiter |
Post jobs, manage listings, view and update applications |
Routes are protected at both the API level (via protect and requireRecruiter middleware) and the UI level (via the ProtectedRoute component with an optional allowedRole prop).
Run the entire stack with a single command:
docker-compose up --buildThis starts three containers — frontend (nginx), backend (Bun), and MongoDB — all networked together.
# Stop all containers
docker-compose down
# Stop and remove volumes (clears database)
docker-compose down -v- Phase 1 — Authentication (BetterAuth + Google OAuth + roles)
- Phase 2 — Jobs module (CRUD + search + filters + pagination)
- Phase 3 — Applications module (apply, track, recruiter dashboard)
- Phase 4 — Docker (containerize frontend + backend + MongoDB)
- Phase 5 — Supabase (resume PDF/docx uploads)
- Phase 6 — Firebase (push notifications on job application status change)
- Phase 7 — Deployment (Render)
MIT