Build Calday

YESback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A personal booking page whose hard problems are holding a slot so two people cannot take it, computing availability without ever reading your event titles, and expanding a weekly rule through a timezone so the week of a clock change does not send everybody an hour wrong.

estimated effort weekendthe files for this build are in the project pack

RuntimeNext.js on a host with a stable public URL for the OAuth callback and the booking pageStoreSQLite with a unique index on (link_id, slot_start_utc)AvailabilityThe provider's free/busy query, batched, never a listing of eventsTimeInstants in UTC plus the IANA zone name stored beside every ruleNotificationOne transactional email per booking, sent after the calendar write succeeds

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the application.

    Get it nodejs.org or a version manager. open ↗

    Verify node --version prints v22 or higher

  2. API keyfree

    Why Free/busy is a narrower permission than reading events, and asking for the narrower one is the difference between a booking page that cannot leak your day and one that can.

    Get it Create a project in the provider's console, configure the consent screen and register the callback URL.

    Verify A free/busy call returns intervals and a call for event details is refused by the scope

  3. have readya few pounds a month, or free on a small tier

    Why The OAuth callback and the person booking both need to reach it, so a laptop behind a router will not do once this is real.

    Get it A small host or a tunnel with a stable hostname.

    Verify The callback completes and the booking page loads from another network

  4. API keyfree at personal volume

    Why Confirmations and cancellations are the part people notice, and sending them from a home address means they land in spam.

    Get it Any provider with an API and a verified sending domain.

    Verify A test confirmation with a calendar attachment arrives and the attachment opens

  5. decidefree

    Why Daylight saving bugs only appear on transition weeks and only in zones that have them, so pick the two zones your tests will cover before writing the expansion code.

    Get it Choose your own zone plus one in the opposite hemisphere.

    Verify Both appear in the test suite with a spring and an autumn case

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- links(id, slug, title, duration_minutes, buffer_before, buffer_after, min_notice_minutes, max_days_ahead, calendar_ids_json, owner_timezone, active)
- rules(id, link_id, weekday, start_local, end_local, timezone, effective_from, effective_to) · local wall time plus a zone name, never an offset
- overrides(id, link_id, date_local, kind, start_local, end_local) · a day off, or extra hours, expressed the same way
- holds(id, link_id, slot_start_utc, token, created_at, expires_at) · a short reservation taken when the form opens, so two people do not both reach submit
- bookings(id, link_id, slot_start_utc, slot_end_utc, invitee_name, invitee_email, invitee_timezone, note, state, calendar_event_id, ical_uid, sequence, created_at)
- freebusy_cache(calendar_id, window_start, window_end, busy_json, fetched_at, ttl_seconds, stale) · cached intervals with an explicit stale flag, because a stale cache must not be silently trusted
- The unique index on (link_id, slot_start_utc) in bookings is the entire double-booking defence. Everything above it, including the holds table, is an optimisation that makes the failure rarer and friendlier.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/booking.dbLinks, rules, holds and bookings. Back this up; it is the lock.
CALENDAR_CLIENT_IDrequired123456789-abc.apps.example.comThe registered application. Public half of the OAuth pair.
CALENDAR_CLIENT_SECRETsecretrequiredsecret-...redactedNever in the repository, never in a client bundle.
PUBLIC_BASE_URLrequiredhttps://book.example.comUsed for the callback and in every link you email. Must match the registered callback.
EMAIL_API_KEYsecretrequiredre_...redactedTransactional email. Confirmations, cancellations and reschedules.
FREEBUSY_TTL_SECONDSrequired120How long cached busy intervals are fresh. Past this they are stale, not current.
HOLD_MINUTESrequired10How long the form holds a slot. Long enough to type, short enough not to block.
INVITEE_RETENTION_DAYSrequired365After this the name, email and note are removed and the booking stays.

The build, in order

  1. Availability from rules and free/busy

    A public page shows slots computed from your rules minus your real busy time, with no event details anywhere in the response.

    1. It is the private answer and it is also the correct one.

      Files src/lib/availability.tstests/no-event-details.test.ts

    2. Two clear steps beat one clever query.

      Files src/lib/rules.tstests/rules.test.ts

    3. Check the current cap; do not copy this number from a blog post.

      Files src/lib/freebusy.ts

    done when · tick each as it passes
    watch out
    • Working out busy time by listing events gets the answer wrong twice over, because a calendar event carries a transparency field whose values decide whether it blocks time and an event set to transparent does not, while cancelled and declined entries are still in the list; the free/busy query applies that rule for you and hands back intervals with no titles attached, and it caps how many calendars one request will expand at a documented maximum, so batch your calendars rather than sending fifty in one call.
  2. A slot that can only be taken once

    Two simultaneous bookings for the same slot end with one confirmation and one clear refusal.

    1. The database is the only lock in this system.

      Files src/lib/book.tssrc/db/schema.sql

    2. It turns a race into a rare case rather than a common one.

      Files src/lib/holds.tstests/holds.test.ts

    3. The person booked; losing their booking is the worse outcome.

      Files src/lib/book.ts

    done when · tick each as it passes
    watch out
    • Creating the calendar event first and treating a successful response as the booking means two people who submit within the same second both get an event and both get a confirmation email, because nothing in the calendar API is a mutual exclusion; insert into your own table first, let the unique index reject the second one, and make the losing path a polite message with the next three free slots rather than a five hundred error.
  3. Timezones that survive the clock change

    A weekly rule renders correctly for both parties through a daylight saving transition.

    1. One offset for the week is wrong for half of it.

      Files src/lib/expand.tstests/dst.test.ts

    2. Skip it, and write down that you skipped it.

      Files src/lib/expand.ts

    3. Their browser may be somewhere else by the time you email them.

      Files src/db/schema.sql

    done when · tick each as it passes
    watch out
    • Computing today's UTC offset for your zone and adding it to every other day in the window shifts every slot after a clock change by an hour, which means a week of meetings sent out an hour wrong to people who then turn up an hour wrong, and you find out from them; expand each day separately through a zone-aware library, and write a test for the week containing each transition in your own zone and in one southern hemisphere zone.
  4. Confirmations, cancellations and reschedules

    Both calendars agree after a reschedule, and a cancellation removes the event rather than leaving a ghost.

    1. Check the current requirement in the calendaring spec your clients read.

      Files src/lib/ics.tstests/ics-sequence.test.ts

    2. Releasing then taking can leave both slots gone.

      Files src/lib/reschedule.ts

    3. They disagree about almost everything in this format.

      Files docs/ics-testing.md

    done when · tick each as it passes
    watch out
    • Sending a fresh calendar attachment with a new identifier when somebody reschedules leaves the old meeting sitting in their calendar next to the new one, and they will attend the wrong one; keep the same unique identifier for the life of the booking, raise the sequence number on each change the way the calendaring format your clients read requires, and check the behaviour against at least two mail clients because they disagree.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.