Build Zeeg

KINDAback to the verdict

0%0 of 18 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 scheduling page whose difficulty is keeping two kinds of private information apart: the host's calendar, read as busy intervals and nothing more, and the visitor's details, routed by explicit rules to the right appointment and kept no longer than the appointment needs.

estimated effort multi-daythe files for this build are in the project pack

RuntimeTypeScript with PostgreSQLCalendarOne provider read as busy intervals through incremental syncTimeInstants in the database, rules in the host's zone, display in the visitor's zoneRoutingRules as ordered data with a default route and a test harnessRetentionVisitor data kept for a stated period then removed on a schedule

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 server, the sync loop and the reminder queue.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Reservation correctness depends on real transactions and row contention.

    Get it Docker or your package manager. open ↗

    Verify Two concurrent transactions reserving one slot serialise correctly

  3. free

    Why Incremental sync, token invalidation and scope choice are provider specifics that only appear against a real account, and a recurring series with a cancelled instance is where availability bugs show.

    Get it Create OAuth credentials in the provider's console, request the narrowest scope that reads busy time and writes your own events, and read the sync guide before writing the loop. open ↗

    Verify An incremental sync picks up an event added by hand

  4. free tier or pennies

    Why A reminder that never arrives is a missed appointment, and delivery has to be retried and recorded.

    Get it Any provider with an API key and a verified sender.

    Verify A test reminder arrives with the recipient's zone named

  5. decidefree

    Why How long visitor data lives and whose zone the rules are written in are policy decisions that belong written before the first booking.

    Get it Write down the period after an appointment at which visitor data is removed, and whether the host's zone is fixed or follows the calendar.

    Verify Both are in the configuration and stated on the confirmation

  6. have readyfree

    Why Overlapping rules and the default route only get tested against a form someone actually needs, with answers that fall between the cracks.

    Get it Write out the questions you ask new clients and where each answer should send them, including the awkward combinations.

    Verify The harness runs ten sample answer sets through the form

Data model

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

- calendars(id, provider, account_ref, scopes_json, sync_token, last_synced_at)
- busy(id, calendar_id, starts_at, ends_at, external_event_id, is_ours) · intervals only, never a title
- event_types(id, slug, name, duration_minutes, buffer_before, buffer_after, availability_rules_json, timezone, questions_json)
- routing_forms(id, slug, version, questions_json, rules_json, default_route_json) · rules are ordered and first match wins
- bookings(id, event_type_id, starts_at, ends_at, visitor_name, visitor_email, visitor_timezone, answers_json, routed_by, state, external_event_id, purge_at)
- holds(id, event_type_id, starts_at, expires_at, session_id) · counted against capacity while alive
- The busy table has no column for what an event is, so there is nothing to leak; the tool can only ever know that a period is taken.

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_URLsecretrequiredpostgres://localhost:5432/bookingBusy intervals, bookings and visitor answers.
CALENDAR_CLIENT_IDsecretrequireda provider client idReads busy time and writes only the tool's own events.
CALENDAR_CLIENT_SECRETsecretrequiredclient-secret-...redactedPaired with the client id. Never logged.
STORE_EVENT_CONTENTrequiredneverPresent as a constant. The tool knows when you are busy and never why.
HOST_TIMEZONErequiredEurope/BerlinAvailability rules are written in this zone and expanded with a timezone library.
HOLD_MINUTESrequired10Long enough to fill the form, short enough not to block a popular slot.
SYNC_STALE_MINUTESrequired30Beyond this availability narrows rather than being offered optimistically.
RETENTION_DAYS_AFTER_APPOINTMENTrequired30Visitor data is removed after this. Stated on the confirmation.

The build, in order

  1. Busy intervals, not events

    The calendar is read as taken time with nothing else stored, and it stays current.

    1. Asserted in the suite, including log output.

      Files src/calendar/sync.tstests/no-event-content.test.ts

    2. Identical query parameters every time, and no duplicates after the resync.

      Files src/calendar/tokens.tstests/resync.test.ts

    3. Fail closed. A double booking is worse than a missing slot.

      Files src/availability/stale.ts

    done when · tick each as it passes
    watch out
    • The provider invalidates sync tokens from time to time and answers with a 410, and a sync loop that treats that as a transient error retries forever while availability drifts from reality; the same guide notes that changing query parameters between requests earns a 400. Handle 410 with a full resync, keep parameters identical, and assert in the suite that no title, description, attendee or location is ever stored.
  2. Slots that are right in two timezones

    Every offered slot is free for the host and displayed correctly for the visitor, including across a clock change.

    1. With a timezone library, not arithmetic on local midnight.

      Files src/availability/expand.tssrc/availability/expand.test.ts

    2. Twice a year per zone is where scheduling tools are wrong.

      Files tests/dst.test.ts

    3. Never the busy map, verified on every response.

      Files src/public/slots.tstests/no-busy-leak.test.ts

    done when · tick each as it passes
    watch out
    • Generating slots by adding minutes to a local midnight breaks on the day the clocks change, offering a slot that does not exist or skipping an hour that does, once a year in each zone, which is rare enough to survive testing and common enough to hit real clients. Expand rules with a timezone library, test both transition days in both zones, and store instants.
  3. Booking once and writing it back

    Two visitors cannot take one slot, and the booking appears in the calendar without touching anything else.

    1. Check-then-insert double-books when a link is popular.

      Files src/booking/create.tstests/concurrency.test.ts

    2. Never touch a foreign event, tested by trying.

      Files src/calendar/write.tstests/foreign-events.test.ts

    3. Unguessable, single purpose, and showing both zones.

      Files src/booking/links.ts

    done when · tick each as it passes
    watch out
    • Checking availability and then inserting the booking is a race of a few hundred milliseconds, and it double-books exactly when a link is shared widely. Reserve inside the transaction and let the database serialise it, then write the calendar event after the commit, identified by its stored id.
  4. Routing forms as rules you can read

    A visitor answers a few questions and lands on the right appointment for a reason the owner can see.

    1. With a required default route.

      Files src/routing/evaluate.tssrc/routing/evaluate.test.ts

    2. Sample answers in, destination and fired rule out.

      Files src/routing/harness.ts

    3. So a client's path is explicable after an edit.

      Files src/routing/versions.ts

    done when · tick each as it passes
    watch out
    • Rules that overlap with no defined order route the same answers differently after an edit, and the owner cannot explain why a client landed on the wrong page. Order the rules explicitly, evaluate first match wins, require a default, and show which rule fired in the harness and on the booking.
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.