Build Zeeg
KINDAback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - accountfree
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 - accountfree 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/booking | Busy intervals, bookings and visitor answers. |
CALENDAR_CLIENT_IDsecret | required | a provider client id | Reads busy time and writes only the tool's own events. |
CALENDAR_CLIENT_SECRETsecret | required | client-secret-...redacted | Paired with the client id. Never logged. |
STORE_EVENT_CONTENT | required | never | Present as a constant. The tool knows when you are busy and never why. |
HOST_TIMEZONE | required | Europe/Berlin | Availability rules are written in this zone and expanded with a timezone library. |
HOLD_MINUTES | required | 10 | Long enough to fill the form, short enough not to block a popular slot. |
SYNC_STALE_MINUTES | required | 30 | Beyond this availability narrows rather than being offered optimistically. |
RETENTION_DAYS_AFTER_APPOINTMENT | required | 30 | Visitor data is removed after this. Stated on the confirmation. |
The build, in order
Busy intervals, not events
The calendar is read as taken time with nothing else stored, and it stays current.
Asserted in the suite, including log output.
Files
src/calendar/sync.tstests/no-event-content.test.tsIdentical query parameters every time, and no duplicates after the resync.
Files
src/calendar/tokens.tstests/resync.test.tsFail closed. A double booking is worse than a missing slot.
Files
src/availability/stale.ts
done when · tick each as it passeswatch 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.
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.
With a timezone library, not arithmetic on local midnight.
Files
src/availability/expand.tssrc/availability/expand.test.tsTwice a year per zone is where scheduling tools are wrong.
Files
tests/dst.test.tsNever the busy map, verified on every response.
Files
src/public/slots.tstests/no-busy-leak.test.ts
done when · tick each as it passeswatch 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.
Booking once and writing it back
Two visitors cannot take one slot, and the booking appears in the calendar without touching anything else.
Check-then-insert double-books when a link is popular.
Files
src/booking/create.tstests/concurrency.test.tsNever touch a foreign event, tested by trying.
Files
src/calendar/write.tstests/foreign-events.test.tsUnguessable, single purpose, and showing both zones.
Files
src/booking/links.ts
done when · tick each as it passeswatch 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.
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.
With a required default route.
Files
src/routing/evaluate.tssrc/routing/evaluate.test.tsSample answers in, destination and fired rule out.
Files
src/routing/harness.tsSo a client's path is explicable after an edit.
Files
src/routing/versions.ts
done when · tick each as it passeswatch 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.
Reminders, retention and running it in publicproduct builder
Both parties are reminded in their own zone, visitor data expires on schedule, and the public page survives the internet.
Named zone, calendar attachment, recorded delivery.
Files
src/notify/reminders.tsCollected for one appointment, removed after it.
Files
src/retention.tstests/retention.test.tsA page that cannot see the calendar must stop offering slots.
Files
src/calendar/health.tssrc/public/ratelimit.ts
done when · tick each as it passeswatch out- Keeping every visitor's name, email and answers forever turns a booking tool into a contact database nobody consented to, and a revoked calendar token that goes unnoticed leaves a page offering slots against a calendar it can no longer see. Retention runs on a schedule and is tested, and a revoked connection pauses the page.
That is the whole plan for Zeeg. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Team scheduling and round robin.
- Reading event details for any purpose.
- Payments at booking.
- Two-way sync of foreign events.
- race-free booking under load
- team routing and round robin
- reliable reminders and timezone edge cases
- calendar-provider edge cases and timezone correctness
- multi-calendar conflict handling
- Rescheduling links that carry the routing answers forward
- Round robin across two hosts, once single-host privacy is proven
- A follow-up message after the appointment, sent within the retention period
Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.