[ 01 ] — Context
Problem
- Turf owners in Dhaka booked slots over phone and WhatsApp — double-booking was routine, availability was never current, and players had no way to compare grounds.
- Timezones and 24-hour clock confusion made 'is this slot free right now' a genuinely hard query to answer correctly.
[ 02 ] — Response
Solution
LedgerTurf is a monorepo (npm workspaces) with an Express REST API and a React SPA. Players discover turfs on a geo-indexed map, owners publish and manage slots, and admins get full visibility — with JWT auth and role-based access at every layer.
Slots are protected with an overlap check inside a Mongoose transaction session, and availability is computed in UTC with Bangladesh's UTC+6 offset handled explicitly — so 'available now' means the same thing to everyone.
SPA deep links are handled with Vercel rewrite rules so refreshing /turf/:id never 404s.
[ 03 ] — Structure
Architecture
React SPA (Vite + Redux) ──► /api/v1 ──► Express (JWT bearer)
│ asyncHandler pattern
├──► Mongo 2dsphere geo index
├──► Booking overlap check
│ (transaction session)
├──► Review avg-rating hooks
└──► Cloudinary uploads
Vercel: frontend + backend, SPA rewrite rules[ 04 ] — Trade-offs
Key engineering decisions
- 01
GeoJSON 2dsphere index
Turf locations are stored as GeoJSON and indexed with MongoDB's 2dsphere index for radius-based discovery — the spatial query is commented out in production for Vercel stability but the index and model are in place.
- 02
Explicit UTC+6 handling
Past-date validation and 'availableNow' both compute against a single UTC clock offset, eliminating the midnight-rollover class of bugs common to BD booking apps.
- 03
Npm workspaces monorepo
frontend, backend, and an api-shim share the repo so type shapes stay in sync while each package deploys independently to Vercel.
[ 05 ] — Under the hood
Engineering highlights
const session = await mongoose.startSession();
session.startTransaction();
try {
const clash = await Booking.findOne({
turf, startTime: { $lt: end },
endTime: { $gt: start },
}).session(session);
if (clash) throw new BookingConflictError();
await Booking.create([{ turf, start, end }], { session });
await session.commitTransaction();
} finally { session.endSession(); }[ 06 ] — Outcomes
Results
[ 08 ] — Where it's going
Status & next steps
- Unique partial index on (turf, startTime) to make double-booking impossible at the DB level.
- Re-enable the geo-search path now that Vercel configuration is stable.
