- Overview
- Problem Statement
- System Architecture
- Features & Implementation
- Tech Stack
- Advanced Concepts
- Project Structure
- Getting Started
- Environment Variables
- API Reference
- Screenshots
- Contributing
TURFI is a full-stack, real-time, location-aware sports turf booking platform built on the MERN stack. It bridges the gap between players looking for sports grounds and turf owners managing their facilities β creating a digital ecosystem where discovery, booking, communication, and trust all live in one place.
This is not a CRUD app. It is a startup-grade product demonstrating real-world engineering: concurrency handling, event-driven architecture, geospatial queries, real-time sync, payment lifecycles, and multi-role access control.
Search β Discover β Book β Pay β Play
List Turf β Manage Schedule β Communicate β Earn
Verify β Moderate β Analyse β Control
| Pain Point | TURFI's Solution |
|---|---|
| Manual / phone-based bookings | Instant online booking with real-time slot availability |
| No visibility into availability | Live WebSocket-powered slot sync β no double bookings |
| Opaque pricing | Transparent pricing with filters and comparisons |
| Fake or unverified listings | Admin verification system with trust badges |
| No centralized discovery | Location-based search powered by Google Maps & MongoDB $near |
| No owner-player communication | Built-in real-time chat with typing indicators |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT LAYER β
β React.js + Redux Toolkit + TailwindCSS + TanStack Query β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ¬ββββββββββββββββββββ
β REST APIs (JWT Auth) β WebSocket (Socket.IO)
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SERVER LAYER (Node.js + Express) β
β ββββββββββββββ ββββββββββββββ βββββββββββββββ ββββββββββββ β
β β Auth Routesβ βTurf Routes β βBooking Routesβ βChat/WS β β
β β (JWT/RBAC) β β(Geo + CRUD)β β(Pay + Slots) β β(Socket.IOβ β
β ββββββββββββββ ββββββββββββββ βββββββββββββββ ββββββββββββ β
ββββββ¬ββββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββ¬βββββ
β β β β
βΌ βΌ βΌ βΌ
MongoDB Cloudinary Razorpay Redis +
(Geo Index) (Media CDN) (Payments) BullMQ
(Background
Workers)
β
βΌ
Google Maps API
(Geocoding + Display)
PLAYER β Search | Book | Chat | Review | Pay
OWNER β List | Schedule | Chat | Analytics | Refund
ADMIN β Verify | Moderate | Dispute | Platform Stats
What it does: Shows live slot availability; prevents double-bookings across concurrent users.
How it's implemented:
- Socket.IO room-based architecture: each turf has its own socket room (
turf:{id}) - When a user starts the booking flow, a slot lock is acquired (stored in Redis with a TTL of ~5 minutes)
- All clients in the turf room receive an instant
slot:lockedorslot:confirmedevent - If payment fails or session expires, the lock is released and slots re-open in real-time
// Server-side slot locking
io.on("connection", (socket) => {
socket.on("lock:slot", async ({ turfId, slotId, userId }) => {
const lockKey = `lock:${turfId}:${slotId}`;
const locked = await redis.set(lockKey, userId, "EX", 300, "NX");
if (locked) {
io.to(`turf:${turfId}`).emit("slot:locked", { slotId });
} else {
socket.emit("slot:unavailable", { slotId });
}
});
});Concepts demonstrated: Concurrency control, distributed locking (Redis), real-time event propagation
What it does: Finds turfs near the user's current location and sorts by distance, price, and rating.
How it's implemented:
- Turf locations are stored as GeoJSON Point objects in MongoDB
- 2dsphere index is created on the
locationfield for performant geospatial queries - The frontend requests the user's coordinates via the Geolocation API, then passes them to the backend
- Google Maps API is used to render turf pins, display routes, and show autocomplete for location search
// MongoDB geospatial query
const turfs = await Turf.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [lng, lat] },
$maxDistance: radiusInMeters,
},
},
isVerified: true,
}).populate("owner", "name avatar");// Turf schema with geospatial index
const turfSchema = new Schema({
location: {
type: { type: String, default: "Point" },
coordinates: [Number], // [longitude, latitude]
},
});
turfSchema.index({ location: "2dsphere" });Concepts demonstrated: MongoDB geospatial indexing, Google Maps integration, browser Geolocation API
What it does: Enables direct messaging between players and owners for inquiries, negotiation, and support.
How it's implemented:
- Socket.IO private rooms per conversation (
chat:{userId1}:{userId2}) - Messages are persisted in MongoDB with
readStatus,deliveredAt, andseenAtfields - Typing indicators broadcast via
user:typinganduser:stop-typingevents with a debounce - On reconnect, Socket.IO auto-joins the user back to their active rooms using stored session data
- Chat history is fetched via REST on initial load; subsequent messages arrive over the socket
socket.on("message:send", async (data) => {
const message = await Message.create(data);
io.to(data.roomId).emit("message:receive", message);
});
socket.on("typing:start", ({ roomId }) => {
socket.to(roomId).emit("user:typing");
});Concepts demonstrated: Event-driven architecture, persistent socket sessions, message delivery guarantees
What it does: Surfaces the most relevant turfs based on user context and behavior.
How it's implemented:
- Rule-based engine using a weighted scoring formula:
score = (1/distance Γ 0.4) + (avgRating Γ 0.35) + (priceScore Γ 0.25)
- User history (viewed turfs, booked turfs, preferred sports) is tracked in the user document
- Recommendations are refreshed on each session using an aggregation pipeline
- Architecture is designed to be swapped with an ML model (collaborative filtering) later without changing the API contract
const recommended = await Turf.aggregate([
{ $geoNear: { near: userLocation, distanceField: "dist.calculated", spherical: true }},
{ $addFields: { score: { $add: [
{ $multiply: [{ $divide: [1, "$dist.calculated"] }, 0.4] },
{ $multiply: ["$avgRating", 0.07] },
]}}},
{ $sort: { score: -1 } },
{ $limit: 10 },
]);Concepts demonstrated: MongoDB aggregation pipelines, scoring algorithms, extensible ML-ready design
What it does: Handles secure payment collection and confirmation, with refund and invoice support.
How it's implemented:
- Order creation on the server using Razorpay's Node SDK; order ID returned to frontend
- Payment verification using HMAC SHA-256 signature validation on the server β the booking is only confirmed after signature is verified
- Webhooks capture asynchronous payment events (success, failure, refund) from Razorpay
- On successful payment, a booking record is created and the slot lock is promoted to a confirmed booking
- Invoices are generated as PDFs using
pdfkitand stored on Cloudinary
// Server-side payment verification
const generatedSignature = crypto
.createHmac("sha256", process.env.RAZORPAY_SECRET)
.update(`${razorpay_order_id}|${razorpay_payment_id}`)
.digest("hex");
if (generatedSignature === razorpay_signature) {
await Booking.findByIdAndUpdate(bookingId, { status: "confirmed" });
io.to(`turf:${turfId}`).emit("slot:confirmed", { slotId });
}Concepts demonstrated: Payment lifecycle management, HMAC signature verification, webhook handling
What it does: Delivers booking confirmations, upcoming match reminders, and cancellation alerts via in-app and email channels.
How it's implemented:
- BullMQ job queues (backed by Redis) handle all async notification dispatch
- A scheduled job runs 2 hours before each booking and sends a reminder
- In-app notifications are pushed via Socket.IO to the user's session room
- Email notifications use Nodemailer with HTML templates
// Schedule a reminder job at booking time
await reminderQueue.add(
"match-reminder",
{ userId, bookingId, turfName, slotTime },
{ delay: slotTime - Date.now() - 2 * 60 * 60 * 1000 } // 2h before
);
// Worker processes the job
reminderWorker.process(async (job) => {
await sendEmail(job.data);
io.to(`user:${job.data.userId}`).emit("notification:new", { ... });
});Concepts demonstrated: Background job queues, event scheduling, multi-channel notification systems
What it does: Owners submit documents (ownership proof, facility photos); an admin manually reviews and approves before the listing goes live.
How it's implemented:
- Documents and facility images are uploaded to Cloudinary via secure signed uploads
- A
verificationStatusfield (pending | approved | rejected) controls turf visibility - Admins see a dedicated dashboard queue of pending verifications
- Approved turfs receive a Trust Badge (stored as a boolean flag) visible to all users
// Turf model
verificationStatus: {
type: String,
enum: ["pending", "approved", "rejected"],
default: "pending",
},
documents: [{ url: String, public_id: String, type: String }],
isTrusted: { type: Boolean, default: false },Concepts demonstrated: Multi-step admin workflows, role-based access control, Cloudinary secure upload
What it does: Post-booking reviews with star ratings; verified bookings unlock the ability to review.
How it's implemented:
- Reviews are only permitted after a booking status is
completedβ enforced server-side avgRatingandtotalReviewsare maintained on the Turf document using MongoDB's$avgaggregation and a post-save hook- Helpful votes on reviews surface the most useful content
- Flagging system routes suspicious reviews to admin moderation queue
// Recompute average rating after each review
turfSchema.post("save", async function () {
const stats = await Review.aggregate([
{ $match: { turf: this._id } },
{ $group: { _id: "$turf", avg: { $avg: "$rating" }, count: { $sum: 1 } }},
]);
await Turf.findByIdAndUpdate(this._id, {
avgRating: stats[0]?.avg ?? 0,
totalReviews: stats[0]?.count ?? 0,
});
});Concepts demonstrated: Post-save middleware, aggregation pipelines, trust signal systems
What it does: Gives owners insight into bookings, revenue, and peak hours; gives admins a platform-wide view.
How it's implemented:
- Owner dashboard: Revenue charts (daily/weekly/monthly), booking count, peak-hour heatmap
- Admin dashboard: Total platform GMV, new user signups, pending verifications, dispute rate
- All analytics are computed using MongoDB aggregation pipelines β no separate analytics DB required
- Charts are rendered with Recharts on the frontend; data is pre-aggregated server-side for performance
// Peak hours aggregation
const peakHours = await Booking.aggregate([
{ $match: { turf: turfId, status: "completed" } },
{ $group: { _id: { $hour: "$slotStart" }, count: { $sum: 1 } } },
{ $sort: { count: -1 } },
]);Concepts demonstrated: Data aggregation, analytics API design, time-series visualization
What it does: Handles booking cancellations with configurable refund policies, and a dispute resolution flow for admin.
How it's implemented:
- Cancellation policy is stored per turf (e.g., full refund > 24h before, 50% within 24h, no refund within 2h)
- On cancellation, a Razorpay refund is initiated programmatically via the API
- Disputes (raised when a player is denied entry, or a turf is misrepresented) are submitted with evidence and routed to admin
- Refund status is tracked and surfaced to the user in real-time via Socket events
const policy = await Turf.findById(turfId).select("cancellationPolicy");
const hoursLeft = (booking.slotStart - Date.now()) / 3600000;
const refundPct = hoursLeft > 24 ? 1 : hoursLeft > 2 ? 0.5 : 0;
const refundAmount = booking.amountPaid * refundPct;
if (refundAmount > 0) {
await razorpay.payments.refund(booking.paymentId, { amount: refundAmount * 100 });
}Concepts demonstrated: Business rule engines, payment refund APIs, dispute lifecycle management
β
Real-Time Synchronization β Socket.IO rooms, slot locking via Redis
β
Role-Based Access Control β User | Owner | Admin middleware guards
β
Geospatial Querying β MongoDB 2dsphere index + $near operator
β
Event-Driven Architecture β BullMQ queues for notifications and emails
β
Concurrent Booking Prevention β Distributed Redis lock with TTL
β
Secure Authentication β JWT access + refresh token rotation
β
Payment Lifecycle Handling β Order β Verify β Confirm β Refund
β
Scalable API Design β RESTful, versioned, paginated APIs
β
Media Pipeline β Cloudinary signed uploads, auto-optimization
β
Analytics via Aggregations β MongoDB pipelines β no extra analytics service
turfi/
βββ client/ # React frontend
β βββ src/
β β βββ components/ # Reusable UI components
β β β βββ Map/ # Google Maps integration
β β β βββ Chat/ # Real-time chat UI
β β β βββ Booking/ # Slot picker, payment flow
β β βββ pages/
β β β βββ User/ # Player-facing pages
β β β βββ Owner/ # Owner dashboard
β β β βββ Admin/ # Admin panel
β β βββ store/ # Redux slices
β β βββ hooks/ # Custom React hooks
β β βββ socket/ # Socket.IO client setup
β βββ package.json
β
βββ server/ # Node.js + Express backend
β βββ src/
β β βββ controllers/ # Route handler logic
β β βββ models/ # Mongoose schemas
β β β βββ User.js
β β β βββ Turf.js # GeoJSON location field
β β β βββ Booking.js
β β β βββ Message.js
β β β βββ Review.js
β β βββ routes/ # Express route definitions
β β βββ middleware/ # Auth, RBAC, error handlers
β β βββ socket/ # Socket.IO event handlers
β β β βββ chat.js
β β β βββ booking.js
β β βββ queues/ # BullMQ workers & jobs
β β β βββ reminderQueue.js
β β β βββ emailQueue.js
β β βββ utils/ # Helpers (geo, payment, cloudinary)
β β βββ config/ # DB, Redis, Cloudinary config
β βββ package.json
β
βββ README.md
node >= 18.x
npm >= 9.x
MongoDB Atlas or local MongoDB instance
Redis (local or Upstash)
Razorpay account (test keys)
Google Maps API key
Cloudinary account# 1. Clone the repository
git clone https://github.com/yourusername/turfi.git
cd turfi
# 2. Install server dependencies
cd server && npm install
# 3. Install client dependencies
cd ../client && npm install# Terminal 1 β Start Redis
redis-server
# Terminal 2 β Start the backend server
cd server
cp .env.example .env # Fill in your environment variables
npm run dev
# Terminal 3 β Start the React frontend
cd client
npm run devOpen http://localhost:5173 in your browser.
# App
PORT=5000
NODE_ENV=development
CLIENT_URL=http://localhost:5173
# MongoDB
MONGODB_URI=mongodb+srv://<user>:<password>@cluster.mongodb.net/turfi
# JWT
JWT_SECRET=your_jwt_secret_key
JWT_REFRESH_SECRET=your_refresh_secret
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# Redis
REDIS_URL=redis://localhost:6379
# Cloudinary
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
# Razorpay
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxx
RAZORPAY_SECRET=your_razorpay_secret
# Google Maps (server-side geocoding)
GOOGLE_MAPS_API_KEY=your_google_maps_key
# Email (Nodemailer)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your@gmail.com
SMTP_PASS=your_app_passwordVITE_API_URL=http://localhost:5000/api
VITE_SOCKET_URL=http://localhost:5000
VITE_GOOGLE_MAPS_API_KEY=your_google_maps_key
VITE_RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxx| Method | Endpoint | Description |
|---|---|---|
POST |
/api/auth/register |
Register new user/owner |
POST |
/api/auth/login |
Login, returns JWT pair |
POST |
/api/auth/refresh |
Refresh access token |
POST |
/api/auth/logout |
Invalidate refresh token |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/turfs?lat=&lng=&radius= |
Geo-search nearby turfs |
GET |
/api/turfs/:id |
Get turf details |
POST |
/api/turfs |
Create turf listing (Owner) |
PUT |
/api/turfs/:id |
Update turf (Owner) |
GET |
/api/turfs/:id/slots?date= |
Get available slots |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/bookings |
Create booking + Razorpay order |
POST |
/api/bookings/verify |
Verify payment signature |
GET |
/api/bookings/my |
Get user's bookings |
POST |
/api/bookings/:id/cancel |
Cancel with refund |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/admin/turfs/pending |
Pending verifications |
PUT |
/api/admin/turfs/:id/verify |
Approve/reject turf |
GET |
/api/admin/stats |
Platform analytics |
GET |
/api/admin/disputes |
All open disputes |
(Coming soon β UI in active development)
| Page | Description |
|---|---|
| πΊοΈ Discovery Map | Google Maps view with turf pins, filter panel |
| π Slot Picker | Calendar with real-time slot availability |
| π¬ Chat Window | Real-time owner-player messaging |
| π§βπΌ Owner Dashboard | Revenue charts, booking list, turf management |
| π‘οΈ Admin Panel | Verification queue, dispute board, platform stats |
Contributions are welcome! Please follow these steps:
# 1. Fork the repository
# 2. Create a feature branch
git checkout -b feature/your-feature-name
# 3. Commit changes
git commit -m "feat: add your feature description"
# 4. Push and open a PR
git push origin feature/your-feature-namePlease follow Conventional Commits for commit messages.
This project is licensed under the MIT License β see the LICENSE file for details.
Built with β€οΈ using the MERN Stack
TURFI β Discover. Book. Play.