Building GigFlow: A Real-Time Freelance Marketplace with Secure Hiring Logic
Source: Dev.to
Introduction
Modern marketplaces aren’t just about CRUD APIs — they’re about correctness, trust, and real‑time feedback. In this post I walk through how I built GigFlow, a full‑stack freelance marketplace where clients can post jobs, freelancers can bid, and hiring happens atomically and in real time.
Project Focus
- Secure authentication
- Correct hiring logic
- Preventing race conditions
- Real‑time updates using Socket.io
GigFlow Overview
- Any authenticated user can post a gig (client role)
- Any user can bid on gigs (freelancer role)
- A client can hire exactly one freelancer per gig; all other bids are automatically rejected
- The hired freelancer receives a real‑time notification
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React (Vite), Tailwind CSS, Context API, Socket.io client, Fetch API with credentials |
| Backend | Node.js + Express, MongoDB + Mongoose, JWT authentication (HttpOnly cookies), Socket.io, MongoDB transactions |
| Deployment | Frontend on Vercel, Backend on Render, Database on MongoDB Atlas |
Authentication
Authentication uses JWT stored in HttpOnly cookies, which:
- Prevents access from JavaScript (XSS‑safe)
- Works cleanly with
credentials: "include"
Each request:
- Verifies the JWT
- Attaches the authenticated user to
req.user
This enables a role‑less design: users can act as both client and freelancer without separate accounts.
Data Models
// User
{
"name": "String",
"email": "String",
"password": "HashedString"
}
// Gig
{
"title": "String",
"description": "String",
"budget": "Number",
"ownerId": "ObjectId",
"status": "open | assigned",
"assignedTo": "ObjectId | null"
}
// Bid
{
"gigId": "ObjectId",
"freelancerId": "ObjectId",
"message": "String",
"price": "Number",
"status": "pending | hired | rejected"
}
Hiring Logic (Core Challenge)
Rule: Only one freelancer can be hired for a gig — ever.
Edge Case
Two clients (or two browser tabs) click Hire at the same time. Without protection, both bids could be marked as hired.
Solution: MongoDB Transaction
- Start a session
- Check if the gig is still open
- Update gig → set
statustoassignedandassignedToto the selected freelancer - Mark selected bid →
hired - Mark all other bids →
rejected - Commit transaction
If any step fails, the transaction rolls back, guaranteeing:
- Exactly one hired freelancer
- No partial or inconsistent state
Real‑Time Notification
When a freelancer is hired:
- The backend emits a
hiredevent to that freelancer’s Socket.io room. - The freelancer’s dashboard updates instantly—no polling, no page refresh.
User Flow
- Create gig → gig status
open - View bids → freelancers submit bids
- Click Hire → gig becomes
assigned - **Freelancer receives real‑time hire notification`
State is kept minimal and predictable using the Context API.
Deployment Considerations
- Set all required environment variables explicitly.
- Configure CORS to allow the deployed frontend domain; Socket.io shares the same CORS settings as Express.
- API base URL must point to the deployed backend.
- Fetch requests must include credentials.
Key pillars
- Correct API design
- Secure authentication
- Transactional integrity
- Real‑time communication
- Production deployment awareness
Live Demo & Source Code
- Live App:
- GitHub Repository:
Final Thoughts
The hardest part of full‑stack development isn’t writing code — it’s ensuring the system behaves correctly when multiple actions occur simultaneously. GigFlow was built with that principle in mind.