A full-stack peer-to-peer calendar app where users swap time slots with others. Mark a slot "swappable," browse the marketplace, request a swap, and accept/reject in real-time.
SlotSwapper is a web application for exchanging calendar time slots between users. User A marks their Tuesday 10–11 AM meeting as swappable; User B marks Wednesday 2–3 PM as swappable. User A requests a swap of their Tuesday slot for User B's Wednesday slot. On acceptance, both calendars update automatically.
Tech Stack:
- Frontend: React 18 + TypeScript + Vite + Tailwind CSS + Zustand (state)
- Backend: Node.js 20 + Express + TypeScript + Prisma ORM + MySQL 8
- Real-time: Socket.io for instant notifications
- Auth: JWT + bcrypt password hashing
- Deployment: Docker + Docker Compose (local); If possible Render (backend) + Netlify (frontend)
Architecture: Monolithic repo with separate backend (Node/Express) and frontend (React) services. Both communicate via REST API + WebSocket. MySQL backend stores users, events, and swap requests.
- Node.js 20+ and npm
- MySQL 8+ (or Docker)
- Git
git clone https://github.com/AbdulWasih05/SlotSwapper.git
cd servicehive
docker-compose up
# Wait for "listening on port 5000" and "Vite server running"
# Frontend: http://localhost:5173
# Backend: http://localhost:5000Backend:
cd backend
npm install
cp .env.example .env
# Edit .env with:
# DATABASE_URL="mysql://root:password@localhost:3306/slotswapper"
# JWT_SECRET="your-dev-secret-key-change-in-prod"
# PORT=5000
# use this command to generate a jwt secret key
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
npx prisma migrate dev
npm run dev
# Expect: "Express server running on port 5000"Frontend (in new terminal):
cd frontend
npm install
cp .env.example .env
# Edit .env with:
# VITE_API_URL=http://localhost:5000
# VITE_WS_URL=ws://localhost:5000
npm run dev
# Expect: "Vite server running at http://localhost:5173"- Register: Navigate to http://localhost:5173, click "Sign Up", create account with email/password
- Create Event: Go to Calendar, add an event (title, start time, end time)
- Mark Swappable: Toggle event status to SWAPPABLE
- Register 2nd User: Open incognito window or different broswer, register different email
- Request Swap: 2nd user goes to Marketplace, sees 1st user's slot, requests swap with their own slot
- Accept Swap: 1st user goes to Requests, accepts swap
- Verify: Both calendars now show swapped ownership
Environment Variables Reference:
| Key | Backend | Frontend | Example |
|---|---|---|---|
| DATABASE_URL | Required | — | mysql://root:password@localhost:3306/slotswapper |
| JWT_SECRET | Required | — | dev-secret-key-min-32-chars |
| PORT | Optional | — | 5000 |
| NODE_ENV | Optional | — | development |
| VITE_API_URL | — | Required | http://localhost:5000 |
| VITE_WS_URL | — | Required | ws://localhost:5000 |
Frontend (React) Backend (Express) Database (MySQL)
:5173 :5000
| | |
+------- HTTP REST API ------|------- SELECT/INSERT --|
| | |
+------ WebSocket (io) ------|--- Events/Swaps -------|
Three main entities:
- users: Email, hashed password, profile
- events: Title, timestamps, status (BUSY/SWAPPABLE/SWAP_PENDING), userId
- swap_requests: Links two events and users; status (PENDING/ACCEPTED/REJECTED)
All endpoints require JWT token in Authorization: Bearer <token> header (except /api/auth/register and /api/auth/login).
| Method | Endpoint | Purpose | Body | Response |
|---|---|---|---|---|
| POST | /api/auth/register |
Create account | {email, password, name} |
{user, token} |
| POST | /api/auth/login |
Login | {email, password} |
{user, token} |
| GET | /api/auth/me |
Get current user | — | {user} |
| GET | /api/events |
List user's events | — | {events:[]} |
| POST | /api/events |
Create event | {title, startTime, endTime} |
{event} |
| PATCH | /api/events/:id/status |
Toggle BUSY/SWAPPABLE | {status} |
{event} |
| PUT | /api/events/:id |
Update event | {title?, startTime?, endTime?} |
{event} |
| DELETE | /api/events/:id |
Delete event | — | {message} |
| GET | /api/swappable-slots |
Browse all swappable slots (others' only) | — | {slots:[]} |
| POST | /api/swap-request |
Create swap request | {mySlotId, theirSlotId} |
{swapRequest} |
| GET | /api/swap-requests |
List incoming/outgoing swaps | — | {incoming:[], outgoing:[]} |
| POST | /api/swap-response/:requestId |
Accept/reject swap | {accept: boolean} |
{swapRequest} |
WebSocket Events (Socket.io):
swap:request:received— Incoming swap requestswap:request:accepted— Your request was acceptedswap:request:rejected— Your request was rejected
Sample Request/Response:
# Register
POST /api/auth/register
{
"email": "user@example.com",
"password": "SecurePass123",
"name": "John Doe"
}
# Response:
{
"user": { "id": 1, "email": "user@example.com", "name": "John Doe" },
"token": "eyJhbGciOiJIUzI1NiIs..."
}
# Create Event
POST /api/events
Authorization: Bearer <token>
{
"title": "Team Meeting",
"startTime": "2025-11-06T10:00:00Z",
"endTime": "2025-11-06T11:00:00Z"
}
# Response:
{
"event": {
"id": 5,
"userId": 1,
"title": "Team Meeting",
"startTime": "2025-11-06T10:00:00Z",
"endTime": "2025-11-06T11:00:00Z",
"status": "BUSY"
}
}
# Swap Request
POST /api/swap-request
Authorization: Bearer <token>
{
"mySlotId": 5,
"theirSlotId": 12
}
# Response:
{
"swapRequest": {
"id": 3,
"status": "PENDING",
"requesterSlotId": 5,
"recipientSlotId": 12
}
}Below is the required server-side logic for each endpoint:
Purpose: Browse all available slots for swapping from other users.
Server-side logic:
- Query all events with
status = SWAPPABLE - Exclude events where
userId = currentUserId(hide own slots) - Return with owner details (username, email) for UI display
- Sort by start time for user convenience
Response includes: Event ID, title, start/end times, owner name, owner ID
Purpose: Initiate a swap request.
Server-side validation & logic:
- Verify
mySlotIdexists and belongs to current user - Verify
theirSlotIdexists and belongs to a different user - Verify both slots have
status = SWAPPABLE(critical!) - Create new
SwapRequestrecord:- Set
status = PENDING - Link
requester_id= current user - Link
recipient_id= owner of theirSlotId - Store
requester_slot_id = mySlotId - Store
recipient_slot_id = theirSlotId
- Set
- Update both events atomically:
mySlotId.status = SWAP_PENDING(locks User A's slot)theirSlotId.status = SWAP_PENDING(locks User B's slot)
- Emit WebSocket event:
swap:request:receivedto recipient
Purpose: Accept or reject an incoming swap request.
If Rejected (accept = false):
- Verify current user is the recipient of this swap request
- Update
SwapRequest.status = REJECTED - Reset both events back to
status = SWAPPABLE(unlock them) - Emit WebSocket event:
swap:request:rejectedto requester
If Accepted (accept = true) — ATOMIC TRANSACTION:
- Verify current user is the recipient of this swap request
- Fetch both events within transaction
- Verify both events still have
status = SWAP_PENDING(re-check) - Swap ownership (critical exchange):
- Temp store:
ownerA = requester_slot.userId,ownerB = recipient_slot.userId - Set:
requester_slot.userId = ownerB(Event A now owned by User B) - Set:
recipient_slot.userId = ownerA(Event B now owned by User A)
- Temp store:
- Update both events:
status = BUSY(finalize them) - Update
SwapRequest.status = ACCEPTED - Commit transaction (all-or-nothing)
- Emit WebSocket events:
swap:request:acceptedto requesterslot:updatedto both users
Ownership Exchange Example:
BEFORE:
Event A: "Team Meeting" (userId: 1, status: SWAP_PENDING)
Event B: "Focus Block" (userId: 2, status: SWAP_PENDING)
AFTER:
Event A: "Team Meeting" (userId: 2, status: BUSY) ← Now User 2's
Event B: "Focus Block" (userId: 1, status: BUSY) ← Now User 1's
Return appropriate HTTP status codes for validation failures:
400 Bad Request: Missing or invalid slot IDs403 Forbidden: Slot belongs to another user or user not recipient of request404 Not Found: Slot or swap request doesn't exist409 Conflict: Slot not in correct status (not SWAPPABLE or not SWAP_PENDING)
Critical requirement: The swap acceptance (POST /api/swap-response with accept=true) must execute as a single atomic database transaction to prevent:
- Race condition: Both users simultaneously accepting different swaps for same slots
- Partial updates: Status updated but ownership not swapped (corrupted state)
- Concurrent modifications: Another user modifying slots mid-transaction
Use Prisma transaction:
await prisma.$transaction(async (tx) => {
// All swap logic here
// Either all succeeds or all rolls back
});# Backend tests (swap logic, auth, marketplace)
cd backend
npm test
# Linting and format check
npm run lint
npm run format
# Frontend tests (if implemented)
cd frontend
npm test
# Linting
npm run lintdocker-compose build # Build all images
docker-compose up # Start all services
docker-compose down # Stop all services
docker-compose logs -f # View logsBackend (Render):
- Connect GitHub repo to Render
- Set environment:
DATABASE_URL,JWT_SECRET,NODE_ENV=production - Build command:
npm install && npx prisma migrate deploy - Start command:
npm start
Frontend (Vercel):
- Import GitHub repo to Vercel
- Set environment:
VITE_API_URL=<your-render-url>,VITE_WS_URL=wss://<your-render-url> - Deploy
Database (PlanetScale):
- Create MySQL database
- Use connection string in
DATABASE_URL
Assumptions:
- Swap requests are immutable (accept/reject only, no modification)
- Single-timezone system (no TZ conversion between users)
- First-come-first-served for concurrent swap requests
- Recurring events not supported (create individual instances)
Known Limitations:
- WebSocket notifications require both users online simultaneously
- Email verification not implemented (instant account creation)
- No calendar sync with Google Calendar or Outlook (future enhancement)
- Swap history visible in DB only; no dedicated UI endpoint
- Single-region deployment (may need CDN/multi-region in production)
What I'd Improve:
- Add email verification + password reset
- Implement timezone awareness
- Advanced matching algorithm for swap suggestions
- WebSocket fallback to polling
- Rate limiting on API endpoints
backend/
package.json (npm scripts: dev, build, test, lint, start)
prisma/schema.prisma (database schema)
src/server.ts (Express entry point)
frontend/
package.json (npm scripts: dev, build, preview, lint)
vite.config.ts (Vite configuration)
src/App.tsx (React entry point)
docker-compose.yml (orchestration)
.env.example (backend template)
frontend/.env.example (frontend template)
MIT License. Questions? Open an issue on GitHub.