A free, open-source, self-hostable scheduling page — your own Calendly.
OpenSlot is a single, beautiful booking page you run yourself and connect to your Google Calendar and your domain. Visitors pick a meeting length, a day, and a time (shown in their own time zone), enter their details, and get a real calendar invite with an auto-generated Google Meet link. No accounts, no per-seat fees, no third party in the middle — you own the code and the data.
The signature touch: each meeting length has its own color, and that accent flows through the entire calendar — available days, the selected day, the time slots, and the buttons all recolor to match.
Built with Next.js (App Router) + TypeScript. The UI is a faithful build of the hi-fi design reference in
design_handoff_scheduler/.
- Self-hosted & private — runs on your own infrastructure; meeting data never leaves your Google account and your server.
- Free forever — MIT licensed, no SaaS subscription.
- Yours to brand — your name, your tagline, your domain, your working hours.
- Responsive on any device — one fluid layout that adapts from desktop to phone. On narrow screens the rail, calendar, and time slots stack vertically so nothing overflows or gets cut off — no separate mobile build, no app to install.
- Real calendar sync — server-side Google free/busy + event creation with a Meet link and an emailed invite. OAuth secrets stay on the server, never in the browser.
- Reschedule & cancel links — every booking comes with Calendly-style reschedule and cancel links (in the invite and on the confirmation screen), secured with signed tokens so only the guest can change their own meeting.
- Google Meet and/or Zoom — auto-generate a Meet link, attach a fixed Zoom link, or
offer both and let the guest pick at booking. Set
NEXT_PUBLIC_MEETING_OPTIONS(e.g.meet,zoom) andZOOM_MEETING_URL. - Event types (Calendly-style) — define named types (e.g. a 15-min "Student" type on
Zoom and a "Professional" type on Meet with all lengths). The booker picks the type
first; it sets the available lengths and the conferencing. Configure via
NEXT_PUBLIC_EVENT_TYPES. - Conflict-aware across all your calendars — check free/busy across as many calendars as you like (work, health, side projects), so a meeting on any of them automatically removes that slot and you never get double-booked. See Blocking across multiple calendars.
- Works out of the box — runs in demo mode with simulated availability before you connect anything, so you can try it in under a minute.
- Extensible — a provider-agnostic backend; an Outlook/Microsoft 365 (Teams) provider can be added without touching the UI.
Requires Node 18.18+ (Node 20+ recommended).
git clone https://github.com/JarodGabriel/Open-Slot-Calendar-Control.git
cd Open-Slot-Calendar-Control
npm install
# copy the env template (macOS/Linux)
cp .env.example .env.local
# …or on Windows PowerShell:
# copy .env.example .env.local
npm run devOpen http://localhost:3000. With no credentials set it runs in demo mode — fully clickable, availability simulated, bookings faked. When you're ready for real bookings, follow Connect your Google Calendar.
All configuration is environment variables in .env.local (copied from
.env.example). NEXT_PUBLIC_* values are branding shown in the
browser; the rest are server-side.
| Variable | What it controls | Example |
|---|---|---|
NEXT_PUBLIC_HOST_NAME |
Name shown on the page and in invites | Jane Doe |
NEXT_PUBLIC_HOST_INITIALS |
Monogram in the avatar circle | JD |
NEXT_PUBLIC_HOST_TAGLINE |
Italic tagline under your name | Let's talk. |
The
.icsdownload filename and event titles are derived fromNEXT_PUBLIC_HOST_NAMEautomatically (e.g.meeting-with-jane-doe.ics), so there's nothing else to rename.
| Variable | What it controls | Default |
|---|---|---|
HOST_TIMEZONE |
IANA zone your working hours are defined in | America/Los_Angeles |
WORK_START_HOUR / WORK_END_HOUR |
Bookable window, 24h clock | 9 / 17 |
NEXT_PUBLIC_ALLOW_WEEKENDS |
Allow Saturday/Sunday bookings | false |
HOST_CALENDAR_ID |
Calendar new bookings are created on | primary |
HOST_BUSY_CALENDAR_IDS |
Extra calendars to check for conflicts (see below) | (none) |
By default OpenSlot only checks HOST_CALENDAR_ID for conflicts. To stop
double-bookings against your other calendars (work, health, a side project),
add their calendar IDs — comma-separated — to HOST_BUSY_CALENDAR_IDS. A busy
block on any listed calendar (the booking calendar is always included)
removes that slot.
Each calendar must be visible to the connected Google account — share it
into that account with at least See free/busy access, the same way you'd
overlay calendars in Google Calendar. Find a calendar's ID in Google Calendar
→ that calendar's Settings → "Integrate calendar" → Calendar ID (looks like an
email or …@group.calendar.google.com). Example:
HOST_BUSY_CALENDAR_IDS="work@yourcompany.com,abc123@group.calendar.google.com"A calendar the account can't read is logged and skipped, never failing the whole availability lookup.
Finding your calendar IDs the easy way: once Google is connected, visit
/api/setup/calendars?key=YOUR_GOOGLE_CLIENT_SECRET — it lists every calendar
the connected account can see, with each id, so you can copy the right values
into HOST_CALENDAR_ID / HOST_BUSY_CALENDAR_IDS. It's gated behind your client
secret, so the list isn't public.
The four lengths and their accent/tint colors live in
src/lib/durations.ts. Edit that one array to change the
durations, labels, blurbs, or palette — the whole calendar re-themes automatically.
OpenSlot is a single-host scheduler: it authenticates as you with a stored refresh token, so your visitors never sign in. One-time setup (~5 minutes):
- Google Cloud Console → create (or pick) a project → enable the Google Calendar API.
- APIs & Services → OAuth consent screen → configure it (User type External is fine) and add your own Google account under Test users.
- Credentials → Create credentials → OAuth client ID → Web application. Under
Authorized redirect URIs add exactly:
http://localhost:3000/api/auth/google/callback - Copy the Client ID and Client secret into
.env.local:GOOGLE_CLIENT_ID="…" GOOGLE_CLIENT_SECRET="…"
- Run
npm run dev, then visit http://localhost:3000/api/auth/google and approve the consent screen. - The callback page prints a
GOOGLE_REFRESH_TOKEN="…"line. Paste it into.env.localand restart the server.
Done — OpenSlot now reads your real free/busy and creates real events with Meet links and emailed invites.
Security: the refresh token grants access to your calendar. It lives only in
.env.local(which is git-ignored) and is shown once during setup; the app never stores it anywhere else. Never commit it.
OpenSlot is a standard Next.js app and runs on any Node host (Vercel, Netlify, Fly.io, Render, a VPS, etc.). Using Vercel as the example:
- Push your fork to GitHub and import the repo in Vercel.
- In the project's Environment Variables, add everything from your
.env.local(host branding, working hours, and the Google credentials). - Set
GOOGLE_REDIRECT_URIto your production callback, e.g.https://book.yourdomain.com/api/auth/google/callback, and add that same URL to the Authorized redirect URIs on your Google OAuth client. - Add your custom domain in Vercel (e.g.
book.yourdomain.com) and point your DNS at it. - Re-run the connect flow once against the production URL
(
https://book.yourdomain.com/api/auth/google) to mint a refresh token for that origin, and set it as theGOOGLE_REFRESH_TOKENenv var.
Then share https://book.yourdomain.com — that's your personal scheduling page.
Going beyond yourself as a test user? Move the OAuth consent screen out of "Testing" and submit it for verification per Google's requirements.
Browser (Scheduler.tsx)
│ GET /api/availability?date&durationMin → open instants (ms epoch)
│ POST /api/book {startInst,name,email,…} → event + Meet link + invite
▼
API routes ──► getProvider() ──► GoogleProvider (real free/busy + events.insert)
└─► DemoProvider (simulated, when no creds)
- Availability — the server slices
WORK_START_HOUR…WORK_END_HOURinHOST_TIMEZONEinto 30-minute increments, subtracts the calendar's busy ranges, and drops past slots. - Time zones — slots are stored as absolute instants and formatted in the visitor's
auto-detected zone, so a 9 am PT slot correctly reads as 12 pm ET / 5 pm London. The
math (
src/lib/timezone.ts) is dependency-freeIntland runs identically on server and client. - Booking — re-validated server-side before the event is created; visitors never talk to Google directly.
src/
app/
layout.tsx, page.tsx, globals.css
api/
availability/route.ts GET open slots
book/route.ts POST create event
auth/google/route.ts one-time OAuth connect
auth/google/callback/route.ts → prints your refresh token
components/
Scheduler.tsx the whole UI (3 steps)
icons.tsx
lib/
config.ts env-backed settings
durations.ts the meeting lengths + colors
timezone.ts instant ↔ wall-clock helpers
tzOptions.ts time-zone dropdown list
ics.ts .ics fallback download
calendar/
provider.ts the CalendarProvider interface
slots.ts working-hours → open instants
google.ts GoogleProvider (real)
demo.ts DemoProvider (simulated)
index.ts getProvider() / isDemoMode()
Implement the same CalendarProvider interface
(src/lib/calendar/provider.ts) against Microsoft Graph
(getSchedule for free/busy, me/events with isOnlineMeeting: true for a Teams link),
then return it from getProvider(). The front-end and API routes need no changes — the
confirmation screen reads the returned meetingUrl regardless of provider.
Issues and pull requests are welcome. This is a small, dependency-light codebase by
design — keep changes focused and the UI faithful to the design tokens in
design_handoff_scheduler/README.md.
MIT — free and open source. Use it, fork it, brand it, host it.