A real-time calendar event management and slot-swapping application built with React, Node.js, and WebSocket for live notifications.
SlotSwapper is a collaborative calendar application that allows users to manage their schedules and swap time slots with other users in real-time. The application solves the common problem of scheduling conflicts by enabling users to propose and accept slot swaps, making schedule coordination effortless.
-
Real-time Communication
- Implemented WebSocket for instant notifications instead of polling
- WebSocket runs on the same port as HTTP server for simplified deployment
- Connection-based user tracking ensures notifications reach the right users
-
Authentication Strategy
- Dual authentication: Traditional email/password + Google OAuth
- JWT tokens
- Email OTP verification adds an extra security layer for email signups
- Rate limiting on auth endpoints prevents brute-force attacks
-
Database Design
- Prisma ORM for type-safe database queries
- Enum-based status tracking (
BUSY,SWAPPABLE,SWAP_PENDING) - Separate
SwapRequesttable to track all swap transactions - Automatic cleanup via cron jobs to maintain database hygiene
-
Frontend Architecture
- Component-based React with TypeScript for type safety
- Toast notifications replace blocking alerts for better UX
- Custom confirmation modals with state management
- Double-submit prevention using synchronous ref locks
- Protected routes with automatic redirect to login
-
API Design
- RESTful endpoints with clear resource-based routing
- Consistent response structure (
ApiResponseutility) - Centralized error handling middleware
- Rate limiting per endpoint based on sensitivity
-
DevOps & Deployment
- Docker multi-stage builds for optimized image sizes
- Docker Compose for local development with PostgreSQL
- Separate frontend (Vercel) and backend (Render) hosting for scalability
- Environment-based configuration for dev/staging/prod
- Event Management: Create, update, and delete calendar events
- Slot Swapping: Request to swap time slots with other users
- Real-time Notifications: WebSocket-powered instant notifications for swap requests and responses
- Google OAuth: Secure authentication with Google Sign-In
- OTP Verification: Email-based OTP for account verification
- Rate Limiting: Protected endpoints to prevent abuse
- Responsive UI: Modern, mobile-friendly interface with Tailwind CSS
- Toast Notifications: User-friendly feedback for all actions
- Dockerized: Easy deployment with Docker and Docker Compose
- Project Overview
- Features
- Tech Stack
- Project Structure
- Prerequisites
- Quick Start Guide
- Configuration
- Running Locally
- Docker Deployment
- API Documentation
- Features Deep Dive
- Deployment
- Assumptions & Challenges
- License
- React 19 - UI library
- TypeScript - Type safety
- Vite - Build tool and dev server
- Tailwind CSS 4 - Styling
- React Router - Client-side routing
- Axios - HTTP client
- WebSocket - Real-time communication
- Node.js - Runtime environment
- Express.js - Web framework
- TypeScript - Type safety
- Prisma - ORM for database management
- PostgreSQL - Primary database
- WebSocket (ws) - Real-time notifications
- JWT - Authentication tokens
- Nodemailer - Email service for OTP
- bcrypt - Password hashing
- express-rate-limit - Rate limiting middleware
- Docker - Containerization
- Docker Compose - Multi-container orchestration
- Render - Cloud deployment platform
SlotSweeper/
├── backend/
│ ├── src/
│ │ ├── controller/ # Route handlers
│ │ │ ├── auth.controller.ts
│ │ │ ├── event.controller.ts
│ │ │ └── swap.controller.ts
│ │ ├── routes/ # Express routes
│ │ │ ├── auth.route.ts
│ │ │ ├── event.route.ts
│ │ │ └── swap.route.ts
│ │ ├── middleware/ # Custom middleware
│ │ │ ├── auth.middleware.ts
│ │ │ ├── error.middleware.ts
│ │ │ └── rateLimit.middleware.ts
│ │ ├── websocket/ # WebSocket server
│ │ │ └── websocket.ts
│ │ ├── jobs/ # Scheduled tasks
│ │ │ └── cronJobs.ts # Automated cleanup jobs
│ │ ├── utilities/ # Helper functions
│ │ │ ├── ApiError.ts
│ │ │ ├── ApiResponse.ts
│ │ │ ├── asynchandler.ts
│ │ │ ├── prisma.ts
│ │ │ └── sendOTP.ts
│ │ └── index.ts # App entry point
│ ├── prisma/
│ │ ├── schema.prisma # Database schema
│ │ └── migrations/ # Database migrations
│ ├── .env # Environment variables
│ ├── package.json
│ ├── tsconfig.json
│ └── prisma.config.ts
├── frontend/
│ ├── src/
│ │ ├── components/ # React components
│ │ │ ├── MyCalendar.tsx
│ │ │ ├── SwapMarketplace.tsx
│ │ │ ├── Notifications.tsx
│ │ │ ├── ProtectedRoute.tsx
│ │ │ └── ui/ # UI components
│ │ ├── pages/ # Page components
│ │ │ ├── Dashboard.tsx
│ │ │ ├── LoginPage.tsx
│ │ │ └── SignUpPage.tsx
│ │ ├── types/ # TypeScript types
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── public/
│ ├── package.json
│ ├── vite.config.ts
│ └── tsconfig.json
├── docker/
│ ├── Dockerfile.backend # Backend Docker image
│ └── Dockerfile.frontend # Frontend Docker image
├── docker-compose.yml # Multi-container setup
└── README.md
- Node.js >= 20.x
- npm >= 10.x
- PostgreSQL >= 14.x (or use Docker)
- Docker & Docker Compose (optional, for containerized deployment)
- Gmail Account (for SMTP/OTP emails)
- Google Cloud Project (for OAuth, optional but recommended)
Follow these steps to get SlotSwapper running locally in under 10 minutes:
# Clone the repository
git clone https://github.com/Ankitsinghsisodya/SlotSwapper.git
cd SlotSwapper
# Install backend dependencies
cd backend
npm install
# Install frontend dependencies
cd ../frontend
npm install
cd ..Option A: Using Docker (Recommended)
# Start PostgreSQL container
docker run --name slotsweeper-db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=slotsweeper \
-p 5432:5432 \
-d postgres:16-alpineOption B: Local PostgreSQL
# Create database (if using local PostgreSQL)
psql -U postgres
CREATE DATABASE slotsweeper;
\qBackend - Create backend/.env:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/slotsweeper?schema=public"
PORT=8000
JWT_SECRET_KEY="your-super-secret-jwt-key-change-this"
GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
SERVER_ROOT_URI="http://localhost:8000"
CLIENT_ROOT_URI="http://localhost:5173"
SMTP_EMAIL="your-email@gmail.com"
SMTP_PASSWORD="your-gmail-app-password"Frontend - Create frontend/.env:
VITE_SERVER_URI=http://localhost:8000
VITE_WS_URI=ws://localhost:8000cd backend
# Generate Prisma Client
npx prisma generate
# Run database migrations
npx prisma migrate dev
# (Optional) Open Prisma Studio to view database
npx prisma studioTerminal 1 - Backend:
cd backend
npm run dev
# Server will start at http://localhost:8000Terminal 2 - Frontend:
cd frontend
npm run dev
# App will start at http://localhost:5173git clone https://github.com/Ankitsinghsisodya/SlotSwapper.git
cd SlotSwappercd backend
npm installcd ../frontend
npm installCreate backend/.env file:
# Database
DATABASE_URL="postgresql://username:password@localhost:5432/slotsweeper?schema=public"
# Server
PORT=8000
# JWT
JWT_SECRET_KEY="your-super-secret-jwt-key"
# Google OAuth
GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# URLs
SERVER_ROOT_URI="http://localhost:8000"
CLIENT_ROOT_URI="http://localhost:5173"
# SMTP (for OTP emails)
SMTP_EMAIL="your-email@gmail.com"
SMTP_PASSWORD="your-app-password"Create frontend/.env:
VITE_SERVER_URI=http://localhost:8000
VITE_WS_URI=ws://localhost:8000- Go to Google Cloud Console
- Create a new project or select existing
- Enable Google+ API
- Create OAuth 2.0 credentials
- Add authorized redirect URIs:
http://localhost:5173/api/v1/auth/google/callback- Your production URL callback
- Copy Client ID and Client Secret to
.env
- Enable 2-Step Verification on your Gmail account
- Generate App Password at Google App Passwords
- Use the generated password in
SMTP_PASSWORD
cd backend
# Generate Prisma Client
npx prisma generate
# Run migrations
npx prisma migrate dev
cd backend
npm run devBackend will run at http://localhost:8000
cd frontend
npm run devFrontend will run at http://localhost:5173
# Build and start all services
docker-compose up --build
# Run in detached mode
docker-compose up -d --build
# View logs
docker-compose logs -f
# Stop all services
docker-compose down
# Stop and remove volumes (deletes database data)
docker-compose down -vServices:
- Frontend: http://localhost:4173
- Backend: http://localhost:8000
- PostgreSQL: localhost:5432
docker build -f docker/Dockerfile.backend \
--build-arg DATABASE_URL="postgresql://postgres:postgres@postgres:5432/slotsweeper" \
-t slotsweeper-backend .
docker run -p 8000:8000 \
--env-file backend/.env \
slotsweeper-backenddocker build -f docker/Dockerfile.frontend \
--build-arg VITE_SERVER_URI="http://localhost:8000" \
--build-arg VITE_WS_URI="ws://localhost:8000" \
-t slotsweeper-frontend .
docker run -p 4173:4173 slotsweeper-frontend- Development:
http://localhost:8000 - Production: Your deployed backend URL
Most endpoints require JWT authentication. Include the token in cookies (automatically handled by browser) or in the Authorization header:
Authorization: Bearer <your-jwt-token>
POST /api/v1/auth/signup
Register a new user account.
Request Body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "SecurePass123!"
}Response (200):
{
"statusCode": 200,
"message": "OTP sent successfully",
"data": {
"email": "john@example.com"
}
}Rate Limit: 100 requests per 15 minutes per IP
POST /api/v1/auth/verifyOTP
Verify email with OTP code sent during signup.
Request Body:
{
"email": "john@example.com",
"otp": "123456"
}Response (200):
{
"statusCode": 200,
"message": "User verified successfully",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
}
}Rate Limit: 100 requests per 15 minutes per IP
POST /api/v1/auth/login
Login with email and password.
Request Body:
{
"email": "john@example.com",
"password": "SecurePass123!"
}Response (200):
{
"statusCode": 200,
"message": "Login successful",
"data": {
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"picture": null
}
}
}Rate Limit: 100 requests per 15 minutes per IP
GET /api/v1/auth/google/url
Get Google OAuth authorization URL.
Response (200):
{
"statusCode": 200,
"data": {
"url": "https://accounts.google.com/o/oauth2/v2/auth?..."
}
}Rate Limit: None
POST /api/v1/auth/google/googleLogin
Complete Google OAuth login.
Request Body:
{
"code": "google-oauth-code-from-callback"
}Response (200):
{
"statusCode": 200,
"message": "Login successful",
"data": {
"user": {
"id": 2,
"name": "John Doe",
"email": "john@gmail.com",
"picture": "https://lh3.googleusercontent.com/..."
}
}
}Rate Limit: None
POST /api/v1/auth/logout
Logout current user (clears JWT cookie).
Response (200):
{
"statusCode": 200,
"message": "Logout successful"
}GET /api/v1/events/my-events
Get all events for authenticated user.
Headers: Authorization: Bearer <token>
Response (200):
{
"statusCode": 200,
"message": "Events fetched successfully",
"data": {
"events": [
{
"id": 1,
"title": "Team Meeting",
"startTime": "2025-11-07T10:00:00.000Z",
"endTime": "2025-11-07T11:00:00.000Z",
"status": "SWAPPABLE",
"ownerId": 1,
"createdAt": "2025-11-06T08:00:00.000Z"
}
]
}
}POST /api/v1/events/create-event
Create a new calendar event.
Headers: Authorization: Bearer <token>
Request Body:
{
"title": "Project Review",
"startTime": "2025-11-08T14:00:00.000Z",
"endTime": "2025-11-08T15:30:00.000Z"
}Response (201):
{
"statusCode": 201,
"message": "Event created successfully",
"data": {
"event": {
"id": 5,
"title": "Project Review",
"startTime": "2025-11-08T14:00:00.000Z",
"endTime": "2025-11-08T15:30:00.000Z",
"status": "BUSY",
"ownerId": 1
}
}
}PUT /api/v1/events/update-event/:id
Update an existing event.
Headers: Authorization: Bearer <token>
Request Body:
{
"title": "Updated Meeting Title",
"startTime": "2025-11-08T15:00:00.000Z",
"endTime": "2025-11-08T16:00:00.000Z",
"status": "SWAPPABLE"
}Response (200):
{
"statusCode": 200,
"message": "Event updated successfully",
"data": {
"event": {
/* updated event object */
}
}
}DELETE /api/v1/events/delete-event/:id
Delete an event.
Headers: Authorization: Bearer <token>
Response (200):
{
"statusCode": 200,
"message": "Event deleted successfully"
}GET /api/v1/swap/swappable-slots
Get all available swappable slots from other users.
Headers: Authorization: Bearer <token>
Response (200):
{
"statusCode": 200,
"data": {
"swappableSlots": [
{
"id": 3,
"title": "Lunch Break",
"startTime": "2025-11-07T12:00:00.000Z",
"endTime": "2025-11-07T13:00:00.000Z",
"status": "SWAPPABLE",
"owner": {
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com"
}
}
]
}
}POST /api/v1/swap/swap-request
Request to swap your slot with another user's slot.
Headers: Authorization: Bearer <token>
Request Body:
{
"requesterSlotId": 1,
"responderSlotId": 3
}Response (200):
{
"statusCode": 200,
"message": "Swap request sent successfully",
"data": {
"swapRequest": {
"id": 10,
"requesterId": 1,
"responderId": 2,
"requesterSlotId": 1,
"responderSlotId": 3,
"status": "PENDING"
}
}
}Rate Limit: 20 requests per hour per user
GET /api/v1/swap/swap-incoming-requests
Get all swap requests sent to you.
Headers: Authorization: Bearer <token>
Response (200):
{
"statusCode": 200,
"data": {
"incomingRequests": [
{
"id": 10,
"status": "PENDING",
"createdAt": "2025-11-06T10:00:00.000Z",
"requester": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"requesterSlot": {
"id": 1,
"title": "Team Meeting",
"startTime": "2025-11-07T10:00:00.000Z",
"endTime": "2025-11-07T11:00:00.000Z"
},
"responderSlot": {
"id": 3,
"title": "Lunch Break",
"startTime": "2025-11-07T12:00:00.000Z",
"endTime": "2025-11-07T13:00:00.000Z"
}
}
]
}
}GET /api/v1/swap/swap-outgoing-requests
Get all swap requests you've sent.
Headers: Authorization: Bearer <token>
Response: Similar structure to incoming requests
POST /api/v1/swap/swap-response
Accept or reject a swap request.
Headers: Authorization: Bearer <token>
Request Body:
{
"swapId": 10,
"response": "ACCEPTED"
}Options: "ACCEPTED" or "REJECTED"
Response (200):
{
"statusCode": 200,
"message": "Swap request accepted successfully"
}Connect to WebSocket at ws://localhost:8000 (or your production WS URL).
Client → Server:
{
"type": "register",
"userId": 1
}Server → Client Notifications:
Swap Request Received:
{
"type": "swap_request",
"data": {
"requestId": 10,
"requester": "John Doe",
"message": "John Doe wants to swap slots with you"
}
}Swap Accepted:
{
"type": "swap_accepted",
"data": {
"message": "Jane Smith accepted your swap request"
}
}Swap Rejected:
{
"type": "swap_rejected",
"data": {
"message": "Jane Smith rejected your swap request"
}
}| Method | Endpoint | Description | Auth Required | Rate Limit |
|---|---|---|---|---|
| POST | /api/v1/auth/signup |
Register new user | ❌ | 100 req/15min |
| POST | /api/v1/auth/verifyOTP |
Verify OTP | ❌ | 100 req/15min |
| POST | /api/v1/auth/login |
Login with credentials | ❌ | 100 req/15min |
| GET | /api/v1/auth/google/url |
Get Google OAuth URL | ❌ | - |
| POST | /api/v1/auth/google/googleLogin |
Login with Google | ❌ | - |
| POST | /api/v1/auth/logout |
Logout user | ✅ | - |
| GET | /api/v1/events/my-events |
Get user's events | ✅ | - |
| POST | /api/v1/events/create-event |
Create new event | ✅ | - |
| PUT | /api/v1/events/update-event/:id |
Update event | ✅ | - |
| DELETE | /api/v1/events/delete-event/:id |
Delete event | ✅ | - |
| GET | /api/v1/swap/swappable-slots |
Get available swappable slots | ✅ | - |
| POST | /api/v1/swap/swap-request |
Request slot swap | ✅ | - |
| GET | /api/v1/swap/swap-incoming-requests |
Get incoming swap requests | ✅ | - |
| GET | /api/v1/swap/swap-outgoing-requests |
Get outgoing swap requests | ✅ | - |
| POST | /api/v1/swap/swap-response |
Accept/Reject swap | ✅ | - |
Example: Create Event
curl -X POST http://localhost:8000/api/v1/events/create-event \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"title": "Team Standup",
"startTime": "2025-11-07T09:00:00.000Z",
"endTime": "2025-11-07T09:30:00.000Z"
}'Example: Request Swap
curl -X POST http://localhost:8000/api/v1/swap/swap-request \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"requesterSlotId": 1,
"responderSlotId": 3
}'- JWT-based authentication with httpOnly cookies
- Google OAuth 2.0 integration for social login
- Email OTP verification for account security
- Password hashing with bcrypt
- Protected routes with authentication middleware
Implemented using express-rate-limit to prevent abuse:
- Applied to: Login, Signup, and OTP Verification endpoints
- Limit: 100 requests per 15 minutes per IP address
- All other endpoints: No rate limiting applied
The same rate limiter instance is used across the three protected auth endpoints to provide consistent protection against brute-force attacks while being generous enough for legitimate users.
Production Recommendation: Consider implementing Redis-backed rate limiting for horizontal scaling and more granular per-user limits on sensitive operations.
Real-time notifications for:
- New swap requests - Notifies responder when someone requests their slot
- Swap accepted - Notifies requester when their swap is accepted
- Swap rejected - Notifies requester when their swap is declined
WebSocket runs on the same port as the HTTP server (8000) using Node.js ws library.
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String?
googleId String? @unique
picture String?
events Event[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Event {
id Int @id @default(autoincrement())
title String
startTime DateTime
endTime DateTime
status EventStatus @default(BUSY)
ownerId Int
owner User @relation(fields: [ownerId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum EventStatus {
BUSY
SWAPPABLE
SWAP_PENDING
}
model SwapRequest {
id Int @id @default(autoincrement())
requesterId Int
responderId Int
requesterSlotId Int
responderSlotId Int
status SwapStatus @default(PENDING)
createdAt DateTime @default(now())
}
enum SwapStatus {
PENDING
ACCEPTED
REJECTED
}The backend includes automated scheduled tasks to maintain database hygiene:
Configuration: backend/src/jobs/cronJobs.ts
- Schedule: Daily at midnight (00:00) Asia/Kolkata timezone
- Tasks:
- OTP Cleanup: Automatically deletes OTP records older than 5 minutes
- Event Cleanup: Removes past events with end times older than 5 minutes
How it works:
- Uses the
cronnpm package for scheduling - Job starts automatically when the backend server launches (imported in
index.ts) - Runs in the background without blocking the main application
- Logs errors if cleanup operations fail
Cron Expression: "0 0 * * *" (daily at midnight)
Note: If you need more frequent cleanup (e.g., every 5 minutes for OTPs), update the cron expression in
cronJobs.ts:
- Every minute:
"* * * * *"- Every 5 minutes:
"*/5 * * * *"- Every hour:
"0 * * * *"
Database Impact:
- Keeps the
otptable lean by removing expired verification codes - Prevents accumulation of old event records
- Automatic - no manual intervention required
Backend allows multiple origins for development and production:
http://localhost:5173(Vite dev)http://localhost:4173(Vite preview)http://localhost:3000- Production URLs
Configured with:
- Credentials support for cookies
- Preflight request handling
- Custom headers support
- Create a new Web Service on Render
- Connect your GitHub repository
- Configure:
- Build Command:
npm ci --include=dev && npx prisma generate && npm run build - Start Command:
npx prisma migrate deploy && node dist/index.js - Root Directory:
backend
- Build Command:
- Add environment variables from
backend/.env - Add PostgreSQL database
- Deploy!
- Push your code to GitHub
- Import project in Vercel
- Configure:
- Framework Preset: Vite
- Root Directory:
frontend - Build Command:
npm run build - Output Directory:
dist
- Add environment variables:
VITE_SERVER_URI: Your backend URL (e.g.,https://your-backend.onrender.com)VITE_WS_URI: Your backend WebSocket URL (e.g.,wss://your-backend.onrender.com)
- Deploy!
- Vercel will automatically deploy on every push to main branch
Update URLs to production values:
# Backend
SERVER_ROOT_URI="https://your-backend.onrender.com"
CLIENT_ROOT_URI="https://your-frontend.vercel.app"
# Frontend
VITE_SERVER_URI=https://your-backend.onrender.com
VITE_WS_URI=wss://your-backend.onrender.com-
User Behavior
- Users will mark their slots as "SWAPPABLE" when they're willing to swap
- Users only swap entire time slots, not partial durations
- Only two users are involved in each swap transaction
- Users have stable internet for WebSocket connections
-
Technical Assumptions
- PostgreSQL is available (local or Docker)
- Gmail SMTP is used for OTP emails (app passwords enabled)
- Modern browsers with WebSocket support
- Users authenticate before accessing protected features
- Timezone handling is left to client-side for display (stored as UTC in DB)
-
Business Logic
- Events can only be in one of three states: BUSY, SWAPPABLE, or SWAP_PENDING
- A swap request can only be PENDING, ACCEPTED, or REJECTED
- When a swap is accepted, both events' owners are swapped automatically
- Expired OTPs (>5 minutes) are cleaned up by cron job daily
- Past events are cleaned up automatically
-
Security Assumptions
- JWT tokens in httpOnly cookies provide sufficient session security
- Rate limiting by IP is adequate for preventing abuse
- CORS configuration allows legitimate origins only
- Environment variables are properly secured in production
Challenge: Designing a scalable WebSocket system that notifies the right users without polling.
Solution:
- Implemented connection-based user tracking with a
Map<userId, WebSocket> - Register users on WebSocket connection with their JWT-verified userId
- Target specific users for swap notifications
- Gracefully handle disconnections and reconnections
Challenge: Users could click "Create Event" multiple times rapidly, causing duplicate events.
Solution:
- Added synchronous ref lock (
useRef) in React to catch rapid clicks - Combined with state-based UI disable (
isCreating) for visual feedback - Disabled submit button while request is in flight
- Added loading spinner for better UX
Challenge: Supporting both email/password and Google OAuth with OTP verification.
Solution:
- Created separate auth flows: email → OTP verification vs. Google → instant login
- Unified user model to handle both
passwordandgoogleId(both optional) - JWT middleware validates tokens regardless of auth method
- Rate limiting applied per auth method to prevent abuse
Challenge: Credentials (cookies) not being sent cross-origin during local development.
Solution:
- Configured CORS to reflect requesting origin with
origin: true - Enabled
credentials: truefor cookie support - Added preflight (OPTIONS) request handling
- Documented proper CORS setup for production deployment
Challenge: Prisma migrations failing in Docker builds due to missing DATABASE_URL.
Solution:
- Pass
DATABASE_URLas build arg to Docker forprisma generate - Run
prisma migrate deployin container startup script, not during build - Install dev dependencies during build to ensure
tscand Prisma CLI available - Created multi-stage approach for cleaner builds
Challenge: Events stored in UTC but users expect local time display.
Solution:
- Store all timestamps in UTC in PostgreSQL
- Let frontend handle timezone conversion using JavaScript
Datemethods - Use
datetime-localinput in forms for natural time entry - Format displays with
toLocaleString()for user's local timezone
Challenge: Initial cron schedule (daily) didn't match business logic (5-minute OTP expiry).
Solution:
- Documented cron expression and provided examples for different frequencies
- Made it easy to customize: change one string in
cronJobs.ts - Explained that daily cleanup is sufficient for old events, but OTPs could use more frequent runs
- Job auto-starts on server launch (imports in
index.ts)
Challenge: Ensuring swap operations don't leave inconsistent state if one update fails.
Solution:
- Used Prisma transactions for atomic swap operations
- Update swap request status and both events' owners in single transaction
- Rollback entire operation if any step fails
- Added proper error handling and user-facing error messages
Challenge: Balancing security (prevent abuse) with UX (don't block legitimate users).
Solution:
- Implemented single rate limiter: 100 requests per 15 minutes per IP
- Applied only to authentication endpoints (login, signup, verifyOTP)
- Generous limit allows legitimate use while preventing brute-force attacks
- Other endpoints remain unrestricted to avoid blocking normal application usage
- Recommended Redis-backed store for production scalability and per-user limits
Challenge: Replacing blocking alert() and confirm() without losing important user feedback.
Solution:
- Integrated toast component library for non-blocking notifications
- Created custom confirmation modal for destructive actions (delete)
- Consistent success/error feedback across all user actions
- WebSocket notifications trigger toasts for real-time events
- WebSocket State Management: Keep connection state simple; avoid over-engineering reconnection logic initially
- Rate Limiting: Start strict, then relax based on real usage patterns
- Docker Development: Always test Docker builds early; environment variables and build context can be tricky
- Type Safety: TypeScript catches many bugs early; invest time in proper typing
- User Feedback: Non-blocking UI feedback (toasts) greatly improves perceived responsiveness
- Add Redis for distributed WebSocket connections (horizontal scaling)
- Implement slot conflict detection (prevent double-booking)
- Add recurring events support
- Implement swap history/audit log
- Add email notifications for swap requests (not just real-time)
- Create mobile app (React Native)
- Add user profiles and preferences
- Implement slot search/filter functionality
- Add calendar export (iCal format)
- Implement multi-party swaps (3+ users)
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Ankit Singh Sisodya - @Ankitsinghsisodya
- React and Vite teams for amazing tools
- Prisma for the excellent ORM
- Tailwind CSS for beautiful styling
- Node.js and Express communities
For support, email ankitsingh24012005@gmail.com or open an issue on GitHub.
Happy Swapping! 🔄