A Scalable job portal built with the PERN stack that connects job seekers and recruiters with advanced search, application tracking, ATS Scoring and role based access control.
Climb your career like a Yeti climbs a mountain.
- Overview
- Screenshots
- Demo URL
- Tech Stack
- Architecture Overview
- Folder Structure
- Environment Variables
- 📦 Libraries Used
- Installation & Setup
- Docker Setup
- API Documentation
- Database Design
- Cron Task
- Testing
- Deployment
- Security
- Performance Optimization
- Scalability Consideration
- Challenges and Learnings
- Limitation
- Additional features
- Updates
- Future Improvements
The Project is a Job Portal Platform with all the features needed to build a job portal platform, such as CRUD operations, role-based access control, jobs, companies, apply, withdraw, create a job, create a new company, admin controller, and a cron queue.
- Frontend: https://yeti-jobs.vercel.app
- Backend: https://yeti-jobs.onrender.com/api/v1/
- Backend API Demo: https://yeti-jobs.onrender.com/api/v1/swagger
- Apply/Withdraw Job, Add to Bookmark/Remove To Bookmark
- Search Jobs
- Edit Profile And Other Credentials
- Add a Resume/Profile Picture
- View all jobs
- View all applications
- View All Bookmarks
- All Companies List
- Individual Company jobs and Description About Company
- View Single Job
- ATS Scoring And Feedback for Resume
- Company Dashboard
- All Applicants
- See All employees
- See Profile
- Create/Delete/Edit a Job
- Update Company
- Change Applicant Status
- Get All Followers Company
- Dashboard Stats such as:
All Owned Jobs All Applications All Employees All Followers
- Assign User to companies.
- Delete/Create/Update Company
- Company Entire Overview dashboard
- Company Dashboard Stats such as:
Total Jobs Total Applications Open Jobs All Followers
- Authentication (JWT)
- Email Verification & password reset
- Role based access control.
- Frontend:
- React
- TailwindCSS
- Backend:
- Node.js
- Express
- Database:
- Raw PostgreSQL
- Devops:
- Docker
- The System follows a layered architecture: Client(React) -> API(Express) -> Service Layer -> PostgreSQL
- JWT-base authentication
- Modular services for jobs, applications, companies
- File storage via Supabase
- server.js
- src -- controllers/ -- middleware/ -- models/ -- routes/ -- services/ -- utils/ -- tests/ -- db/
- api/
- components/
- pages/
- hooks/
- context/
- lib/
- data/
- DATABASE_PASSWORD
- JWT_SECRET_KEY
- NODEMAILER_MY_EMAIL
- NODEMAILER_MY_PASSWORD
- NODEMAILER_MY_host
- SUPABASE_URL
- SUPABASE_ANON_KEY
- CLIENT_BASE_URL
- PORT
- MAXAGE
- GROK_API
- VITE_SERVER_URL
To Run the System to a Local Server, we've to make sure have the muliple of systems for differnet purpose. Requirements: Node.js, Postgres Server, Supabase Keys, Nodemailer Keys Backend Configuration: Here’s the shorter, cleaner version of what you need — straight to the point.
- Node.js – to run the JavaScript code
- PostgreSQL – database to store data
- Supabase keys – for database + auth (URL + anon key)
- Nodemailer keys – to send emails (email + app password)
cd backend
touch .env # Create Env File
vim .env (Insert all the env keys on here)- After inserting all the env keys
npm i # Install all our node libraries
node app.js # Run our nodejs server✅ your server will run on the http://localhost:PORT
cd frontend
touch .env
vim .env # Insert a: VITE_SERVER_URL on .env file. npm i: # Install all our node libraires
npm run dev # load our react page to browser✅ your client page will run on the http://localhost:5173
- Docker base has only one single container for the Node.js configuration.
- Use the
nodeimage. - In the coming time, I plan to migrate my database to Docker.
- First, build the image of the Node.js application.
cd backend
docker build -t yeti-jobs-backend . # on current folder- Now Run the Docker Container:
docker run -d -p 3000:3000 --name yeti-jobs-backend: # Run's on the backgound
- Swagger UI: https://yeti-jobs.onrender.com/api/v1/swagger
- It Will Provide a Interactive Graphical User Interface to Api Documentation to all our backend endpoints.
- View all available routes (jobs, users, companies, etc.)
- Check request parameters, body, and headers
- See response formats and status codes
The database follows a strict Separation of Concerns principle — each table is normalized to handle a single responsibility with high data integrity.
- Enums enforce fixed value sets at the database level for fields like
role,education,job_type,application_status, andemail_verification_type. - Indexes are applied on frequently queried columns — a GIN index on
jobs.search_titlefor full-text search, and btree indexes oncompanies.nameandemail_verified.verified_code. - Constraints & checks validate data at three levels — client, server, and database — ensuring integrity even if upper layers are bypassed.
- Triggers automate internal operations such as populating the
search_titletsvector column on job insert/update. - Referential integrity is handled via
ON DELETE CASCADE(e.g. deleting a user removes their email verifications and follows) andON DELETE RESTRICTwhere linked data must be preserved before deletion is allowed.
erDiagram
USERS {
uuid uid PK
text fname
text lname
enum education "Basic|Matrix|Undergraduation|Postgraduation|High School"
text email
text password
enum role "guest|admin|recruiter"
uuid company_id FK
text phone
text resume_url
text profile_pic_url
text[] skills
int experience
}
COMPANIES {
uuid uid PK
text name
text description
text website
timestamptz created_at
int2 founded_year
text location
text logo_url
}
JOBS {
uuid uid PK
text title
text description
int8 salary
enum job_type "Remote|Onsite|Hybrid"
enum is_job_open "active|closed"
uuid company_id FK
uuid created_by FK
tsvector search_title
text[] skills
int8 total_job_views
date created_at
int experience_years
text location
date expired_at
}
APPLICATIONS {
uuid uid PK
uuid user_id FK
uuid job_id FK
enum status "applied|rejected|hired|shortlisted"
timestamptz applied_at
text cover_letter
int4 notice_period
int8 expected_salary
text why_hire
}
SAVED_JOBS {
uuid uid PK
uuid user_id FK
uuid job_id FK
uuid company_id FK
timestamptz created_at
}
USER_COMPANIES_FOLLOWS {
uuid uid PK
uuid user_id FK
uuid company_id FK
timestamptz created_at
}
EMAIL_VERIFIED {
uuid uid PK
uuid user_id FK
enum verified_type "verify_mail|forget_password"
timestamptz created_at
timestamptz expired_at
int4 verified_code
bool is_verified
}
ATS_SCORE {
uuid uid PK
uuid user_id FK "Foreign key to USERS.uid"
timestamptz created_at
int4 score
jsonb feedback
}
USER_EDUCATION {
uuid uid PK
uuid user_id FK "Foreign key to USERS.uid"
text university_name
text degree
date start_date
date end_date
text grade
}
USERS }o--o| COMPANIES : "belongs to"
USERS ||--o{ JOBS : "creates"
USERS ||--o{ APPLICATIONS : "submits"
USERS ||--o{ SAVED_JOBS : "bookmarks"
USERS ||--o{ ATS_SCORE : "has"
USERS ||--o{ USER_EDUCATION : "has"
USERS ||--o{ USER_COMPANIES_FOLLOWS : "follows"
USERS ||--o{ EMAIL_VERIFIED : "has"
COMPANIES ||--o{ JOBS : "posts"
COMPANIES ||--o{ SAVED_JOBS : "referenced in"
COMPANIES ||--o{ USER_COMPANIES_FOLLOWS : "followed by"
JOBS ||--o{ APPLICATIONS : "receives"
JOBS ||--o{ SAVED_JOBS : "saved in"
- A cron task runs at a specific time that we define.
- I'm using cron for jobs that have an expiry time of 30 days. It checks every night at midnight.
- At every noon, the cron node checks if any jobs have expired. If expired, it updates the
is_job_activecolumn in thejobstable to "closed".
- Set up the basic configuration using
jestandsupertest, and test only the/api/v1/jobsendpoints. - Add testing using
jestandsupertestfor all job routes. - Include only two test routes initially:
/jobs,/jobs/:id, and/users/login-status. - More tests will be added in the coming days, mainly for job and user routes.
- Unit testing
- Integration testing (using supertest)
- Focus on critical routes (auth, jobs, companies)
The React app is deployed on Vercel, which auto-detects the Vite setup with no complex configuration needed.
- Add
VITE_SERVER_URLin Vercel's environment variables dashboard. - Every push to
maintriggers an automatic redeploy.
The Node.js/Express server is deployed as a Web Service on Render.
Note: Vercel does support Express via serverless functions and a
vercel.jsonconfig — it was explored but Render was chosen for its persistent server model, which fits Express better than a serverless environment.
- Uses an IPv6-compatible connection pooler to connect to Supabase PostgreSQL.
- Cold start: Render's free tier sleeps after 15 minutes of inactivity. A cron job pings the server every 15 minutes to keep it alive.
- Add all backend environment variables in Render's dashboard before deploying.
PostgreSQL database and file storage (resumes, profile pictures) are both hosted on Supabase.
Migrating from localhost to Supabase:
- Get the
DATABASE_URLconnection string from Supabase → Settings → Database. - Replace the local
host,port, anduserconfig with the singleconnectionString. - Add
ssl: { rejectUnauthorized: false }to allow incoming connections from Render.
File uploads are handled via the @supabase/supabase-js SDK — files go directly into Supabase Storage buckets and the returned public URL is saved to the database.
- Every major table will have validation from Zod which checks the integrity of our data.
- beside the client side validate, server side validation, i also make sure to add the database validation
- even if user bypass a both client and server validation it can't insert due to the database validation.
- With checking a text pattern, blank/undefined, correct data type, unique constraint,min length max length which are common for the data validation i've implemented.
- Ensure every piece of data maintains database integrity at all times.
- The Most important things that i added here is the rate limiting.
Rate Limiting:
Rate limiting: 200 req/min globally
Reset password: 2 req/min strictly
- use the helmet for the reponse purpose which remove the
X-Poweredby that the client will not konw which framework we've build without this it'll show it build from the express.- Also have one more feature it's add 12 more responsive header, for better secuirty purpose of prevent from the
xss attack.
- Also have one more feature it's add 12 more responsive header, for better secuirty purpose of prevent from the
- Use the
corslibrary for only allow my client url dont' allow any external api endpoints which also have a better security feature for avoid a cross side attack."_ — should read something like: _"Uses thecorslibrary to whitelist only the client URL, blocking external origins to prevent cross-origin attacks.
- Validate all incoming requests using middleware on both the client side and the server side to guarantee data consistency and security.
- on the client validation guest can't visit the page of the admin dashboard and the other admin restricted page and also the employee restricted page.
- while the employees only restrict to perform a employee can't apply to the jobs or can't perform and also neither a guest or the admin action.
- admin which have little bit of the freedom but also enforce data on control integrity can't visit the page of the guest or the employee action.
- More than: 9+ middleware for server validation of custom middleware.
- With make the controller user action to the only isJobSekkker, companies contoller to the isEmployee and the admin contoller to the isAdmin.
✅ Added indexing on frequently queried columns for faster data retrieval :white_check_mark: Used
SELECT EXISTS(SELECT 1 ...)instead of fullSELECTstatements for condition checks — returnstrue/falsewithout fetching rows :white_check_mark: Indexed search query fields to ensure faster full-text or filter operations :white_check_mark: Indexed email verified code, users email and company name for the faster retrieval.
✅ Validated email domains via Node's built-in
dns/promisesmodule before attempting to send mail — prevents unnecessary SMTP calls :white_check_mark: Implemented pagination for job listings to limit payload size per request
✅ Applied lazy loading with
React.lazy()andSuspense— components and data are only fetched when needed :white_check_mark: Centralized auth state (verified, logged-in status, user role) usinguseContextto avoid redundant checks and prop drilling
- API versioning (
/api/v1) and MVC pattern keep the codebase modular and easy to extend. - Global error handling on both client and server prevents crashes — every error is caught and returned with a proper status code (
2xx,4xx,5xx) and message. - PostgreSQL full-text search with a GIN index on
jobs.search_titlereplaces slowILIKEprefix queries for job searching. - Query optimization — joins, group bys, and nested queries are tested with
EXPLAIN ANALYZEto catch slow plans before they hit production. - Rate limiting (200 req/min globally, 2 req/min on reset password) prevents abuse and protects the server from being overloaded by a single user.
- Caching is not yet implemented but the architecture is ready for it — currently comfortable handling up to ~10k MAU.
- Monitoring & observability is planned for when the user base grows — not a priority at the current scale but will be added before hitting 10k+ users.
-
UI inconsistencies Some components still have minor alignment and responsiveness issues across different screen sizes.
-
Email system reliability Email delivery (verification/reset) lacks robust failure handling, retry mechanisms, and proper logging.
-
Token resend logic Verification and reset token resend flow can create edge cases (e.g., overlapping or unused tokens).
-
Query parameter binding inconsistency A few PostgreSQL queries do not follow consistent parameterized patterns, which may lead to maintainability issues.
-
Limited test coverage Testing is currently focused on selected job routes, leaving other critical modules (auth, companies) under-tested.
-
Cold start latency (backend) Backend hosted on free tier (Render) experiences delays after inactivity.
-
Posstgre Query
- Not Release a
Poolquery after succesfuly connection which cause a silent failure.
- Not Release a
-
Free-tier infrastructure constraints Hosting (Render + Supabase) limits performance, concurrent users, and scalability.
-
No real-time features Currently lacks real-time communication (e.g., chat, notifications, live updates).
-
No recruiter–candidate communication system There is no direct messaging or interaction between job seekers and recruiters.
-
No caching layer Absence of Redis/CDN caching increases response time under heavy load.
-
Limited scalability (~10k MAU) System is optimized for small to medium scale but not yet production-ready for large-scale usage.
-
Basic monitoring & observability No logging, alerting, or performance monitoring tools (e.g., metrics dashboards).
-
Partial Dockerization Only backend is containerized; database and full system orchestration are not yet implemented.
-
Incomplete feature ecosystem Missing advanced features like:
- Interview scheduling
- Notification system
- Dockerize a entire system with to the nodejs application and also docker ignore some files:
.dockerignore - Only Install a system settings where it required on the production not on the development.
- also have the controller and also the abort feature if the request takes longer time dont' wat for more than a 10 sec.
- now on the react 19 we dont' need:
auth.providerrather it also work a auth - use the portal system for the popup of the some features.
- for previous a page on the profile picturee of the resume to change it i can use:
createObjectURLto print show it. - Implement the vercel analytics on client side for the get the stats about the frontend application.
- REcruiter/Company Employee will have full control of change a status of any jobs applicant
- With hr have the full control which use to reject which to move forward to the interview or the hired or rejected hr have full control.
- ATS scoring for any user profile with background jobs queue.
- Add the List of the bruno all api endpoints link to convert to the swagger ui and add the endpoints of:
api/v1/swagger - Add the Phone Number In User Information.
- Resume parsing Analysis with extract skills education from:
pdf-parserlibrary. - The Problem that i'm facing that i must fix is that on the useAuth backeend is not correctly sending a data and frontend is also not implementing of:
useAuthfunction
- Add the Pagination to the Companies Page
- Move to the pooling of the database
- ATS Scoring to the Resume.
- User can add their education from backend as of now but i'll soon also add the frontend side.
Note
- Add notification/email when a company posts a new job.
- Real-time chat between recruiters and applicants.
- Move from useContext to Redux.
- Add logging/monitoring/observability.
- Socket.io for real-time features.
- interview scheduling system with automated email reminders and video call link generation from Google Calendar API.
- User can add a their employment_history and shows that employment history to the user page.
- List of the education history with the college cgpa and degree.
- Alert a user only to those which user followed their company with new jobs, must be the background jobs else it'll block the main block.
- Add the notification page list about notified user about recent events, followed companies notification, recruiter viewed your resume.
- profile completneess score based on the badged applicant top skills and how much active jobs seeker.
- On the edit content page if user try to submit a content without any change don't allow them which reduce a less backend request.
- Adding a CDN to cache our static assets that never changed
- Move Our Asynchronous operation to the background queue with use services such as:
Kafka. - left a add of the my
user_educationtable informat.






