A scheduling and booking web app inspired by Cal.com. A user shares a public link, the invitee picks a time, a booking is created — with timezone-aware slot generation, custom questions, and rescheduling.
| Layer | Choice |
|---|---|
| Frontend | React 18 (Vite) + Tailwind CSS + Redux Toolkit + React Router |
| Backend | Node.js + Express |
| Database | PostgreSQL via Prisma ORM |
| Time | dayjs (utc + timezone) |
| Containers | Docker (multi-stage client → nginx; node:20-slim server) |
Owner dashboard (single seeded user, no auth) — manage event types, availability schedules, and bookings.
Public booking flow — anyone with the link can book a time:
/<username>lists visible event types/<username>/<slug>shows a calendar with available slots- Pick a slot → fill a form (name, email, custom questions) → submit
- Redirects to
/booking/<id>with Add to calendar (Google/Outlook/Office 365/ICS) and reschedule/cancel controls
Slot generation, double-booking prevention, and timezone handling live in server/src/slots.js and server/src/routes/public.js.
User
├─ EventType[] bookable templates
│ └─ Question[] custom questions per event type
├─ Schedule[] named availability schedules (one isDefault)
│ └─ Availability[] day-of-week start/end windows
├─ DateOverride[] per-day exceptions
└─ Booking[]
└─ BookingAnswer[]
See server/prisma/schema.prisma.
cal.com-clone/
├── client/ React + Vite + Tailwind frontend
│ ├── src/
│ │ ├── api/client.js axios instance, baseURL = VITE_API_URL
│ │ ├── components/ Sidebar, DashboardLayout, Calendar, Toast, Icons
│ │ ├── pages/ Home, EventTypes, Availability, Bookings,
│ │ │ PublicProfile, PublicBooking, BookingConfirmation
│ │ ├── store/ Redux store + userSlice (fetchMe, login)
│ │ ├── App.jsx routes (public + protected)
│ │ └── main.jsx ReactDOM root + providers
│ ├── nginx.conf SPA fallback + asset caching
│ ├── Dockerfile multi-stage: vite build → nginx
│ └── Dockerfile.dev vite dev server with HMR
│
├── server/ Express + Prisma backend
│ ├── prisma/
│ │ ├── schema.prisma User, EventType, Question, Schedule,
│ │ │ Availability, DateOverride, Booking, BookingAnswer
│ │ └── seed.js default user + event types + schedule (idempotent)
│ ├── src/
│ │ ├── routes/ me, eventTypes, schedules, bookings, public
│ │ ├── slots.js timezone-aware slot generation
│ │ ├── db.js Prisma client + getDefaultUser()
│ │ └── index.js Express app + route mounting
│ └── Dockerfile node:20-slim + openssl
│
├── docker-compose.yml prod-style: db + server + nginx client
├── docker-compose.dev.yml dev: db + server + vite HMR
└── render.yaml Render Blueprint
docker compose up --buildFrontend → http://localhost:5173 · Backend → http://localhost:4000 · Postgres → 5432
The server runs prisma db push + seed on startup.
docker run --name calclone-db \
-e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=calclone \
-p 5432:5432 -d postgres:15-alpine
cd server && npm install && cp .env.example .env && npx prisma db push && npm run seed && npm run dev
# new terminal
cd client && npm install && npm run devserver/.env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/calclone?schema=public"
PORT=4000
DEFAULT_USER_EMAIL="achyut@example.com"
client/.env
VITE_API_URL=http://localhost:4000
VITE_API_URL is inlined at build time by Vite.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
Liveness |
| GET | /api/me |
Seeded user |
| GET/POST | /api/event-types |
List / create |
| GET/PUT/DELETE | /api/event-types/:id |
CRUD (with questions) |
| GET/POST | /api/schedules |
List / create |
| GET/PUT/DELETE | /api/schedules/:id |
CRUD (slots + overrides) |
| GET | /api/bookings?status=upcoming|past|cancelled |
Owner list |
| GET | /api/bookings/:id |
Detail (with answers) |
| PUT | /api/bookings/:id |
Reschedule |
| DELETE | /api/bookings/:id |
Cancel |
| GET | /api/u/:username |
Public profile |
| GET | /api/u/:username/:slug |
Event type detail |
| GET | /api/u/:username/:slug/slots?date=YYYY-MM-DD |
Slots for a date |
| GET | /api/u/:username/:slug/available-dates?month=YYYY-MM |
Bookable dates |
| POST | /api/bookings |
Create a booking |
- User — Achyut Gupta ·
achyut-gupta-qldpdd·Asia/Kolkata - Event types —
15 min meeting,30 min meeting,Secret meeting(hidden) - Schedule —
Working hours, default, Mon-Fri 09:00-17:00
Idempotent — re-runs don't duplicate.
- Single-tenant, no auth. Login button just routes to the dashboard.
- Times stored in UTC; the schedule's timezone is used for display and slot math.
- Double-booking prevented by an overlap re-check inside
POST /api/bookings. - Email notifications are not implemented — the confirmation page is the source of truth.
server — dev · start · seed · migrate · generate
client — dev · build · preview