Multi-Tenant SaaS Job Portal — Naukri / Hirist style platform
React 19 PWA · .NET 10 Clean Architecture API · PostgreSQL · Redis · Docker
- Project Overview
- Architecture Overview
- Tech Stack
- Backend Architecture
- Frontend Architecture
- Database Architecture
- Infrastructure & DevOps
- Security
- Modules & Features
- Getting Started
- Project Structure
- Roadmap
JobPortal is a production-grade, multi-tenant SaaS job platform that connects:
| Actor | Role |
|---|---|
| Job Seekers | Register, build profiles, upload resumes, search & apply for jobs |
| Employers / Recruiters | Post jobs, search candidate database, manage ATS pipeline |
| Hiring Managers | Review shortlisted candidates, schedule interviews, submit feedback |
| Tenant Admins | Manage their organisation, branding, team members, billing |
| Super Admins | Manage all tenants, subscriptions, platform config, audit logs |
| Public / Guests | Browse public job listings without logging in |
- Multi-tenancy — Row-Level Security in PostgreSQL; each tenant's data is fully isolated
- PWA — Installable, offline-capable React frontend
- Scalable — Redis cache, cursor-based pagination, connection pooling
- Secure — JWT RS256 + refresh tokens, Argon2id password hashing, OWASP Top 10 compliance
- Real-time — SignalR for live notifications
┌──────────────────────────────────────────────────────────┐
│ Browser / PWA │
│ React 19 + Vite + MUI v9 (port 3000) │
└────────────────────────────┬─────────────────────────────┘
│ HTTPS / WebSocket (SignalR)
┌────────────────────────────▼─────────────────────────────┐
│ .NET 10 REST API │
│ Clean Architecture — CQRS + MediatR │
│ Versioned: /api/v1/... (port 5000) │
└──────┬──────────────────────────────────┬────────────────┘
│ │
┌──────▼──────┐ ┌────────▼────────┐
│ PostgreSQL │ │ Redis │
│ (port 5433) │ │ (port 6380) │
│ 5 schemas │ │ Cache + Sessions│
└─────────────┘ └──────────────────┘
- The React SPA communicates with the API over HTTPS. Axios handles JWT injection and silent token refresh.
- The API processes commands/queries via MediatR pipelines (validation → authorization → handler → response).
- EF Core 10 maps domain entities to PostgreSQL using Npgsql. Row-Level Security enforces tenant isolation at DB level.
- Redis caches hot data (job listings, master data) and stores rate-limit counters.
- SignalR pushes real-time notifications to connected clients via
NotificationHub. - Email is dispatched asynchronously via SMTP (MailHog in dev, configurable SMTP in production).
| Layer | Technology |
|---|---|
| Runtime | .NET 10 (C# 13) |
| Web Framework | ASP.NET Core 10 |
| Architecture | Clean Architecture, CQRS, DDD |
| Mediator | MediatR 12 |
| ORM | Entity Framework Core 10 |
| DB Driver | Npgsql 10 (PostgreSQL) |
| Auth | JWT RS256 + Refresh Tokens, ASP.NET Identity (custom) |
| Password Hashing | Argon2id (Konscious.Security.Cryptography) |
| Real-time | SignalR |
| Cache | Redis (StackExchange.Redis) |
| SMTP (MailKit) | |
| Validation | FluentValidation |
| Logging | Serilog + structured logs |
| API Docs | Scalar / OpenAPI |
| Layer | Technology |
|---|---|
| Framework | React 19 |
| Build Tool | Vite 6 |
| UI Library | MUI v9 (Material UI) |
| State Management | Zustand |
| Server State | TanStack Query v5 |
| Forms | React Hook Form + Zod validation |
| Routing | React Router v7 |
| HTTP Client | Axios (with JWT interceptors) |
| PWA | vite-plugin-pwa |
| Language | TypeScript 5 |
| Component | Technology |
|---|---|
| Primary DB | PostgreSQL 16 |
| Cache / Sessions | Redis 7 |
| Dev SMTP | MailHog |
| Containerisation | Docker + Docker Compose |
| Web Server | Nginx (frontend) |
| Secret Generation | Custom .NET tool (tools/gen-secrets) |
The backend follows Clean Architecture with strict dependency rules (outer layers depend on inner, never the reverse).
src/backend/src/
├── JobPortal.Domain/ ← Core business logic (no dependencies)
├── JobPortal.Application/ ← Use cases: Commands, Queries, Handlers
├── JobPortal.Infrastructure/ ← EF Core, Redis, Email, SignalR, File Storage
└── JobPortal.API/ ← ASP.NET Core: Controllers, Middleware, DI
Pure C# — no framework dependencies.
Domain/
├── Entities/
│ ├── Auth/ → Tenant, User, Role, Permission, RefreshToken,
│ │ UserSession, AuditLog
│ ├── Portal/ → JobPosting, JobApplication, JobSeekerProfile,
│ │ Company, CandidatePipeline, CandidatePipelineStage,
│ │ InterviewSchedule, OfferLetter, SavedJob,
│ │ JobAlert, Notification, WorkExperience, Education
│ ├── Billing/ → TenantSubscription, SubscriptionInvoice,
│ │ PaymentTransaction, JobCredit, ResumeViewCredit
│ ├── Master/ → Country, State, City, Industry, JobCategory,
│ │ Skill, JobType, ExperienceLevel, EducationLevel
│ └── Config/ → TenantSettings, GlobalSettings, EmailTemplate,
│ FeatureFlag, Announcement, ContentPage
├── Enums/ → ApplicationStatus, JobStatus, PipelineStage, etc.
├── Events/ → Domain events (DomainEventBase, IHasDomainEvents)
├── Exceptions/ → Domain exceptions
└── Common/ → BaseEntity, IAuditableEntity, IHasTenant
CQRS via MediatR. Each feature folder contains Commands, Queries, Validators, and DTOs.
Application/
├── Features/
│ ├── Auth/ → Login, Register, RefreshToken, Logout,
│ │ ForgotPassword, ResetPassword, VerifyEmail,
│ │ TwoFactor
│ ├── Jobs/ → CreateJob, UpdateJob, DeleteJob, GetJob,
│ │ ListJobs, SearchJobs, FeaturedJobs
│ ├── Applications/→ Apply, WithdrawApplication, GetApplication,
│ │ ListApplications, UpdateApplicationStatus
│ ├── Candidates/ → SearchCandidates, GetCandidateProfile
│ ├── Pipeline/ → GetPipeline, MoveCandidateStage, BulkMove
│ ├── Companies/ → CreateCompany, UpdateCompany, GetCompany
│ ├── Interviews/ → ScheduleInterview, UpdateInterview, GetInterview
│ ├── Offers/ → CreateOffer, UpdateOffer, GetOffer
│ ├── Billing/ → GetSubscription, GetInvoices, GetCredits
│ ├── Notifications→ GetNotifications, MarkAsRead, BroadcastToTenant
│ ├── JobAlerts/ → CreateAlert, DeleteAlert, ListAlerts
│ ├── SavedJobs/ → SaveJob, UnsaveJob, ListSavedJobs
│ ├── Dashboard/ → GetJobSeekerDashboard, GetRecruiterDashboard
│ ├── Admin/ → TenantManagement, GlobalSettings, AuditLogs
│ ├── Master/ → Countries, States, Cities, Industries,
│ │ Categories, Skills
│ └── Config/ → TenantSettings, EmailTemplates, FeatureFlags
├── Common/
│ ├── Behaviours/ → ValidationBehaviour, LoggingBehaviour,
│ │ PerformanceBehaviour, AuthorizationBehaviour
│ ├── Interfaces/ → IApplicationDbContext, ICurrentUserService,
│ │ ICacheService, IEmailService, IFileService,
│ │ INotificationService, ITenantService
│ └── Models/ → Result<T>, PagedList<T>, PaginationParams
└── EventHandlers/ → Domain event handlers
Infrastructure/
├── Persistence/
│ ├── ApplicationDbContext.cs → EF Core DbContext
│ ├── Configurations/ → Fluent API entity configs
│ └── Interceptors/ → Audit, soft-delete interceptors
├── Identity/ → JWT generation, Argon2id hashing
├── Services/
│ ├── CurrentUserService → Extracts user from HttpContext
│ ├── TenantService → Resolves current tenant
│ ├── RedisCacheService → IDistributedCache wrapper
│ ├── EmailService → SMTP via MailKit
│ ├── LocalFileStorageService → Resume/logo uploads
│ ├── SignalRNotificationService → Push via NotificationHub
│ └── DateTimeService → UTC clock abstraction
├── Hubs/
│ ├── NotificationHub → SignalR hub
│ └── NotificationUserIdProvider → Maps user ID to SignalR connection
└── Middleware/ → Tenant resolution middleware
API/
├── Controllers/v1/
│ ├── AuthController → /api/v1/auth
│ ├── JobsController → /api/v1/jobs
│ ├── ApplicationsController → /api/v1/applications
│ ├── CandidatesController → /api/v1/candidates
│ ├── CompaniesController → /api/v1/companies
│ ├── PipelineController → /api/v1/pipeline
│ ├── InterviewsController → /api/v1/interviews
│ ├── OffersController → /api/v1/offers
│ ├── BillingController → /api/v1/billing
│ ├── NotificationsController → /api/v1/notifications
│ ├── JobAlertsController → /api/v1/job-alerts
│ ├── SavedJobsController → /api/v1/saved-jobs
│ ├── DashboardController → /api/v1/dashboard
│ ├── MasterController → /api/v1/master
│ ├── ConfigController → /api/v1/config
│ └── AdminController → /api/v1/admin
├── Middlewares/
│ ├── ExceptionHandlingMiddleware
│ └── RequestLoggingMiddleware
├── Extensions/ → DI registration extension methods
└── Program.cs → App bootstrap
Every request flows through:
Request → ValidationBehaviour (FluentValidation)
→ LoggingBehaviour
→ PerformanceBehaviour
→ AuthorizationBehaviour
→ Handler → Response
1. POST /api/v1/auth/login
→ Argon2id verify password
→ Issue JWT (RS256, 15 min) + Refresh Token (SHA-256 hashed, 7 days)
→ Store refresh token hash in DB
2. POST /api/v1/auth/refresh
→ Validate refresh token
→ Rotate: issue new JWT + new Refresh Token
→ Invalidate old refresh token
3. POST /api/v1/auth/logout
→ Revoke refresh token in DB
src/frontend/src/
├── api/
│ └── apiClient.ts → Axios instance: JWT injection + silent refresh
├── features/ → Feature-sliced modules (co-located logic)
│ ├── auth/
│ │ ├── hooks/ → useAuth.ts (mutations), authApi.ts (raw calls)
│ │ └── store/ → authStore.ts (Zustand: user + refreshToken)
│ ├── jobs/
│ ├── applications/
│ ├── pipeline/
│ ├── candidates/
│ ├── companies/
│ ├── billing/
│ ├── notifications/
│ └── ...
├── pages/
│ ├── public/ → LandingPage, JobSearchPage, JobDetailPage
│ ├── auth/ → LoginPage, RegisterPage,
│ │ ForgotPasswordPage, ResetPasswordPage
│ ├── jobseeker/ → Dashboard, Profile, Applications, SavedJobs
│ ├── recruiter/ → Dashboard, PostJob, Pipeline, Candidates
│ ├── company/ → CompanyProfile
│ ├── admin/ → TenantManagement, Settings
│ ├── billing/ → SubscriptionPage
│ ├── interviews/
│ ├── offers/
│ └── notifications/
├── components/
│ ├── ui/ → Button, FormTextField, FormSelect,
│ │ FormCheckbox, LoadingSpinner,
│ │ ErrorAlert, ConfirmDialog
│ └── layout/ → TopNav, MainLayout, AuthLayout
├── router/
│ └── index.tsx → React Router v7: RequireAuth, RequireGuest guards
├── theme/
│ └── index.ts → lightTheme / darkTheme (MUI v9 tokens)
├── types/
│ └── index.ts → All shared TypeScript types / interfaces
├── hooks/ → Shared custom hooks
├── store/ → Global Zustand stores
├── utils/ → Helper functions
└── lib/ → Third-party wrappers
| Concern | Tool |
|---|---|
| Server state (API data, caching, refetch) | TanStack Query v5 |
| Auth state (user, tokens) | Zustand (persisted to localStorage) |
| UI / ephemeral state | React local state |
Public routes → /jobs, /companies, /login, /register
RequireGuest → Redirects authenticated users away from /login, /register
RequireAuth → Redirects unauthenticated users to /login
Role guards → /admin/* (Super Admin), /recruiter/* (Recruiter/Employer)
- Attaches
Authorization: Bearer <token>to every request - On 401 response → silently calls
/auth/refresh→ retries original request - On refresh failure → clears auth store → redirects to login
PostgreSQL 16 with 5 dedicated schemas for clean separation of concerns.
| Schema | Purpose | Key Tables |
|---|---|---|
auth |
Identity, access control, sessions | Tenants, Users, Roles, Permissions, RolePermissions, UserRoles, RefreshTokens, UserSessions, AuditLogs, PasswordResetTokens, EmailVerifications, TwoFactorSettings |
master |
Reference / lookup data | Countries, States, Cities, Industries, JobCategories, JobSubCategories, Skills, JobTypes, ExperienceLevels, EducationLevels, LanguageMaster |
portal |
Core job portal data | Companies, JobPostings, JobSeekerProfiles, WorkExperiences, Educations, JobApplications, SavedJobs, JobAlerts, CandidatePipelines, PipelineStages, InterviewSchedules, OfferLetters, Notifications |
billing |
Subscriptions & payments | TenantSubscriptions, SubscriptionInvoices, PaymentTransactions, JobCredits, ResumeViewCredits |
config |
Platform & tenant config | TenantSettings, GlobalSettings, EmailTemplates, FeatureFlags, Announcements, ContentPages |
All tenant-scoped tables have a tenant_id UUID NOT NULL column.
Row-Level Security (RLS) is enabled on every portal/billing/config table:
-- Example: JobPostings RLS policy
CREATE POLICY tenant_isolation ON portal.JobPostings
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);The API sets SET LOCAL app.current_tenant_id = '<id>' at the start of each request via EF Core interceptors.
- B-tree indexes on all foreign keys and common filter columns
- Full-text search indexes (
tsvector) onJobPostings(title, description, skills) - Composite indexes for pagination queries (e.g.,
(tenant_id, status, created_at)) - Partial indexes on active/non-deleted rows
- Primary keys:
UUID(generated asgen_random_uuid()) - Audit columns:
created_at TIMESTAMPTZ,updated_at TIMESTAMPTZ,created_by UUID,updated_by UUID - Soft-delete:
deleted_at TIMESTAMPTZon applicable tables - Naming:
PascalCasetables,snake_casecolumns
| Service | Image | Port |
|---|---|---|
db |
Custom PostgreSQL 16 (with init scripts) | 5433 |
redis |
redis:7-alpine | 6380 |
api |
.NET 10 API (multi-stage build) | 5000 |
frontend |
React/Nginx (multi-stage build) | 3000 |
mailhog |
mailhog/mailhog (dev profile only) | 8025 (UI) |
# 1. Generate secrets (JWT RSA keys + pepper)
cd tools/gen-secrets
dotnet run
# 2. Copy and fill environment file
cp .env.example .env # set POSTGRES_PASSWORD, REDIS_PASSWORD, etc.
# 3. Start all services (dev — includes MailHog)
docker compose --profile dev up -d --build
# 4. Check MailHog web UI at http://localhost:8025| Variable | Description |
|---|---|
POSTGRES_DB |
Database name (default: jobportal) |
POSTGRES_USER |
DB user (default: jpuser) |
POSTGRES_PASSWORD |
DB password (required) |
REDIS_PASSWORD |
Redis auth password (required) |
JWT_ISSUER |
JWT issuer URL |
JWT_AUDIENCE |
JWT audience URL |
SMTP_HOST |
SMTP server host |
SMTP_PORT |
SMTP server port |
tools/gen-secrets is a .NET utility that generates:
- RSA 2048-bit key pair for JWT RS256 signing
- Argon2id pepper value for password hashing
Output is written to src/backend/src/JobPortal.API/appsettings.Secrets.json (git-ignored, mounted as Docker volume).
- JWT RS256 — asymmetric signing; private key never leaves the API server
- Refresh Token Rotation — every refresh issues a new token and revokes the old one
- Argon2id password hashing with server-side pepper
- Two-Factor Authentication (TOTP) support
- Role-Based Access Control (RBAC) — Roles + Permissions + UserRoles (DB-driven)
| Risk | Mitigation |
|---|---|
| Broken Access Control | RLS policies, JWT claims validation, role guards |
| Cryptographic Failures | RS256 JWT, Argon2id, HTTPS enforced |
| Injection | EF Core parameterised queries, FluentValidation input validation |
| Insecure Design | Clean Architecture separates concerns; domain invariants enforced |
| Security Misconfiguration | Secrets never in image; env vars; separate appsettings.Secrets.json |
| Vulnerable Components | Nuget + npm audits, pinned versions |
| Authentication Failures | Token rotation, revocation, rate limiting (Redis) |
| Integrity Failures | RSA-signed JWTs; Docker image build from source |
| Logging / Monitoring | Serilog structured logs, AuditLogs table, request logging middleware |
| SSRF | No user-controlled HTTP destinations |
- CORS — allowlist-based (configured per environment)
- Rate Limiting — Redis-backed sliding window
- Input Validation — FluentValidation on all commands
- Exception Handling — Global middleware returns RFC 7807 problem details (no stack traces in production)
- Landing page with hero job search, featured jobs, top companies
- Job search with full-text search + filters (location, salary, type, experience, industry)
- SEO-friendly job URLs
- Company profiles and listings
- Profile: personal info, headline, work experience, education, skills, certifications, languages
- Resume upload (PDF/DOC) and built-in resume builder
- One-click apply with custom cover letter
- Application tracker (Kanban or list view) with status timeline
- Saved jobs wishlist
- Job alerts (keyword + filters, email: instant/daily/weekly)
- In-app and push notifications (PWA)
- Company profile management (logo, about, culture)
- Job posting management: rich text description, custom application questions, auto-expire, featured/sponsored toggle
- ATS pipeline: per-job Kanban board (New → Screening → Interview → Offer → Hired/Rejected)
- Candidate search (full-text on resume database, subscription-gated)
- Interview scheduling with calendar invites and multi-round feedback
- Offer letter management
- Team management (invite users, assign roles)
- Reports & analytics: time-to-hire, application funnel, source analytics
- Tenant management (create, suspend, delete tenants)
- Subscription plan management
- Global configuration and settings
- Content management (email templates, announcements, content pages)
- Platform analytics and KPIs
- Audit log viewer
- Docker Desktop (with Compose v2)
- .NET 10 SDK (for running gen-secrets or local dev)
- Node.js 22+ (for local frontend dev)
# Clone the repo
git clone https://github.com/DNVerma88/JOBPORTAL.git
cd JOBPORTAL
# Generate JWT keys and pepper
cd tools/gen-secrets
dotnet run
cd ../..
# Set required environment variables
$env:POSTGRES_PASSWORD="your_strong_password"
$env:REDIS_PASSWORD="your_redis_password"
# Start all services (with MailHog for email testing)
docker compose --profile dev up -d --build| Service | URL |
|---|---|
| Frontend | http://localhost:3000 |
| API | http://localhost:5000 |
| API Docs (Scalar) | http://localhost:5000/scalar |
| MailHog (dev) | http://localhost:8025 |
| PostgreSQL | localhost:5433 |
| Redis | localhost:6380 |
Backend:
cd src/backend
dotnet restore
dotnet build
cd src/JobPortal.API
dotnet runFrontend:
cd src/frontend
npm install
npm run devJobPortal/
├── db/ ← PostgreSQL init scripts
│ ├── schemas/ → Extensions, schemas, types, functions
│ ├── tables/ → auth/, master/, portal/, billing/, config/
│ ├── indexes/ → Index definitions per schema
│ ├── rls/ → Row-Level Security policies
│ └── seed/ → Reference data seeds
├── docs/
│ └── PROJECT_PLAN.md → Full product requirements & planning
├── src/
│ ├── backend/
│ │ └── src/
│ │ ├── JobPortal.Domain/
│ │ ├── JobPortal.Application/
│ │ ├── JobPortal.Infrastructure/
│ │ └── JobPortal.API/
│ └── frontend/
│ └── src/ → React 19 + Vite application
├── tools/
│ └── gen-secrets/ → Secret generation utility
└── docker-compose.yml
| Phase | Status |
|---|---|
| .NET 10 backend — Clean Architecture, all layers, 0 build errors | ✅ Complete |
| PostgreSQL — 100 SQL files: schemas, tables, indexes, RLS, seeds | ✅ Complete |
| Frontend scaffold — Vite + React 19 + MUI v9 + PWA, auth pages | ✅ Complete |
| Job search & listing pages | 🔲 Planned |
| Job detail page | 🔲 Planned |
| Recruiter dashboard (post jobs, manage applications) | 🔲 Planned |
| Job Seeker profile page | 🔲 Planned |
| ATS Kanban board (CandidatePipelines) | 🔲 Planned |
| Real-time notifications (SignalR) | 🔲 Planned |
| Subscription & billing pages | 🔲 Planned |
| Resume builder | 🔲 Planned |
| Interview scheduling | 🔲 Planned |
| Super Admin panel | 🔲 Planned |
This project is private and proprietary.
Built with .NET 10, React 19, PostgreSQL 16, and Redis 7.