Monorepo structure
package.json, node_modules, and .env. They are deployed independently — the Frontend to Vercel, and the Backend to any Node.js-compatible host.
Frontend
The Frontend is a React 18 SPA built with Vite and TypeScript. It communicates with the Backend exclusively over the network — there is no server-side rendering.- Routing: React Router v7
- State management: Redux Toolkit with
redux-persistfor session hydration - HTTP: Axios, with a shared
axiosInstancethat readsVITE_API_URLfor the base URL - Real-time: Socket.io client, connected to
VITE_API_URL - Payments: Stripe.js loaded via
@stripe/react-stripe-js - UI: TailwindCSS, Shadcn (Radix UI primitives), MUI, Framer Motion
- Deployment: Vercel; a
vercel.jsonrewrite rule routes all paths toindex.htmlso React Router handles navigation
Backend
The Backend is an Express 4 server written in TypeScript. It follows the Repository Pattern, separating HTTP concerns from business logic and data access into distinct layers.- HTTP framework: Express 4
- Language: TypeScript, compiled to
dist/withtsc - Process manager:
nodemonfor development hot-reload - Authentication: JWT access + refresh tokens issued as HTTP-only cookies
- Real-time: Socket.io attached to the same Node.js
http.Serverinstance as Express - File uploads: Multer with
multer-storage-cloudinary; files go directly to Cloudinary - Logging: Winston with
winston-daily-rotate-fileand Morgan HTTP request logging
Repository pattern
The Backend enforces a four-layer architecture to keep each concern isolated:src/.
Database
MongoDB is the primary data store, accessed through Mongoose. The application uses the following collections:| Collection | Model file | Purpose |
|---|---|---|
users | userModel.ts | Shared user accounts (all roles) |
clients | clientModel.ts | Client profile data |
freelancers | freelancerModel.ts | Freelancer profile and skills |
jobs | jobModel.ts | Job postings created by clients |
applications | applicationModel.ts | Freelancer proposals on jobs |
contracts | contractModel.ts | Active contracts between client and freelancer |
reviews | reviewMode.ts | Post-contract reviews |
wallets | walletModel.ts | User wallet balances |
escrows | escrowModel.ts | Funds held in escrow during active contracts |
conversations | conversationModel.ts | Chat conversation threads |
messages | messageModel.ts | Individual chat messages |
notifications | notificationModel.ts | In-app notifications |
categories | categoryModel.ts | Job categories (managed by admin) |
skills | skillsModel.ts | Skills taxonomy (managed by admin) |
Real-time (Socket.io)
Socket.io is initialised on the same HTTP server as Express and handles real-time chat between clients and freelancers. The socket server is started after the Express server begins listening. Events are emitted for:- New message delivery
- Message read receipts
- Online/offline presence
VITE_API_URL).
Authentication
Authentication uses a dual-token JWT pattern:- Access token — short-lived, signed with
JWT_SECRET, sent as an HTTP-only cookie - Refresh token — longer-lived, signed with
REFRESH_SECRET, used to issue new access tokens without re-login
@react-oauth/google on the Frontend and google-auth-library on the Backend. The Google client ID is shared between both sides via VITE_GOOGLE_CLIENT_ID (Frontend) and can be verified server-side.
Payments and escrow
Stripe handles all payment processing:- The client creates a contract and pays upfront via Stripe. Funds are held in an escrow record in MongoDB.
- The client approves the work after delivery.
- An admin reviews and releases payment to the freelancer’s wallet.
- Stripe webhook events confirm payment status. The Backend exposes a
/webhookroute that receives raw request bodies and verifies the signature usingSTRIPE_WEBHOOK_SECRET.
File storage
Cloudinary stores all user-uploaded media:- Profile images (resized to 500×500 on upload)
- Chat media (images and videos up to 100 MB)
multer-storage-cloudinary. No files are written to the server’s local filesystem.
Email and OTP
Nodemailer sends transactional emails (OTP verification, password reset) using the credentials inEMAIL_USER and EMAIL_PASS. OTP codes are stored in Redis with a TTL so they expire automatically without requiring database writes.
