A full-stack web application where users can create events and RSVP to them, with strict capacity enforcement and race condition handling to prevent overbooking.
- Project Overview
- Tech Stack
- Features
- How to Run Locally
- Deployed URLs
- RSVP Capacity & Concurrency Explanation
- API Endpoints
- Project Structure
This is a production-ready MERN (MongoDB, Express.js, React.js, Node.js) application that allows users to:
- Create accounts and authenticate using JWT
- Create events with details like title, description, date/time, location, capacity, and images
- Browse upcoming events with search and date filtering
- RSVP to events with strict capacity enforcement
- View personal dashboard showing created events and events they're attending
The application ensures data integrity and prevents overbooking even under concurrent access scenarios using MongoDB atomic operations and transactions.
- Node.js - Runtime environment
- Express.js - Web framework
- MongoDB - Database (MongoDB Atlas)
- Mongoose - MongoDB ODM
- JWT - Authentication
- Multer - File upload handling
- Cloudinary - Image storage (optional, can use local storage)
- React.js - UI library
- React Router - Routing
- Axios - HTTP client
- Tailwind CSS - Styling
- Vite - Build tool
- Backend: Render or Railway
- Frontend: Vercel or Netlify
- Database: MongoDB Atlas
- User signup with password hashing (bcrypt)
- User login with JWT tokens
- Protected routes
- Token-based authentication
- Create events with all required fields
- View all upcoming events
- View individual event details
- Edit/Delete events (creator only)
- Image upload support
- Search events by title
- Filter events by date
- RSVP to events with capacity enforcement
- Leave events (cancel RSVP)
- View RSVP status
- Real-time remaining seats display
- Race condition handling to prevent overbooking
- View all events created by the user
- View all events the user is attending
- Node.js (v18 or higher)
- MongoDB Atlas account (or local MongoDB)
- Cloudinary account (optional, for image storage)
- Navigate to the server directory:
cd server- Install dependencies:
npm install- Create a
.envfile in theserverdirectory:
PORT=5000
MONGODB_URI=your_mongodb_atlas_connection_string
JWT_SECRET=your_jwt_secret_key_here
CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
CLOUDINARY_API_KEY=your_cloudinary_api_key
CLOUDINARY_API_SECRET=your_cloudinary_api_secret- Start the server:
npm run devThe backend will run on http://localhost:5000
- Navigate to the client directory:
cd client- Install dependencies:
npm install- Create a
.envfile in theclientdirectory:
VITE_API_URL=http://localhost:5000/api- Start the development server:
npm run devThe frontend will run on http://localhost:5173 (or another port if 5173 is taken)
If you don't want to use Cloudinary:
- You can skip the Cloudinary environment variables in the backend
.env - The application will still work, but images won't be uploaded (imageUrl will be empty)
- Frontend: [To be deployed on Vercel/Netlify]
- Backend: [To be deployed on Render/Railway]
- MongoDB: MongoDB Atlas
- Push your code to GitHub
- Create a new Web Service on Render (or Railway)
- Connect your GitHub repository
- Set build and start commands:
- Build Command:
cd server && npm install - Start Command:
cd server && npm start
- Build Command:
- Add environment variables:
PORT(usually auto-set by platform)MONGODB_URI(your MongoDB Atlas connection string)JWT_SECRET(a secure random string)CLOUDINARY_CLOUD_NAME(optional)CLOUDINARY_API_KEY(optional)CLOUDINARY_API_SECRET(optional)
- Deploy!
- Push your code to GitHub
- Create a new project on Vercel (or Netlify)
- Connect your GitHub repository
- Set build settings:
- Root Directory:
client - Build Command:
npm run build - Output Directory:
dist
- Root Directory:
- Add environment variables:
VITE_API_URL(your deployed backend URL, e.g.,https://your-backend.onrender.com/api)
- Deploy!
Important: Make sure to update the VITE_API_URL in your frontend environment variables to point to your deployed backend URL.
When multiple users try to RSVP to an event simultaneously, there's a risk of overbooking. This is a classic race condition problem:
Example Scenario:
- Event capacity: 10
- Current attendees: 9
- Two users (User A and User B) try to RSVP at exactly the same time
Without proper handling:
- Both requests check the current attendee count (9)
- Both see that there's space available (10 - 9 = 1 seat left)
- Both requests proceed to add the user
- Result: 11 attendees (OVERBOOKING!)
A race condition occurs when:
- Multiple operations execute concurrently
- They access and modify the same shared resource (the event's attendees array)
- The final result depends on the timing of these operations
- Without synchronization, incorrect results can occur (overbooking)
This application uses MongoDB atomic operations combined with transactions to ensure data integrity:
The RSVP endpoint uses findOneAndUpdate with atomic operators and a transaction session:
const updatedEvent = await Event.findOneAndUpdate(
{
_id: eventId,
$expr: { $lt: [{ $size: '$attendees' }, '$capacity'] }, // Atomic capacity check
attendees: { $ne: userId }, // Atomic duplicate check
},
{
$addToSet: { attendees: userId }, // Atomic add operation
},
{
new: true,
session: session,
}
);-
Atomic Condition Check:
$expr: { $lt: [{ $size: '$attendees' }, '$capacity'] }ensures that the update only happens ifattendees.length < capacity- This check happens atomically at the database level, not in application code
-
Atomic Duplicate Prevention:
attendees: { $ne: userId }ensures the user is not already in the attendees array- Prevents duplicate RSVPs
-
Atomic Add Operation:
$addToSetatomically adds the user to the attendees array only if they're not already present- All operations (check + update) happen as a single atomic operation
-
Transaction Session:
- Wraps the operation in a MongoDB transaction
- Ensures all-or-nothing execution
- If the update fails (capacity exceeded or duplicate), the transaction is aborted
- Single Atomic Operation: MongoDB evaluates the query condition and performs the update as a single atomic operation at the database level
- No Read-Modify-Write Gap: Traditional approaches (read count, check, then write) have a gap between read and write where race conditions can occur. Atomic operations eliminate this gap
- Database-Level Enforcement: The capacity check happens in the database query itself, not in application logic, making it immune to race conditions
- Transactional Safety: The transaction ensures that if any part fails, the entire operation is rolled back
Even if 100 users try to RSVP simultaneously when only 1 seat is left:
- Only one request will succeed (the one that reaches the database first)
- All other requests will receive a 409 Conflict response indicating the event is full
- No overbooking can occur
To test the race condition handling:
- Create an event with capacity: 1
- Use tools like
curlor Postman to send multiple RSVP requests simultaneously - Verify that only 1 RSVP succeeds, and all others return appropriate error messages
POST /api/auth/signup- Register a new userPOST /api/auth/login- Login userGET /api/auth/me- Get current user (protected)
GET /api/events- Get all upcoming events (supports?search=...&date=...)GET /api/events/:id- Get single eventPOST /api/events- Create event (protected)PUT /api/events/:id- Update event (protected, creator only)DELETE /api/events/:id- Delete event (protected, creator only)GET /api/events/user/dashboard- Get user's events (protected)
POST /api/events/:id/rsvp- RSVP to event (protected)DELETE /api/events/:id/rsvp- Leave event (protected)
.
├── server/ # Backend
│ ├── config/ # Configuration files
│ │ ├── database.js # MongoDB connection
│ │ └── cloudinary.js # Cloudinary setup
│ ├── controllers/ # Route handlers
│ │ ├── authController.js
│ │ ├── eventController.js
│ │ └── rsvpController.js
│ ├── middleware/ # Middleware functions
│ │ └── auth.js # JWT authentication
│ ├── models/ # Mongoose models
│ │ ├── User.js
│ │ └── Event.js
│ ├── routes/ # Express routes
│ │ ├── authRoutes.js
│ │ └── eventRoutes.js
│ ├── utils/ # Utility functions
│ │ └── generateToken.js
│ ├── app.js # Express app setup
│ └── server.js # Server entry point
│
└── client/ # Frontend
├── src/
│ ├── components/ # Reusable components
│ │ ├── Navbar.jsx
│ │ └── PrivateRoute.jsx
│ ├── context/ # React Context
│ │ └── AuthContext.jsx
│ ├── pages/ # Page components
│ │ ├── Login.jsx
│ │ ├── Signup.jsx
│ │ ├── Events.jsx
│ │ ├── EventDetail.jsx
│ │ ├── CreateEvent.jsx
│ │ └── Dashboard.jsx
│ ├── utils/ # Utilities
│ │ └── api.js # Axios configuration
│ ├── App.jsx # Main app component
│ └── main.jsx # Entry point
└── ...
- Password hashing with bcrypt
- JWT token-based authentication
- Protected routes (frontend and backend)
- Input validation
- Environment variables for sensitive data
- CORS configuration
- Error handling middleware
- Fully responsive design (mobile, tablet, desktop)
- Clean and professional layout
- Real-time capacity display
- Visual progress bars for event capacity
- Search and filter functionality
- User-friendly error messages
- The application uses MongoDB Atlas for database hosting
- Image uploads are handled via Cloudinary (optional)
- All API endpoints return JSON responses
- Frontend communicates with backend via Axios with interceptors for authentication
- Email notifications for RSVPs
- Event categories and tags
- User profiles
- Event comments and ratings
- Calendar integration
- Social sharing features
Built with the MERN stack