buildyourown.software

Scheduling · by Calendly LLC

Build your own Calendly.

A page that shows when you're free and books a meeting. Calendly charges per seat, per month, forever, for a feature Google Calendar nearly has built in.

What you're paying now

$1,920 per year

Calendly Teams for 10 people: $16 × 10 seats × 12 months on annual billing. $2,400 if billed monthly.

Build it now

Build it in any tool

Paste into Claude Code, ChatGPT, Codex, Replit, Lovable, or Cursor. The agent fetches everything it needs.

Paste anywhere

Build my own Calendly. Fetch https://buildyourown.software/like/calendly/llms.md and follow it: build the app from the build prompt, then write and run the tests from the test plan. Ask me before changing the data model or the non-goals.

Claude Code

claude "Build my own Calendly. Fetch https://buildyourown.software/like/calendly/llms.md and follow it: build the app from the build prompt, then write and run the tests from the test plan. Ask me before changing the data model or the non-goals."

Codex CLI

codex "Build my own Calendly. Fetch https://buildyourown.software/like/calendly/llms.md and follow it: build the app from the build prompt, then write and run the tests from the test plan. Ask me before changing the data model or the non-goals."
Raw files for agents:llms.mdbuild-prompt.mdtest-prompt.mdIf your tool can't open links, copy the full prompt in section 05 instead.

01

What it does

Calendly gives you a public link. Someone picks a time from your real availability, fills in their name and email, and a calendar event lands on both calendars with a video link. It sends confirmations and reminders and lets the guest reschedule or cancel.

On top of that it sells team features: round-robin assignment, collective availability, routing forms that decide who a lead should meet, and CRM sync. Those are the reason for the Teams and Enterprise tiers.

For one person or a small team, the whole product is: read free/busy from Google, apply some rules, write an event. That is a small app with one OAuth integration.

02

What it costs

List prices from the vendor's own pricing page. The fine print is where the money goes.

Free

$0

1 seat

  • 1 event type
  • 1 calendar connection
  • One-on-one meetings only

Standard

$10

per seat / month billed annually ($12 monthly)

  • Unlimited event types
  • Multiple calendars
  • Reminders and follow-ups
  • Stripe and PayPal payments

Teams

$16

per seat / month billed annually ($20 monthly)

  • Round-robin and collective events
  • Routing forms
  • Salesforce meeting sync
  • Managed event types

Enterprise

$15k+

per year, minimum 50 seats

  • SSO and SAML
  • Audit logs
  • Salesforce routing
  • Dedicated support

Contact sales. Starts at $15,000 per year.

The fine print

  • Every person who hosts a meeting needs a paid seat, including people who book two meetings a month.
  • The Free plan allows exactly one event type. The moment you want a 15-minute and a 30-minute option, you pay.
  • Reminder emails and SMS are paid features. Google Calendar sends reminders for free.
  • AI notetaker and other add-ons are priced separately on top of the seat.
  • Your guests' data and your booking history live in Calendly. Export is manual.

Prices checked September 10, 2026 at calendly.com. They change. Check before you quote them.

03

What you actually use

The headline features, and an honest call on whether a typical team needs each one.

8 of 12 headline features are worth building for a typical user. The rest is what you're paying for.

  • Build it

    Public booking page

    A link per event type that shows available slots in the guest's timezone.

    Why: This is the product.

  • Build it

    Availability rules

    Working hours per weekday, date overrides, buffers before and after, minimum notice, max per day.

    Why: The rules are what make the page trustworthy. They're also just a config object.

  • Build it

    Calendar sync (Google, Outlook)

    Read busy times from your calendars and write the booked event.

    Why: One OAuth flow and two API calls. Start with Google; add Microsoft when someone asks.

  • Build it

    Confirmation, reminder, and follow-up emails

    Emails to host and guest at booking, before the meeting, and after.

    Why: A confirmation email is required. Reminders are a cron job.

  • Build it

    Reschedule and cancel links

    Guest can move or cancel without emailing you.

    Why: Saves you the back-and-forth that scheduling tools exist to remove.

  • Build it

    Video conferencing links

    Auto-generate a Google Meet or Zoom link on the event.

    Why: Google Meet is a flag on the calendar event. Zoom needs another OAuth app; do it later.

  • Build it

    Custom questions on the booking form

    Ask for a phone number, company, or 'what do you want to talk about'.

    Why: A JSON array of fields. Ten minutes.

  • Build it

    Multiple event types

    15-min intro, 30-min call, 60-min working session, each with its own rules.

    Why: Calendly locks this behind a paid plan. It's one database table.

  • Maybe

    Round-robin and collective events

    Assign to the next available teammate, or find a slot where everyone is free.

    Why: Only if you're a sales team distributing inbound demos. Otherwise skip.

  • Skip

    Routing forms

    Ask questions, then route to the right person or event type.

    Why: A form with a redirect. Build it when your inbound volume makes it worth having.

  • Maybe

    Payments at booking

    Charge for the slot with Stripe or PayPal.

    Why: Consultants and coaches, yes. Stripe Checkout before the event is created is about a day.

  • Skip

    CRM sync, SSO, audit logs

    Push meetings to Salesforce or HubSpot; enterprise admin.

    Why: A webhook on booking covers CRM sync. SSO is not a small-team problem.

04

How it works under the hood

The data model, the flows that matter, and the integrations you need. This is what the build prompt is based on.

Data model

User (host)

id · name · email · slug · timezone · google_refresh_token · meet_enabled

Encrypt the refresh token at rest.

Calendar connection

id · user_id · provider · calendar_id · role (check_busy | write_events)

Check busy on several calendars; write events to one.

Event type

id · user_id · slug · name · duration_min · description · location (meet | phone | in_person | custom) · color · questions (json) · active

Availability

id · user_id · weekly_hours (json: weekday → [start,end][]) · date_overrides (json) · buffer_before_min · buffer_after_min · min_notice_hours · max_per_day · slot_interval_min

Booking

id · event_type_id · host_id · guest_name · guest_email · guest_timezone · start_at · end_at · answers (json) · status (confirmed | cancelled | rescheduled) · calendar_event_id · meet_url · manage_token · created_at

manage_token is a random secret in the reschedule/cancel links.

Notification

id · booking_id · kind (confirmation | reminder_24h | reminder_1h | followup) · send_at · sent_at

Key flows

Compute available slots

  1. 1.Guest opens /<host>/<event-type> and the page reads their browser timezone.
  2. 2.Server takes the requested date range (2 weeks) and expands weekly_hours plus overrides into candidate windows in the host's timezone.
  3. 3.Fetch busy intervals from Google Calendar freebusy for every check_busy calendar, plus existing bookings.
  4. 4.Subtract busy intervals expanded by buffers; drop anything inside min_notice; enforce max_per_day.
  5. 5.Slice remaining windows into slots every slot_interval_min that fit duration_min; convert to guest timezone; return.

Book a slot

  1. 1.Guest picks a slot and submits name, email, and answers.
  2. 2.Server re-checks the slot is still free (race with another guest or a new calendar event).
  3. 3.Insert booking in a transaction with a unique index on (host_id, start_at) to prevent doubles.
  4. 4.Create the Google Calendar event with both attendees and a Meet link; store the event id and Meet URL.
  5. 5.Queue confirmation emails to host and guest with an .ics attachment and the manage link.
  6. 6.Schedule reminder notifications at start_at minus 24h and minus 1h.

Reschedule or cancel

  1. 1.Guest opens /manage/<manage_token>.
  2. 2.Cancel: mark cancelled, delete the calendar event, email both parties, drop pending reminders.
  3. 3.Reschedule: show the slot picker again; on pick, create a new booking, mark the old one rescheduled, update the calendar event.

Send reminders

  1. 1.A cron job runs every 5 minutes.
  2. 2.Select notifications where send_at ≤ now and sent_at is null and the booking is confirmed.
  3. 3.Send with Resend; stamp sent_at.

Integrations

  • Google Calendar APIrequiredOAuth, freebusy lookup, event create/update/delete, Meet links.
  • Resend (or Postmark)requiredConfirmation, reminder, and cancellation emails.
  • Postgres (Supabase or Neon)requiredBookings, event types, availability.
  • Cron (Vercel Cron or a scheduled job)requiredReminder delivery.
  • Microsoft GraphoptionalOutlook calendar support.
  • Zoom APIoptionalZoom links instead of Meet.
  • Stripe CheckoutoptionalPaid bookings.
  • Outbound webhookoptionalPost bookings to your CRM or Slack.

05

The build prompt

The full spec the one-line command points at. Paste it directly if your tool can't fetch URLs, or edit the data model and non-goals first to fit your team.

calendly-build-prompt.md · 104 lines · 9,457 chars
# Build a scheduling app (replacing Calendly)

You are building a self-hosted scheduling app for one host, extensible to a small team. It replaces Calendly for people who need a public booking page tied to their Google Calendar with reminders and reschedule/cancel links. Build it end to end. Correctness around timezones and double-booking matters more than features.

## Stack

- Next.js (App Router) with TypeScript, Tailwind
- Postgres via Supabase or Neon, with Drizzle or Prisma
- Google Calendar API via `googleapis` (OAuth 2.0, offline access)
- Resend for email; generate `.ics` files with the `ics` package
- Vercel Cron (or any scheduler) hitting `/api/cron/notifications` every 5 minutes
- `date-fns` and `date-fns-tz` (or Temporal polyfill) for all date math. Never do timezone math by hand.

If the repo already uses a different stack, use that.

## Data model

All tables have `id` uuid, `created_at`, `updated_at`.

- `hosts`: `email` (unique), `name`, `slug` (unique, url-safe), `timezone` (IANA), `google_refresh_token` (encrypted), `write_calendar_id`, `avatar_url`.
- `calendar_connections`: `host_id`, `provider` (`google`), `calendar_id`, `summary`, `check_busy` (bool).
- `event_types`: `host_id`, `slug`, `name`, `description`, `duration_min`, `location_kind` (`meet` | `phone` | `in_person` | `custom`), `location_detail`, `color`, `questions` (jsonb array of `{ id, label, type: text|textarea|phone|select, required, options? }`), `active` (bool). Unique on (`host_id`, `slug`).
- `availability`: one row per host. `weekly_hours` (jsonb: `{ mon: [["09:00","12:00"],["13:00","17:00"]], ... }` in the host's timezone), `date_overrides` (jsonb: `{ "2026-12-24": [] , "2026-12-26": [["10:00","14:00"]] }`), `buffer_before_min`, `buffer_after_min`, `min_notice_hours`, `max_per_day`, `slot_interval_min` (default 30), `booking_window_days` (default 60).
- `bookings`: `event_type_id`, `host_id`, `guest_name`, `guest_email`, `guest_timezone`, `start_at` (timestamptz), `end_at`, `answers` (jsonb), `status` (`confirmed` | `cancelled` | `rescheduled`), `rescheduled_to_id` (nullable), `calendar_event_id`, `meet_url`, `manage_token` (32 random bytes, base64url, unique), `cancel_reason`. Partial unique index on (`host_id`, `start_at`) where `status = 'confirmed'`.
- `notifications`: `booking_id`, `kind` (`confirmation_guest` | `confirmation_host` | `reminder_24h` | `reminder_1h` | `cancelled` | `rescheduled`), `send_at`, `sent_at` (nullable), `error` (nullable).

## Screens

### Host (authenticated)

1. **Sign in with Google** requesting `calendar.readonly`, `calendar.events`, and `openid email profile`. Store the refresh token encrypted. Create the host on first login with a slug from their email local part.
2. **Dashboard (`/app`)**: upcoming bookings (next 14 days) with guest, event type, time in host timezone, and links to the calendar event and the manage page. Past bookings below.
3. **Event types (`/app/event-types`)**: list, create, edit, toggle active, copy link. Editor has name, slug, duration, description, location, color, and a question builder.
4. **Availability (`/app/availability`)**: weekly hours editor (per weekday, multiple ranges), date overrides with a calendar picker, buffers, minimum notice, max per day, slot interval, booking window.
5. **Calendars (`/app/calendars`)**: list Google calendars from the API, tick which to check for busy time, pick one to write events to.
6. **Settings**: name, slug, timezone (searchable IANA list), sign out, delete account.

### Guest (public)

7. **Host page (`/<slug>`)**: name, avatar, list of active event types with duration.
8. **Booking page (`/<slug>/<event-slug>`)**: month calendar on the left showing days with availability, time slots for the selected day on the right, in the guest's timezone (auto-detected, changeable via a dropdown). Also readable as `?date=2026-09-15&month=2026-09`. Show a 12h/24h toggle.
9. **Booking form**: name, email, custom questions, then "Confirm". On success show a confirmation with the time in both timezones, the Meet link, "add to calendar" links (Google, Outlook, .ics), and a link to manage the booking.
10. **Manage page (`/manage/<token>`)**: booking details with Reschedule and Cancel. Reschedule opens the slot picker; cancel asks for an optional reason.

## Slot algorithm (put this in `lib/slots.ts`, pure, fully unit-tested)

Input: event type, availability, list of busy intervals (UTC), existing confirmed bookings, `from`/`to` range (UTC), `now`.

1. For each day in the range in the host timezone, take `date_overrides[date]` if present, else `weekly_hours[weekday]`. Convert each range to UTC intervals. Handle days where DST changes.
2. Build the busy set: calendar busy intervals plus confirmed bookings, each expanded by `buffer_before_min` before and `buffer_after_min` after.
3. Subtract busy from available.
4. Remove anything starting before `now + min_notice_hours`.
5. Slice each remaining window into candidate starts every `slot_interval_min`, keeping only starts where `start + duration_min` is still inside the window.
6. Drop days that already have `max_per_day` confirmed bookings.
7. Return slots as UTC ISO strings. The UI converts to the guest timezone.

## Booking flow (`POST /api/book`)

1. Validate input with zod. Reject unknown event type, inactive event type, past start, and answers missing required questions.
2. Recompute available slots for that single day and confirm the requested start is in the list. Use a fresh freebusy call.
3. In one transaction: insert the booking with `status = confirmed`. If the unique index fails, return 409 with "That time was just taken".
4. Create the Google Calendar event on `write_calendar_id`: title `"<event type name> with <guest name>"`, description with answers and the manage link, attendees host and guest, `conferenceData` requesting a Meet link when `location_kind = meet`, `sendUpdates: 'all'`. Store `calendar_event_id` and `meet_url`.
5. Insert notifications: confirmation to guest and host (send_at now), `reminder_24h` and `reminder_1h` (skip any that are already in the past).
6. Return the booking id and manage token.

If step 4 fails, mark the booking cancelled with reason "calendar error" and return 502. Never leave a booking without a calendar event.

## Cancel and reschedule

- Cancel: set `status = cancelled`, delete the calendar event (`sendUpdates: 'all'`), insert a `cancelled` notification, null out `send_at` on unsent reminders.
- Reschedule: run the booking flow for the new time creating a new booking; on success set the old booking to `rescheduled` with `rescheduled_to_id`, patch the existing calendar event's start/end instead of creating a second one, and move `calendar_event_id` to the new booking. Send a `rescheduled` notification.

## Notifications (`GET /api/cron/notifications`, protected by a secret header)

- Select up to 100 notifications with `sent_at IS NULL AND send_at <= now()` whose booking is `confirmed` (or whose kind is `cancelled` / `rescheduled`).
- Render an email per kind. Confirmation emails attach an `.ics` file. Every email shows the time in the recipient's timezone and includes the manage link (guest) or calendar link (host).
- Send with Resend. On success stamp `sent_at`; on failure store `error` and leave `sent_at` null so it retries next run. Cap retries at 5.

## Non-goals

No team accounts, round-robin, collective events, routing forms, payments, SMS, Outlook, Zoom, or CRM sync in this build. Design the schema so a `team_id` on hosts and a `hosts[]` on event types can be added later.

## Acceptance criteria

1. Host signs in with Google, sees their calendars, and can pick busy-check calendars and a write calendar.
2. With weekly hours Mon–Fri 09:00–17:00 in `America/New_York`, a 30-minute event, and no busy time, the booking page shows 16 slots on a weekday and none on Saturday.
3. A busy event 10:00–11:00 with 15-minute buffers removes the 09:30, 10:00, 10:30, and 11:00 slots.
4. `min_notice_hours = 4` hides slots within the next four hours.
5. A date override to `[]` hides every slot that day; an override to a single range shows only that range.
6. Slots render correctly for a guest in `Asia/Kolkata` (half-hour offset) and for a host week that crosses a DST change.
7. Two concurrent `POST /api/book` for the same slot produce exactly one confirmed booking and one 409.
8. A booking creates a calendar event with a Meet link, and both guest and host receive a confirmation email with a valid `.ics`.
9. Cancelling via the manage link deletes the calendar event and prevents pending reminders from sending.
10. Rescheduling updates the existing calendar event instead of creating a new one and sends a rescheduled email.
11. The cron endpoint sends due notifications exactly once, even when run twice in a row.
12. The public booking page works with JavaScript disabled for browsing days (links) and with JS for the slot picker.

## Deliverables

- The app, migrations, a seed script with one host, three event types, and a week of sample bookings.
- `lib/slots.ts` with a table-driven test suite covering criteria 2 through 6.
- README covering Google Cloud OAuth setup (consent screen, scopes, redirect URIs), Resend domain setup, environment variables, and the cron configuration for Vercel.

Build the slot algorithm and its tests first. Then the Google integration. Then the screens. Run the app in the browser after each step.

06

Where to build it

Any of these will take the prompt. Use the one you already pay for.

  • Claude CodeAgentic coding in your terminal

    Best fit. The slot algorithm and timezone handling benefit from an agent that can write and run tests as it goes.

  • ReplitBuild and host in the browser

    Works well. Replit handles the cron for reminders and hosting. You'll register the Google OAuth app yourself.

  • LovablePrompt-to-app with Supabase built in

    Good for the booking page UI. Do the Google Calendar and cron pieces as Supabase edge functions.

  • ChatGPT / CodexPlan, then build with Codex

    Ask ChatGPT to adapt the availability rules to your week first, then send the spec to Codex.

  • StripePayments, if you need them

    Only if you charge for time. Add Stripe Checkout between 'pick a slot' and 'create the event'.

07

Test it

Walk the checklist by hand once, then let the agent write the automated tests.

Manual checklist

  • Connect a Google account and confirm the OAuth consent shows only calendar scopes.
  • Put a busy event on your calendar and confirm the slot disappears from the booking page.
  • Set buffers of 15 minutes and confirm slots adjacent to a busy event are removed.
  • Set a date override to 'unavailable' for tomorrow and confirm no slots show.
  • Open the booking page from a browser set to a different timezone and confirm times shift correctly.
  • Book a slot and confirm a calendar event with a Meet link appears on both calendars.
  • Confirm the guest gets a confirmation email with a working .ics attachment.
  • Book the same slot from two browsers at once and confirm only one succeeds.
  • Use the manage link to reschedule, then confirm the old event is updated, not duplicated.
  • Use the manage link to cancel, then confirm the calendar event is removed and reminders do not send.
  • Wait for (or fake the clock to) the 1-hour reminder and confirm it sends exactly once.
  • Book across a daylight-saving boundary and confirm the times are right on both sides.

Generate automated tests

Paste this into the same tool that built the app. It writes tests against the acceptance criteria from the build prompt.

calendly-test-prompt.md · 31 lines · 2,820 chars
Write automated tests for the scheduling app in this repo. Treat the acceptance criteria below as the spec. Use Vitest for the slot algorithm and API routes, and Playwright for guest booking flows. Mock the Google Calendar client and Resend at the module boundary so tests run offline; record one real-shaped freebusy and events.insert response as fixtures.

## Acceptance criteria to cover

1. Host sign-in stores an encrypted refresh token and lists calendars (mocked API).
2. Weekly hours Mon–Fri 09:00–17:00 `America/New_York`, 30-minute event, no busy time: 16 slots on a weekday, 0 on Saturday.
3. Busy 10:00–11:00 with 15-minute buffers removes 09:30, 10:00, 10:30, 11:00.
4. `min_notice_hours = 4` hides slots inside the next four hours relative to an injected `now`.
5. Date override `[]` hides all slots that day; override `[["10:00","14:00"]]` shows only those.
6. Guest in `Asia/Kolkata` sees correct half-hour-offset times; a host week spanning the US DST change in March and November produces correct UTC starts on both sides.
7. Two concurrent bookings for the same slot: exactly one confirmed booking and one 409.
8. A booking calls `events.insert` with both attendees and `conferenceDataVersion = 1`, stores the returned `hangoutLink`, and queues two confirmation notifications plus two reminders.
9. Cancelling deletes the calendar event and nulls `send_at` on unsent reminders.
10. Rescheduling calls `events.patch` once, never `events.insert`, and creates a `rescheduled` notification.
11. Running the cron endpoint twice sends each due notification once (assert on the Resend mock call count).
12. Guest booking page: pick a day, pick a slot, fill the form, see the confirmation with manage link (Playwright).
13. A guest who submits a slot that was taken between page load and submit sees the "just taken" message and a refreshed slot list.
14. `max_per_day = 2` hides all slots on a day that already has two confirmed bookings.

## Layout

- `tests/unit/slots.test.ts`: table-driven cases for 2, 3, 4, 5, 6, 14. Each case is `{ name, availability, eventType, busy, bookings, now, guestTz, expectStarts }`.
- `tests/integration/`: API routes against a real test Postgres for 7, 8, 9, 10, 11. Use `Promise.all` with two requests for criterion 7.
- `tests/e2e/`: Playwright for 12 and 13, with the Google and Resend modules mocked via environment flag.

## Rules

- Name every test after its criterion: `test("AC7: concurrent bookings produce one confirmed and one 409")`.
- Freeze time with `vi.useFakeTimers()` or an injected clock; never depend on the real date.
- Add `pnpm test` and a GitHub Actions workflow that runs it with a Postgres service container.
- Run the suite. Fix the app where the app is wrong and the test where the test is wrong. Report per-criterion pass/fail and what changed.

Calendly is a trademark of Calendly LLC. This page is independent research and is not affiliated with or endorsed by Calendly LLC. Last updated 2026-09-10.