OneLinersCommand workbench
Guides
Services & Applications / Software Engineering / Security

Build an appointment-booking bot with conflict-safe Google Calendar writes

Create a deterministic booking service that returns Google Calendar availability in an explicit IANA time zone, rejects DST gaps and overlaps, proposes an exact appointment without writing, requires a short-lived user confirmation, rechecks conflicts under a serialized lock, creates one idempotent private event, and applies the same proposal/confirmation discipline to cancellation.

210 min13 stepsChanges system stateRevision 1
Save or explore
Save to collectionCreate a collection in the sidebar first.
0 of 13 steps completed
Goal

Offer and manage appointments without guessing local time, double-booking through ordinary concurrent requests, duplicating events after retries, exposing calendar details, or letting conversational input mutate an external calendar before the attendee confirms the exact instant and recipient.

Supported environments
  • Ubuntu Server 24.04 LTS
  • Node.js 22.13+
  • PostgreSQL 18
  • Google Calendar API v3
Prerequisites
  • Dedicated booking calendar Create or select one calendar owned by the service identity, document its organizer, default IANA time zone, sharing, downstream automations, and emergency owner. Use a disposable calendar for all fixtures.Confirm calendarList.get shows writer/owner access only for the intended appointments@example.com calendar.
  • Narrow OAuth consent Authorize only calendar.events.owned and calendar.events.freebusy for the dedicated owner where those scopes satisfy the design. Store the refresh token separately and complete required consent-screen verification before public use.Record client ID fingerprint, exact scopes, refresh-token owner, revocation owner, and calendar ID without recording token values.
  • Authoritative time-zone and business policy Choose the business IANA zone, supported attendee zones, hours, durations, lead time, booking horizon, buffers, holidays, and DST behavior with an accountable scheduling owner.Review spring-forward and fall-back dates in Europe/Warsaw, America/New_York, and every supported business region.
  • Transactional state and identity Provide PostgreSQL for idempotency/proposal/audit state and an authenticated actor identity for confirmations and cancellations. A client-visible token is not a substitute for actor authentication.psql -X "$DATABASE_URL" -c "SELECT current_database(), current_user;"
Operating boundary

OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.

Full guide

What you will build

System
  • A deterministic availability API over one owned Google Calendar that exposes busy-derived slots without event details and labels every result as unreserved.
  • An exact, short-lived appointment proposal showing attendee, local time, IANA zone, UTC interval, duration, and title before any external write.
  • A serialized confirmation path with current free/busy recheck, stable Calendar event ID, attendee notification, safe retry recovery, and actor audit.
  • A separate cancellation proposal/confirmation with event refetch, ETag drift rejection, attendee update, backup/recovery, and OAuth teardown.
Observable outcome
  • Ambiguous or nonexistent DST wall times, unsupported zones, out-of-policy slots, stale proposals, and newly busy intervals fail safely.
  • Concurrent and retried confirmations resolve to one provider event and one invitation, including provider-success/local-failure recovery.
  • Every insertion or deletion is tied to authenticated actor evidence and an exact short-lived token displayed after a non-writing proposal.
  • Operators can monitor quota/conflict/idempotency invariants, reconcile restores, revoke provider authority, and preserve legitimate events during decommissioning.

Architecture

How the parts fit together

A trusted HTTPS edge authenticates users and forwards bounded requests to a loopback deterministic service. Google free/busy is the availability source, Temporal resolves regional time, PostgreSQL stores unique proposals/audit and serializes confirmations, and Google events receives only fixed-calendar insert/get/delete calls after exact human confirmation.

Trusted ingressTerminate TLS, authenticate actor, enforce origin/CSRF, limits, request IDs, body size, and fixed routes.
Time and policy engineCreate business-zone candidate instants, reject DST ambiguity, apply lead/horizon/duration/buffer rules, and render attendee zone plus UTC.
Google Calendar adapterRead fixed-calendar free/busy and get/insert/delete one stable event with explicit notifications.
Transactional stateStore unique idempotency, token hashes, exact evidence, provider ID/ETag, status, actor, expiry, and audit.
Review boundaryKeep proposal/cancellation proposal non-writing and require actor plus exact token for each external mutation.
  1. User supplies explicit date, attendee zone, duration, identity, and idempotency key; the policy engine creates candidate instants.
  2. Free/busy returns busy intervals only; errors fail closed and buffers remove unsafe candidates.
  3. User selects an exact rendered slot; proposal revalidates policy and stores a token hash without Calendar write.
  4. On confirmation, a database lock serializes the calendar lane, the stable event is checked, and free/busy is rechecked.
  5. One private event with attendee notification and private proposal marker is inserted or recovered by stable ID, then local state/audit commits.
  6. Cancellation follows a new proposal/token, refetches the stable event, rejects ETag drift, deletes with updates, and records the actor.

Assumptions

  • One dedicated calendar and single booking policy are sufficient for the pilot; multiple resources require a transactional resource allocator and per-resource ownership.
  • Supported zones use IANA identifiers and the organization accepts explicit rejection for repeated/nonexistent wall times.
  • Authenticated users can confirm their own appointment and cancellation; delegated staff action needs separately documented authority.
  • Google OAuth consent and sharing permit free/busy plus owned-event insertion/deletion on the fixed calendar only.
  • PostgreSQL is available for global idempotency, serialized confirmation, status, and audit across app instances.
  • The narrow external race between final free/busy and Calendar insert is accepted/monitored because Calendar does not provide an atomic free-if-unoccupied insert.

Key concepts

IANA time zone
Regional rule name such as Europe/Warsaw that captures historical/future offset changes, unlike an abbreviation or fixed offset.
DST disambiguation
Explicit behavior when local time is missing or repeated; this guide rejects both rather than choosing silently.
Free/busy
Busy intervals without event details. Missing/error data is not proof of availability.
Idempotency key
Client operation identity stored uniquely so retries return the same proposal.
Stable event ID
Provider-compatible ID derived from proposal identity, allowing 409/retry recovery without a second invitation.
ETag cancellation guard
Current event version evidence; a changed ETag requires a new human review.
Ambiguous outcome
Provider may have committed while local response/state failed; reconcile stable identity before retrying.

Before you copy

Values used in this guide

{{DATABASE_URL}}secret

TLS PostgreSQL runtime connection for proposal/audit state.

Example: managed booking database secret
{{GOOGLE_CLIENT_ID}}

OAuth web/server client identifier for the booking project.

Example: 123.apps.googleusercontent.com
{{GOOGLE_CLIENT_SECRET}}secret

OAuth client secret stored only in runtime secret management.

Example: managed Google client secret
{{GOOGLE_REFRESH_TOKEN}}secret

Dedicated calendar-owner refresh token for reviewed scopes.

Example: managed refresh-token reference
{{CONFIRMATION_HMAC_SECRET}}secret

Independent high-entropy key hashing booking/cancellation bearer tokens.

Example: managed HMAC key v1
{{BOOKING_POLICY_PATH}}

Root-owned zones/hours/durations/limits/calendar policy.

Example: /opt/appointment-bot/config/booking-policy.json

Security and production boundaries

  • Availability is sensitive; return slot times, not titles, descriptions, attendees, organizers, or reasons for busyness.
  • Fixed calendar ID, provider-method allowlist, OAuth scopes, and runtime tests jointly constrain a credential broader than one insert/delete.
  • Confirmation and cancellation require authenticated actor plus timing-safe token verification; tokens never appear in URLs/logs/analytics.
  • Reject ambiguous zones/local times and display local zone plus UTC at proposal, confirmation, and cancellation.
  • Stable provider identity and private proposal marker recover ambiguous inserts; never generate a fresh provider ID on retry.
  • A final free/busy check cannot eliminate changes by external calendar writers after the check; serialize local writes, monitor conflicts, and reconcile rather than silently moving appointments.
  • sendUpdates is explicit for insertion and deletion so attendees receive lifecycle changes; test delivery with consenting addresses.
  • Policy, personal proposal data, audit, backups, and restores require access, encryption, retention, deletion, and incident ownership.

Stop before continuing if

  • Stop if calendar/scope/token owner or downstream notifications are unknown, or arbitrary calendars/provider methods become reachable.
  • Stop on ambiguous/unsupported time, freeBusy errors, out-of-policy slot, expired/mismatched token, unauthenticated actor, or ETag drift.
  • Stop public confirmation when idempotency/locking/audit/database is unavailable, provider/local outcomes diverge, or duplicate invariant fails.
  • Stop when quota retries are unbounded, attendee data enters telemetry, or cancellation would surprise a changed event owner.
  • Stop teardown deletion until legitimate events, in-flight operations, holds, backups, and OAuth revocation are reconciled.
01

warning

Define the exact proposal, confirmation, and cancellation boundary

read-only

Document four separate operations: availability reads; a non-writing proposal that displays attendee email, local wall time, IANA zone, UTC instant, duration, title, and expiry; a confirmed event insertion after actor authentication and conflict recheck; and a separately proposed/confirmed cancellation after refetching the current event. The bot never moves, patches, mass-deletes, invites arbitrary lists, or chooses a time silently.

Why this step matters

Calendar writes notify people, disclose relationships, block scarce time, and may trigger conferencing or workflows, so a conversational interpretation must remain a proposal until a human sees the exact external effect.

What to understand

Do not describe an availability result as held or booked. Another organizer may consume it before confirmation; display notReserved=true and an explicit expiry.

The attendee confirms an instant and recipient, not ambiguous words such as tomorrow morning. Show both local time/zone and UTC, and repeat them in the resulting confirmation.

Cancellation deletes an external event and sends updates. Require a fresh cancellation token, current event ETag, actor identity, and exact event/time preview.

System changes

  • Creates an authority matrix, data-retention decision, actor ownership, and external-write approval policy.
  • Defines event insertion and deletion as the only supported provider mutations.
Example output / evidence
Availability: read only; not reserved
Proposal: 2026-10-27 09:30 America/New_York = 2026-10-27T13:30:00Z
Attendee: alex@example.com; duration 30 minutes
Calendar written: false
Confirmation required: actor identity + exact short-lived token
Cancellation: second proposal and confirmation

Checkpoint: Approve the booking lifecycle

Continue whenScheduling, privacy, support, and security owners accept the four phases, exact confirmation display, conflict behavior, notification policy, and cancellation review.

Stop whenStop if the product calls suggestions reservations, accepts ambiguous zones, auto-confirms, edits arbitrary events, sends invitations before review, or cannot authenticate the confirming/cancelling actor.

If this step fails

Product copy says 'reserved' immediately after availability lookup.

Likely causeThe read-only free/busy result is being mistaken for a provider hold.

Safe checks
  • Inspect network/provider writes during availability.
  • Review API response field notReserved and UI copy.

ResolutionRename it to available now/not reserved, shorten proposal lifetime, and reserve only through the confirmed insertion path.

Security notes

  • Calendar availability is personal data even when event details are hidden; expose the minimum slot result to the requester.
  • Do not let titles, emails, zones, or idempotency keys select calendar IDs, API methods, notification recipients, or arbitrary URLs.

Alternatives

  • Use Google Calendar appointment schedules when the built-in workflow meets policy and customization needs.
  • Route complex rescheduling to a human coordinator instead of adding generic event patch capability.

Stop conditions

  • Stop if the product calls suggestions reservations, accepts ambiguous zones, auto-confirms, edits arbitrary events, sends invitations before review, or cannot authenticate the confirming/cancelling actor.
02

instruction

Configure one owned calendar and narrow Google OAuth scopes

read-only

Create a Google Cloud OAuth client, configure consent, request the narrow owned-event and free/busy scopes, authorize the dedicated booking calendar owner, and store the resulting refresh token in managed secrets. Verify the calendar access role and default zone. Do not use the broad calendar scope or domain-wide delegation for this single-calendar design.

Why this step matters

Calendar API scopes are capabilities visible during user consent; choosing narrowly focused scopes and one owned calendar reduces exposure if the application credential is compromised.

What to understand

The write scope still allows more event operations than this implementation exposes. Tests and code permit insert, get, freebusy, and delete only; no patch/update/move/import method exists.

Use an OAuth web/server flow appropriate to the deployment and protect client secret plus refresh token. Record revocation through Google Account/Cloud administration.

The freeBusy response contains busy intervals and per-calendar errors. Treat missing calendar data or any error as unavailable, never as free.

System changes

  • Creates an OAuth client, consent grant, refresh token, and access to one owned calendar.
  • Introduces external read/write authority that must be rotated and revoked during incidents or teardown.

Syntax explained

calendar.events.owned
Views and edits events on calendars owned by the authenticated user without broad calendar-management authority.
calendar.events.freebusy
Reads availability without retrieving event titles or descriptions.
one fixed calendarId
Prevents callers from pivoting the credential to another accessible calendar.
Example output / evidence
calendarId: appointments@example.com
accessRole: owner
timeZone: Europe/Warsaw
OAuth scopes: calendar.events.owned, calendar.events.freebusy
refresh token stored: managed secret reference
broad calendar/domain delegation: absent

Checkpoint: Verify OAuth and calendar scope

Continue whenThe owner can read free/busy and create/delete a fixture only on the dedicated calendar; unrelated calendars and management operations remain unavailable.

Stop whenStop if consent requests broad calendar access without need, refresh token ownership is unclear, the service can reach unrelated calendars, or calendar sharing exposes private event details.

If this step fails

freeBusy works but event insertion returns forbidden.

Likely causeThe token has only a read/freebusy scope or the calendar accessRole is not writer/owner.

Safe checks
  • Inspect granted scopes without printing tokens.
  • Call calendarList.get for the fixed calendar and review accessRole.

ResolutionRepeat consent with the reviewed owned-event scope and correct calendar sharing; do not broaden to all-calendar management.

Security notes

  • Never log authorization headers, access tokens, refresh tokens, client secrets, or full OAuth callback URLs.
  • Public applications may require Google verification for requested user-data scopes; plan this before launch.

Stop conditions

  • Stop if consent requests broad calendar access without need, refresh token ownership is unclear, the service can reach unrelated calendars, or calendar sharing exposes private event details.
03

config

Encode IANA zones, business hours, buffers, and booking horizon

caution

Create the root-owned booking policy. It lists explicit IANA zones, weekday windows in the business zone, allowed durations, lead/horizon limits, ten-minute buffers, ten-minute proposal/cancellation expiry, response size, notification behavior, private visibility, request budget, and retention. Holidays should be represented as busy calendar events or added to a reviewed policy revision.

Why this step matters

Time conversion and availability must follow an explicit operational policy, not locale defaults, browser offsets, abbreviations, or model guesses that drift across environments and daylight-saving transitions.

What to understand

IANA zone names express regional rules; a numeric offset alone does not describe future DST changes. Reject abbreviations such as CST and unknown zones.

Business hours are interpreted in the business zone, then each candidate instant is displayed in the attendee zone. A date crossing midnight for the attendee is expected and must be shown.

Temporal disambiguation reject makes nonexistent spring-forward times and repeated fall-back wall times explicit errors requiring the user to choose another exact instant.

System changes

  • Creates one versioned source for time zones, hours, buffers, limits, notification, visibility, and retention.
  • Converts unsupported/ambiguous inputs into safe validation failures rather than guessed appointments.

Syntax explained

allowedTimeZones
Bounds accepted regional zones and prevents uncontrolled time-zone identifiers.
bufferBefore/After
Treats adjacent busy intervals as conflicts around the appointment.
sendUpdates: all
Requests attendee notifications for the confirmed insertion/deletion; never use none silently.
File /opt/appointment-bot/config/booking-policy.json
Configuration
{
  "calendarId": "appointments@example.com",
  "businessTimeZone": "Europe/Warsaw",
  "allowedTimeZones": [
    "Europe/Warsaw", "Europe/London", "America/New_York",
    "America/Los_Angeles", "Asia/Singapore", "Australia/Sydney"
  ],
  "businessHours": {
    "1": [["09:00", "17:00"]],
    "2": [["09:00", "17:00"]],
    "3": [["09:00", "17:00"]],
    "4": [["09:00", "17:00"]],
    "5": [["09:00", "15:00"]]
  },
  "allowedDurationsMinutes": [30, 45, 60],
  "minimumLeadMinutes": 120,
  "maximumDaysAhead": 60,
  "bufferBeforeMinutes": 10,
  "bufferAfterMinutes": 10,
  "proposalTtlMinutes": 10,
  "maximumSlotsReturned": 8,
  "maximumRequestsPerIpPerMinute": 20,
  "sendUpdates": "all",
  "eventVisibility": "private",
  "retentionDays": 30
}
Example output / evidence
business zone: Europe/Warsaw
supported zones: 6
durations: 30,45,60
lead/horizon: 120 minutes / 60 days
buffers: 10 minutes before and after
proposal TTL: 10 minutes
visibility: private; sendUpdates: all

Checkpoint: Test DST and midnight boundaries

Continue whenFixtures cover spring gap, fall overlap, date crossing, leap day, year end, business-zone DST, attendee-zone DST, lead time, horizon, and buffers.

Stop whenStop if any time depends on host/browser locale, a zone abbreviation, floating local time, implicit disambiguation, or an unreviewed holiday/business-hours assumption.

If this step fails

A requested local time exists twice on fall-back day.

Likely causeThe wall clock repeats when DST ends and no offset/instant distinguishes occurrences.

Safe checks
  • Resolve with Temporal disambiguation reject.
  • Show both possible offsets to a human if product later supports explicit selection.

ResolutionReject the proposal and ask for another unambiguous slot or an explicitly designed offset-selection flow.

Security notes

  • Policy updates invalidate outstanding proposals in a production implementation by storing and comparing a policy hash.
  • Do not infer time zone from IP, phone number, email domain, or language without explicit user confirmation.

Stop conditions

  • Stop if any time depends on host/browser locale, a zone abbreviation, floating local time, implicit disambiguation, or an unreviewed holiday/business-hours assumption.
04

config

Create transactional idempotency, proposal, event, and audit state

caution

Apply the PostgreSQL migration. A unique idempotency key prevents duplicate proposal creation, provider event IDs are unique, proposals hold exact UTC/local-zone evidence and token hashes, and audit records external outcomes without storing raw confirmation tokens. Restrict tables and sequences from PUBLIC.

Why this step matters

Durable uniqueness and row locking make retries and crashes observable, while hashed confirmation tokens and append-only audit keep authority separate from client-visible secrets.

What to understand

Run migrations through a dedicated deployment role; the application role receives only the required SELECT/INSERT/UPDATE on these tables and INSERT on audit.

Store attendee email because Google needs an invite address, but classify and retain it minimally. Idempotency keys are hashed in audit and raw only in protected proposal state.

For multiple app instances, the confirmation transaction takes a calendar-scoped PostgreSQL advisory lock before rechecking free/busy and inserting, serializing local confirmations.

System changes

  • Creates protected proposal and audit tables with uniqueness, status, expiry, provider identity, ETag, and actor fields.
  • Introduces personal data state subject to retention, access, backup, and deletion obligations.

Syntax explained

UNIQUE idempotency_key
Returns the original proposal for a repeated client operation rather than creating another.
UNIQUE google_event_id
Prevents two local proposals from claiming the same provider event.
confirmation_token_hash
Stores a keyed digest instead of the bearer confirmation/cancellation token.
File /opt/appointment-bot/db/001_booking.sql
Configuration
\set ON_ERROR_STOP on
CREATE TABLE IF NOT EXISTS booking_proposals (
  id uuid PRIMARY KEY,
  idempotency_key text NOT NULL UNIQUE,
  calendar_id text NOT NULL,
  attendee_email text NOT NULL,
  title text NOT NULL,
  start_utc timestamptz NOT NULL,
  end_utc timestamptz NOT NULL,
  attendee_time_zone text NOT NULL,
  local_start_text text NOT NULL,
  confirmation_token_hash text NOT NULL,
  status text NOT NULL CHECK (status IN (
    'proposed', 'confirmed', 'expired', 'cancel-proposed', 'cancelled', 'failed'
  )),
  google_event_id text UNIQUE,
  google_event_etag text,
  created_at timestamptz NOT NULL DEFAULT now(),
  expires_at timestamptz NOT NULL,
  confirmed_at timestamptz,
  cancelled_at timestamptz,
  reviewer_id text,
  last_error_code text,
  CHECK (end_utc > start_utc)
);

CREATE INDEX IF NOT EXISTS booking_proposals_calendar_time_idx
  ON booking_proposals (calendar_id, start_utc, end_utc);
CREATE INDEX IF NOT EXISTS booking_proposals_expiry_idx
  ON booking_proposals (status, expires_at);

CREATE TABLE IF NOT EXISTS booking_audit (
  sequence bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  occurred_at timestamptz NOT NULL DEFAULT now(),
  event_type text NOT NULL,
  proposal_id uuid,
  actor_id text,
  idempotency_key_hash text,
  calendar_event_id text,
  start_utc timestamptz,
  end_utc timestamptz,
  outcome text NOT NULL,
  metadata jsonb NOT NULL DEFAULT '{}'::jsonb
);

REVOKE ALL ON booking_proposals, booking_audit FROM PUBLIC;
REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM PUBLIC;
Example output / evidence
CREATE TABLE booking_proposals
CREATE INDEX booking_proposals_calendar_time_idx
CREATE TABLE booking_audit
REVOKE
proposal idempotency unique: true
provider event id unique: true

Checkpoint: Exercise constraints concurrently

Continue whenParallel inserts with the same idempotency key produce one proposal, invalid status/range rows fail, and PUBLIC cannot read/write state.

Stop whenStop if keys/tokens are unprotected, idempotency is process memory only, actor identity is absent, audit can be overwritten by the app, or parallel operations create duplicates.

If this step fails

Two concurrent proposal requests return different IDs for one idempotency key.

Likely causeThe unique constraint is missing, a different database is used, or conflict handling occurs only in application memory.

Safe checks
  • Inspect table constraint and connection target.
  • Query protected state by exact idempotency key.

ResolutionDisable confirmation, restore the unique constraint, reconcile duplicate proposals, and retain one canonical operation.

Security notes

  • Confirmation and cancellation tokens are bearer secrets; never store plaintext or place them in URLs, analytics, referrers, or support logs.
  • Database backups contain attendee identity and timing; encrypt and restrict them.

Stop conditions

  • Stop if keys/tokens are unprotected, idempotency is process memory only, actor identity is absent, audit can be overwritten by the app, or parallel operations create duplicates.
05

command

Create an unprivileged runtime and protected directories

caution

Create appointment-bot without a login shell, root-owned application and configuration directories, and a private state directory. Install environment secrets with mode 0600, route HTTPS through a reverse proxy to loopback, and never expose the Node listener directly.

Why this step matters

Runtime isolation prevents a compromised network-facing process from replacing its policy, server code, migration, service definition, or secret file.

What to understand

The reverse proxy enforces TLS, host/path, method, body size, request-rate, and authenticated actor headers. Do not trust public client-supplied actorId directly in production.

Separate database migration credentials from runtime credentials. The app never needs CREATE/ALTER/DROP or calendar administration.

Use encrypted disk and keep local state only for operational exports; canonical proposals live in PostgreSQL.

System changes

  • Creates an unprivileged account and filesystem trust boundaries.
  • Defines a loopback-only application endpoint behind controlled HTTPS ingress.

Syntax explained

nologin
Prevents ordinary interactive service-account login.
root-owned /opt and /etc
Makes deployed code/policy/secrets non-writable by runtime.
0700 state
Restricts operational exports and temporary artifacts.
Command
sudo useradd --system --home /nonexistent --shell /usr/sbin/nologin appointment-bot && sudo install -d -m 0750 -o root -g appointment-bot /opt/appointment-bot /etc/appointment-bot && sudo install -d -m 0700 -o appointment-bot -g appointment-bot /var/lib/appointment-bot && sudo -u appointment-bot test -w /var/lib/appointment-bot && sudo -u appointment-bot test ! -w /opt/appointment-bot
Example output / evidence
appointment-bot shell: /usr/sbin/nologin
state write: allowed
application/config write: denied
listener target: 127.0.0.1:8080

Checkpoint: Verify negative permissions and ingress

sudo useradd --system --home /nonexistent --shell /usr/sbin/nologin appointment-bot && sudo install -d -m 0750 -o root -g appointment-bot /opt/appointment-bot /etc/appointment-bot && sudo install -d -m 0700 -o appointment-bot -g appointment-bot /var/lib/appointment-bot && sudo -u appointment-bot test -w /var/lib/appointment-bot && sudo -u appointment-bot test ! -w /opt/appointment-bot

Continue whenRuntime writes only approved state, cannot read unrelated secrets or modify code, and only the reverse proxy can reach loopback.

Stop whenStop if Node binds publicly, runtime owns code/policy, actor identity is client-controlled, or secrets appear in process arguments/logs.

If this step fails

The service starts only when run as root.

Likely causeFiles, ports, or secret permissions were designed around privilege rather than explicit access.

Safe checks
  • Inspect namei -l for required files.
  • Check listener port and runtime environment ownership.

ResolutionCorrect ownership/group access and use an unprivileged high port; never retain root as the workaround.

Security notes

  • Do not use a developer checkout as production working directory.
  • Authenticate requests and enforce CSRF/origin controls if confirmations are browser based.

Stop conditions

  • Stop if Node binds publicly, runtime owns code/policy, actor identity is client-controlled, or secrets appear in process arguments/logs.
06

config

Pin the calendar, database, time, HTTP, and validation runtime

caution

Create the exact package manifest and reviewed lockfile. Temporal handles IANA conversion with explicit disambiguation, Google APIs handle free/busy and events, pg persists transactional state, Zod bounds request shape, and Express exposes only named routes.

Why this step matters

Pinned direct dependencies and a lockfile make time conversion, OAuth, provider calls, request parsing, and transaction behavior reproducible across evaluation and deployment.

What to understand

Build without production secrets and deploy root-owned artifacts. Review lifecycle scripts and lockfile diffs; runtime does not run npm install.

Do not replace Temporal with Date parsing or host locale helpers. Date silently normalizes invalid/ambiguous local time and creates environment-dependent behavior.

The service is deterministic and does not need AI to select a slot. Conversational clients must convert language into this strict API, then show its exact proposal unchanged for confirmation.

System changes

  • Defines the application dependency graph and start/test entry points.
  • Keeps natural-language interpretation outside the calendar write authority.

Syntax explained

@js-temporal/polyfill
Provides explicit IANA zone conversion and DST disambiguation.
googleapis
Uses OAuth-authenticated Calendar v3 freebusy/events methods.
zod strict schemas
Rejects unknown request fields rather than accepting hidden controls.
File /opt/appointment-bot/package.json
Configuration
{
  "name": "reviewed-appointment-booking-bot",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22.13.0" },
  "scripts": {
    "start": "node src/server.mjs",
    "test": "node --test test/*.test.mjs"
  },
  "dependencies": {
    "@js-temporal/polyfill": "0.5.1",
    "express": "5.2.1",
    "googleapis": "173.0.0",
    "pg": "8.22.0",
    "zod": "4.4.3"
  }
}
Example output / evidence
npm ci
added 126 packages
npm test
# tests 7
# pass 7
# fail 0
Node.js v22.13.1

Checkpoint: Reproduce clean install and offline tests

Continue whennpm ci matches lockfile, tests need no real secrets/network, and changing time/idempotency/write methods fails fixtures.

Stop whenStop if build needs production tokens, lockfile changes, native scripts are unreviewed, or a dependency permits arbitrary provider methods from request input.

If this step fails

DST fixtures differ across build hosts.

Likely causeHost Date/locale or inconsistent time-zone data is still used.

Safe checks
  • Search runtime for new Date parsing of local wall time.
  • Compare Temporal/time-zone dependency versions.

ResolutionRestore one pinned Temporal path, refresh the lockfile deliberately, and rerun every zone transition fixture.

Security notes

  • Dependency upgrades are behavioral changes and require time/provider/security regression tests.
  • Never expose generic proxying to Google API endpoints.

Stop conditions

  • Stop if build needs production tokens, lockfile changes, native scripts are unreviewed, or a dependency permits arbitrary provider methods from request input.
07

config

Implement deterministic availability, proposal, confirmation, and cancellation

caution

Install the reference server. Availability creates policy slots in the business zone, converts them to the attendee zone, queries free/busy, applies buffers/lead/horizon, and returns notReserved. Proposal validates the selected slot and stores a hashed short-lived token without calendar write. Confirmation serializes by calendar, refetches or creates a stable provider event, handles 409 idempotently, sends updates, and audits. Cancellation has its own proposal, token, ETag check, delete, notification, and audit.

Why this step matters

The service keeps every provider mutation behind an exact, expiring human decision and combines stable provider identity with serialized conflict recheck to make retries safe.

What to understand

Availability errors are fail-closed: missing/per-calendar freeBusy errors mean no slots. Busy end is exclusive and start inclusive; buffers widen the candidate before overlap comparison.

The proposal endpoint recalculates the selected instant against business hours; callers cannot submit a visually plausible but out-of-policy localStart.

Before insertion, the transaction locks the calendar lane, checks whether the stable event already exists, and if not reruns free/busy. Provider 409 retrieves and validates private proposal metadata instead of generating another ID.

Google Calendar cannot guarantee no external actor changes the calendar between free/busy and insert. The narrow race is monitored; local confirmations serialize, and operators reconcile any conflict rather than moving events silently.

System changes

  • Adds bounded calendar availability reads and protected proposal/audit state.
  • Adds only confirmed private event insertion and separately confirmed event deletion with attendee updates.

Syntax explained

disambiguation: reject
Rejects nonexistent or repeated local wall times rather than choosing an offset.
pg_advisory_xact_lock
Serializes local confirmations for the fixed calendar during conflict recheck and insert.
stableEventId
Maps one proposal to one legal Calendar event ID across network retries.
extendedProperties.private
Lets a recovered insertion prove it belongs to the same proposal without public event metadata.
File /opt/appointment-bot/src/server.mjs
Configuration
import crypto from "node:crypto";
import fs from "node:fs/promises";
import express from "express";
import { google } from "googleapis";
import { Pool } from "pg";
import { Temporal } from "@js-temporal/polyfill";
import { z } from "zod";

const POLICY = JSON.parse(
  await fs.readFile(process.env.BOOKING_POLICY_PATH || "./config/booking-policy.json", "utf8")
);
for (const name of [
  "DATABASE_URL", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET",
  "GOOGLE_REFRESH_TOKEN", "CONFIRMATION_HMAC_SECRET"
]) {
  if (!process.env[name]) throw new Error("Missing required secret: " + name);
}
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 3000,
  statement_timeout: 5000,
  application_name: "appointment-booking-bot"
});
const oauth = new google.auth.OAuth2(
  process.env.GOOGLE_CLIENT_ID,
  process.env.GOOGLE_CLIENT_SECRET
);
oauth.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN });
const calendar = google.calendar({ version: "v3", auth: oauth });
const app = express();
app.set("trust proxy", false);
app.use(express.json({ limit: "24kb", strict: true }));

const requestSchema = z.object({
  attendeeEmail: z.string().email().max(254),
  title: z.string().min(3).max(120),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  timeZone: z.string().max(64),
  durationMinutes: z.number().int(),
  idempotencyKey: z.string().regex(/^[A-Za-z0-9_-]{16,100}$/)
}).strict();
const confirmSchema = z.object({
  proposalId: z.string().uuid(),
  confirmationToken: z.string().min(32).max(200),
  actorId: z.string().min(3).max(120),
  idempotencyKey: z.string().regex(/^[A-Za-z0-9_-]{16,100}$/)
}).strict();
const cancelSchema = z.object({
  proposalId: z.string().uuid(),
  actorId: z.string().min(3).max(120),
  cancellationToken: z.string().min(32).max(200)
}).strict();

const tokenHash = (token) => crypto
  .createHmac("sha256", process.env.CONFIRMATION_HMAC_SECRET)
  .update(token).digest("hex");
const stableEventId = (proposalId) => crypto
  .createHash("sha256").update("booking:" + proposalId).digest("hex").slice(0, 32);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function withRetry(operation) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    try { return await operation(); }
    catch (error) {
      if (![403, 429, 500, 502, 503, 504].includes(error?.code) || attempt === 3) throw error;
      await sleep(Math.min(32000, 1000 * 2 ** attempt) + crypto.randomInt(0, 1000));
    }
  }
}
function localToInstant(date, time, timeZone) {
  if (!POLICY.allowedTimeZones.includes(timeZone)) throw new Error("UNSUPPORTED_TIME_ZONE");
  const plain = Temporal.PlainDateTime.from(date + "T" + time);
  return plain.toZonedDateTime(timeZone, { disambiguation: "reject" });
}
function slotsForDay(date, timeZone, duration) {
  if (!POLICY.allowedDurationsMinutes.includes(duration)) throw new Error("UNSUPPORTED_DURATION");
  const businessDate = Temporal.PlainDate.from(date);
  const weekday = String(businessDate.dayOfWeek);
  const windows = POLICY.businessHours[weekday] || [];
  const slots = [];
  for (const [from, to] of windows) {
    let cursor = localToInstant(date, from, POLICY.businessTimeZone);
    const endWindow = localToInstant(date, to, POLICY.businessTimeZone);
    while (Temporal.ZonedDateTime.compare(
      cursor.add({ minutes: duration }),
      endWindow
    ) <= 0) {
      const attendee = cursor.withTimeZone(timeZone);
      slots.push({
        localStart: attendee.toPlainDateTime().toString(),
        timeZone,
        startUtc: cursor.toInstant().toString(),
        endUtc: cursor.add({ minutes: duration }).toInstant().toString()
      });
      cursor = cursor.add({ minutes: 15 });
    }
  }
  return slots;
}
async function busyRanges(slots) {
  if (!slots.length) return [];
  const response = await withRetry(() => calendar.freebusy.query({
    requestBody: {
      timeMin: slots[0].startUtc,
      timeMax: slots.at(-1).endUtc,
      timeZone: "UTC",
      items: [{ id: POLICY.calendarId }]
    }
  }));
  const entry = response.data.calendars?.[POLICY.calendarId];
  if (!entry || entry.errors?.length) throw new Error("FREEBUSY_INCOMPLETE");
  return entry.busy || [];
}
function overlaps(slot, busy) {
  const start = Temporal.Instant.from(slot.startUtc).subtract({
    minutes: POLICY.bufferBeforeMinutes
  });
  const end = Temporal.Instant.from(slot.endUtc).add({
    minutes: POLICY.bufferAfterMinutes
  });
  return busy.some((range) =>
    Temporal.Instant.compare(start, Temporal.Instant.from(range.end)) < 0 &&
    Temporal.Instant.compare(end, Temporal.Instant.from(range.start)) > 0
  );
}

app.post("/v1/availability", async (req, res) => {
  try {
    const input = requestSchema.parse(req.body);
    const today = Temporal.Now.plainDateISO(POLICY.businessTimeZone);
    const requested = Temporal.PlainDate.from(input.date);
    const days = requested.since(today).days;
    if (days < 0 || days > POLICY.maximumDaysAhead) throw new Error("DATE_OUTSIDE_POLICY");
    const candidates = slotsForDay(input.date, input.timeZone, input.durationMinutes);
    const busy = await busyRanges(candidates);
    const nowPlusLead = Temporal.Now.instant().add({ minutes: POLICY.minimumLeadMinutes });
    const slots = candidates.filter((slot) =>
      Temporal.Instant.compare(Temporal.Instant.from(slot.startUtc), nowPlusLead) >= 0 &&
      !overlaps(slot, busy)
    ).slice(0, POLICY.maximumSlotsReturned);
    res.json({ slots, zone: input.timeZone, notReserved: true });
  } catch (error) {
    res.status(400).json({ error: error.message || "INVALID_REQUEST" });
  }
});

app.post("/v1/proposals", async (req, res) => {
  const client = await pool.connect();
  try {
    const input = requestSchema.extend({
      localStart: z.string().min(16).max(40)
    }).parse(req.body);
    const start = localToInstant(
      input.localStart.slice(0, 10),
      input.localStart.slice(11, 16),
      input.timeZone
    );
    const end = start.add({ minutes: input.durationMinutes });
    const businessDate = start.withTimeZone(POLICY.businessTimeZone)
      .toPlainDate().toString();
    const allowedSlot = slotsForDay(
      businessDate,
      input.timeZone,
      input.durationMinutes
    ).some((slot) => slot.startUtc === start.toInstant().toString());
    if (!allowedSlot) throw new Error("SLOT_OUTSIDE_BUSINESS_POLICY");
    const token = crypto.randomBytes(32).toString("base64url");
    const id = crypto.randomUUID();
    await client.query("BEGIN");
    const existing = await client.query(
      "SELECT id, status, expires_at FROM booking_proposals WHERE idempotency_key=$1",
      [input.idempotencyKey]
    );
    if (existing.rows[0]) {
      await client.query("COMMIT");
      res.status(200).json({ proposalId: existing.rows[0].id, replayed: true });
      return;
    }
    await client.query(
      "INSERT INTO booking_proposals " +
      "(id,idempotency_key,calendar_id,attendee_email,title,start_utc,end_utc," +
      "attendee_time_zone,local_start_text,confirmation_token_hash,status,expires_at) " +
      "VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'proposed'," +
      "now() + ($11 || ' minutes')::interval)",
      [
        id, input.idempotencyKey, POLICY.calendarId,
        input.attendeeEmail.toLowerCase(), input.title,
        start.toInstant().toString(), end.toInstant().toString(),
        input.timeZone, input.localStart, tokenHash(token), POLICY.proposalTtlMinutes
      ]
    );
    await client.query("COMMIT");
    res.status(201).json({
      proposalId: id,
      confirmationToken: token,
      expiresInMinutes: POLICY.proposalTtlMinutes,
      exactAppointment: {
        localStart: input.localStart,
        timeZone: input.timeZone,
        utcStart: start.toInstant().toString(),
        utcEnd: end.toInstant().toString(),
        attendeeEmail: input.attendeeEmail.toLowerCase()
      },
      calendarWritten: false
    });
  } catch (error) {
    await client.query("ROLLBACK").catch(() => {});
    res.status(400).json({ error: error.message || "PROPOSAL_FAILED" });
  } finally { client.release(); }
});

app.post("/v1/confirm", async (req, res) => {
  const client = await pool.connect();
  try {
    const input = confirmSchema.parse(req.body);
    await client.query("BEGIN");
    await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1,0))", [POLICY.calendarId]);
    const found = await client.query(
      "SELECT * FROM booking_proposals WHERE id=$1 FOR UPDATE",
      [input.proposalId]
    );
    const proposal = found.rows[0];
    if (!proposal) throw new Error("PROPOSAL_NOT_FOUND");
    if (proposal.idempotency_key !== input.idempotencyKey) throw new Error("IDEMPOTENCY_MISMATCH");
    if (proposal.status === "confirmed") {
      await client.query("COMMIT");
      res.json({ eventId: proposal.google_event_id, replayed: true });
      return;
    }
    if (proposal.status !== "proposed" || Date.now() >= Date.parse(proposal.expires_at)) {
      throw new Error("PROPOSAL_EXPIRED_OR_INVALID");
    }
    const supplied = Buffer.from(tokenHash(input.confirmationToken));
    const expected = Buffer.from(proposal.confirmation_token_hash);
    if (supplied.length !== expected.length || !crypto.timingSafeEqual(supplied, expected)) {
      throw new Error("INVALID_CONFIRMATION");
    }
    const eventId = stableEventId(proposal.id);
    let inserted;
    try {
      inserted = await calendar.events.get({
        calendarId: POLICY.calendarId, eventId
      });
      if (inserted.data.extendedProperties?.private?.bookingProposalId !== proposal.id) {
        throw new Error("EVENT_ID_COLLISION");
      }
    } catch (lookupError) {
      if (lookupError?.code !== 404) throw lookupError;
      const busy = await busyRanges([{
        startUtc: proposal.start_utc.toISOString(),
        endUtc: proposal.end_utc.toISOString()
      }]);
      if (overlaps({
        startUtc: proposal.start_utc.toISOString(),
        endUtc: proposal.end_utc.toISOString()
      }, busy)) throw new Error("SLOT_NO_LONGER_AVAILABLE");
      try {
        inserted = await withRetry(() => calendar.events.insert({
          calendarId: POLICY.calendarId,
          sendUpdates: POLICY.sendUpdates,
          requestBody: {
            id: eventId,
            summary: proposal.title,
            visibility: POLICY.eventVisibility,
            attendees: [{ email: proposal.attendee_email }],
            start: {
              dateTime: proposal.start_utc.toISOString(),
              timeZone: proposal.attendee_time_zone
            },
            end: {
              dateTime: proposal.end_utc.toISOString(),
              timeZone: proposal.attendee_time_zone
            },
            extendedProperties: { private: { bookingProposalId: proposal.id } }
          }
        }));
      } catch (insertError) {
        if (insertError?.code !== 409) throw insertError;
        inserted = await calendar.events.get({
          calendarId: POLICY.calendarId, eventId
        });
        if (inserted.data.extendedProperties?.private?.bookingProposalId !== proposal.id) {
          throw new Error("EVENT_ID_COLLISION");
        }
      }
    }
    await client.query(
      "UPDATE booking_proposals " +
      "SET status='confirmed',google_event_id=$2,google_event_etag=$3," +
      "confirmed_at=now(),reviewer_id=$4 WHERE id=$1",
      [proposal.id, inserted.data.id, inserted.data.etag, input.actorId]
    );
    await client.query(
      "INSERT INTO booking_audit " +
      "(event_type,proposal_id,actor_id,idempotency_key_hash,calendar_event_id," +
      "start_utc,end_utc,outcome) " +
      "VALUES ('appointment-confirmed',$1,$2,$3,$4,$5,$6,'created')",
      [
        proposal.id, input.actorId,
        crypto.createHash("sha256").update(input.idempotencyKey).digest("hex"),
        inserted.data.id, proposal.start_utc, proposal.end_utc
      ]
    );
    await client.query("COMMIT");
    res.status(201).json({
      eventId: inserted.data.id,
      status: inserted.data.status,
      htmlLink: inserted.data.htmlLink,
      startUtc: proposal.start_utc,
      attendeeTimeZone: proposal.attendee_time_zone
    });
  } catch (error) {
    await client.query("ROLLBACK").catch(() => {});
    res.status(error.message === "SLOT_NO_LONGER_AVAILABLE" ? 409 : 400)
      .json({ error: error.message || "CONFIRM_FAILED" });
  } finally { client.release(); }
});

app.post("/v1/cancel/proposals", async (req, res) => {
  const client = await pool.connect();
  try {
    const input = z.object({
      proposalId: z.string().uuid(),
      actorId: z.string().min(3).max(120)
    }).strict().parse(req.body);
    const token = crypto.randomBytes(32).toString("base64url");
    await client.query("BEGIN");
    const updated = await client.query(
      "UPDATE booking_proposals " +
      "SET status='cancel-proposed',confirmation_token_hash=$2,reviewer_id=$3," +
      "expires_at=now() + ($4 || ' minutes')::interval " +
      "WHERE id=$1 AND status='confirmed' " +
      "RETURNING id,google_event_id,start_utc,end_utc,attendee_time_zone",
      [input.proposalId, tokenHash(token), input.actorId, POLICY.proposalTtlMinutes]
    );
    if (!updated.rows[0]) throw new Error("CONFIRMED_BOOKING_NOT_FOUND");
    await client.query("COMMIT");
    res.status(201).json({
      proposalId: input.proposalId,
      cancellationToken: token,
      exactCancellation: updated.rows[0],
      calendarWritten: false
    });
  } catch (error) {
    await client.query("ROLLBACK").catch(() => {});
    res.status(400).json({ error: error.message || "CANCEL_PROPOSAL_FAILED" });
  } finally { client.release(); }
});

app.post("/v1/cancel/confirm", async (req, res) => {
  const client = await pool.connect();
  try {
    const input = cancelSchema.parse(req.body);
    await client.query("BEGIN");
    const found = await client.query(
      "SELECT * FROM booking_proposals WHERE id=$1 FOR UPDATE",
      [input.proposalId]
    );
    const proposal = found.rows[0];
    if (!proposal || proposal.status !== "cancel-proposed" ||
        Date.now() >= Date.parse(proposal.expires_at)) {
      throw new Error("CANCEL_NOT_PROPOSED_OR_EXPIRED");
    }
    const suppliedCancel = Buffer.from(tokenHash(input.cancellationToken));
    const expectedCancel = Buffer.from(proposal.confirmation_token_hash);
    if (suppliedCancel.length !== expectedCancel.length ||
        !crypto.timingSafeEqual(suppliedCancel, expectedCancel)) {
      throw new Error("INVALID_CANCELLATION");
    }
    const current = await calendar.events.get({
      calendarId: POLICY.calendarId,
      eventId: proposal.google_event_id
    });
    if (proposal.google_event_etag && current.data.etag !== proposal.google_event_etag) {
      throw new Error("EVENT_CHANGED_REVIEW_AGAIN");
    }
    await withRetry(() => calendar.events.delete({
      calendarId: POLICY.calendarId,
      eventId: proposal.google_event_id,
      sendUpdates: POLICY.sendUpdates
    }));
    await client.query(
      "UPDATE booking_proposals SET status='cancelled',cancelled_at=now(),reviewer_id=$2 WHERE id=$1",
      [proposal.id, input.actorId]
    );
    await client.query(
      "INSERT INTO booking_audit " +
      "(event_type,proposal_id,actor_id,calendar_event_id,start_utc,end_utc,outcome) " +
      "VALUES ('appointment-cancelled',$1,$2,$3,$4,$5,'deleted')",
      [proposal.id, input.actorId, proposal.google_event_id, proposal.start_utc, proposal.end_utc]
    );
    await client.query("COMMIT");
    res.json({ cancelled: true, eventId: proposal.google_event_id });
  } catch (error) {
    await client.query("ROLLBACK").catch(() => {});
    res.status(400).json({ error: error.message || "CANCEL_FAILED" });
  } finally { client.release(); }
});

app.get("/healthz", (_req, res) => res.json({ ok: true }));
app.listen(Number(process.env.PORT || 8080), "127.0.0.1", () =>
  console.log("BOOKING_BOT_READY")
);
Example output / evidence
POST /availability -> 200 {slots:[...],zone:"America/New_York",notReserved:true}
POST /proposals -> 201 {exactAppointment:{...},calendarWritten:false}
POST /confirm -> 201 {eventId:"8a5c...",status:"confirmed"}
replayed confirm -> 200 {eventId:"8a5c...",replayed:true}
POST /cancel/proposals -> calendarWritten:false
POST /cancel/confirm -> {cancelled:true}

Checkpoint: Run one full disposable lifecycle

Continue whenAvailability writes nothing; proposal shows exact zone/UTC and writes only DB state; confirmation creates one invite; replay creates none; cancellation proposal writes nothing; cancellation confirmation deletes once and notifies.

Stop whenStop if proposal writes calendar, arbitrary localStart bypasses policy, conflicts are treated as success, stable ID ownership cannot be proven, confirmation lacks actor/token, or cancellation skips ETag/current-state review.

If this step fails

Confirmation returns SLOT_NO_LONGER_AVAILABLE.

Likely causeAnother actor booked the interval after availability/proposal or buffers changed.

Safe checks
  • Refetch current free/busy.
  • Compare proposal time, policy, and new busy interval without reading event details.

ResolutionDo not override or move another event. Expire the proposal and present freshly computed alternatives.

Security notes

  • Return generic conflict information; free/busy access does not authorize disclosure of event titles or attendees.
  • Do not accept actorId from unauthenticated clients in production; bind it at trusted ingress.

Stop conditions

  • Stop if proposal writes calendar, arbitrary localStart bypasses policy, conflicts are treated as success, stable ID ownership cannot be proven, confirmation lacks actor/token, or cancellation skips ETag/current-state review.
08

config

Test DST, conflicts, retries, notifications, and cancellation boundaries

caution

Install the static boundary suite and extend it with mocked Google/PostgreSQL integration tests. Cover every supported zone transition, invalid zone/offset/date/duration, lead/horizon/buffer edges, freeBusy errors, parallel confirmations, provider timeout before/after insert, 409 recovery, wrong token/idempotency/actor, expired proposal, changed cancellation ETag, notification parameters, quota responses, and state-write failure after provider success.

Why this step matters

Time and distributed-write bugs usually appear at transition, concurrency, and ambiguous-outcome boundaries, so tests must model provider success independently from local transaction success.

What to understand

Use a fake Calendar service that records exact methods, IDs, scopes, payloads, sendUpdates, ETags, and scripted errors; no test may invite a real attendee.

Integration tests run two confirmations for the same slot and assert one stable event. A crash fixture inserts provider state then fails the DB update; retry must recover that event by stable ID/private proposal property.

Test cancellation expiry and ETag drift. Never weaken ETag review by switching to unconditional delete merely to make the fixture pass.

System changes

  • Creates a no-network unit suite plus disposable-calendar/PostgreSQL release fixtures.
  • Adds a release gate for external write and notification semantics.

Syntax explained

transition fixtures
Exercise nonexistent/repeated wall times and different business/attendee DST dates.
ambiguous outcome fixture
Proves retry can distinguish already-created event from missing event.
method allowlist assertion
Fails if generic patch/update/move/import capability appears.
File /opt/appointment-bot/test/security.test.mjs
Configuration
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import test from "node:test";

const source = await fs.readFile("./src/server.mjs", "utf8");
test("uses Temporal with reject disambiguation for DST gaps and overlaps", () => {
  assert.match(source, /disambiguation: "reject"/);
  assert.match(source, /allowedTimeZones/);
});
test("confirmation is distinct from proposal and checks current freebusy", () => {
  assert.match(source, /\/v1\/proposals/);
  assert.match(source, /\/v1\/confirm/);
  assert.match(source, /SLOT_NO_LONGER_AVAILABLE/);
  assert.match(source, /timingSafeEqual/);
});
test("serializes confirmations and provides stable provider idempotency", () => {
  assert.match(source, /pg_advisory_xact_lock/);
  assert.match(source, /stableEventId/);
  assert.match(source, /error\?\.code !== 409/);
});
test("calendar writes notify attendees and remain private", () => {
  assert.match(source, /sendUpdates: POLICY\.sendUpdates/);
  assert.match(source, /visibility: POLICY\.eventVisibility/);
});
test("cancellation is proposed, refetched, etag checked, then confirmed", () => {
  assert.match(source, /\/v1\/cancel\/proposals/);
  assert.match(source, /EVENT_CHANGED_REVIEW_AGAIN/);
  assert.match(source, /calendar\.events\.delete/);
});
test("does not expose a generic calendar update or arbitrary calendar id", () => {
  assert.doesNotMatch(source, /calendar\.events\.(patch|update|move|import)/);
  assert.doesNotMatch(source, /calendarId:\s*req\./);
});
test("bounds JSON, database waits, and Google retries", () => {
  assert.match(source, /limit: "24kb"/);
  assert.match(source, /statement_timeout: 5000/);
  assert.match(source, /attempt < 4/);
});
Example output / evidence
✔ uses Temporal with reject disambiguation for DST gaps and overlaps
✔ confirmation is distinct and checks current freebusy
✔ serializes confirmations and provides stable idempotency
✔ writes notify attendees and remain private
✔ cancellation is proposed, refetched, etag checked, then confirmed
✔ no generic update or arbitrary calendar id
✔ bounds JSON, database waits, and retries
# pass 7
# fail 0

Checkpoint: Run adversarial test matrix

Continue whenEvery negative case fails closed, parallel/retried confirmations create one event, notifications are explicit, and cancellation never deletes changed state.

Stop whenStop if tests call real attendees, skip ambiguous provider outcomes, normalize DST errors silently, omit ETag cancellation, or cannot prove only insert/get/freebusy/delete methods.

If this step fails

A retry creates two attendee invitations.

Likely causeProvider event IDs are random or 409 recovery does not bind the existing event to the proposal.

Safe checks
  • Compare event IDs and private proposal property.
  • Inspect idempotency key/proposal mapping and audit.

ResolutionDisable confirmations, remove/reconcile duplicate through a human-reviewed cancellation, restore stable IDs and 409 ownership checks, then rerun concurrency fixtures.

Security notes

  • Fixtures use reserved example addresses and a disposable calendar with delivery disabled or controlled test recipients.
  • Keep production OAuth/database credentials out of test runners.

Stop conditions

  • Stop if tests call real attendees, skip ambiguous provider outcomes, normalize DST errors silently, omit ETag cancellation, or cannot prove only insert/get/freebusy/delete methods.
09

decision

Run availability shadowing and a controlled invitation pilot

read-only

For seven representative days, compute suggested slots while a human scheduler compares them with Google Calendar UI, holidays, buffers, zones, and existing process; do not enable public confirmation. Then run at least fifty controlled lifecycle cases with consenting test recipients across zones, retries, conflict races, cancellations, and provider delays. Review every external write and notification.

Why this step matters

A technically correct conversion can still conflict with real holidays, ownership, notification expectations, buffers, and support workflow; shadow comparison validates operational semantics before public writes.

What to understand

Sample weekdays, horizon edges, business/attendee midnight crossing, both DST directions, high concurrency, calendar edits between proposal/confirmation, and cancellation after an event edit.

Measure false-free and false-busy slots, confirmation latency, proposal expiry, conflict rate, duplicate/replayed requests, notification delivery, quota/error retries, and support tickets.

Require zero duplicate events/invitations and zero writes without the exact confirmation evidence. An unresolved false-free result blocks release.

System changes

  • Creates reviewed evaluation evidence and a limited authenticated-pilot decision.
  • The initial shadow phase performs reads only; the controlled phase writes only to consenting test appointments.
Example output / evidence
shadow days: 7
availability comparisons: 180
false-free slots: 0
DST/zone display disagreements: 0
controlled confirmed appointments: 52
duplicate provider events/invites: 0
conflict rejections: 4
cancellations with fresh ETag: 12
release: limited authenticated pilot

Checkpoint: Approve pilot report

Continue whenScheduling and security owners sign zero duplicates/unapproved writes/false-free results, correct zone displays, notification behavior, support readiness, and rollback ownership.

Stop whenStop if any slot is falsely free, a notification surprises a recipient, duplicate events occur, time display differs, cancellation removes changed state, or support cannot reconcile an ambiguous outcome.

If this step fails

Shadow output differs from Calendar UI around a holiday.

Likely causeHoliday/out-of-office policy is not represented as busy or business-hours exception.

Safe checks
  • Inspect free/busy interval without event details.
  • Review policy exception ownership and Calendar transparency.

ResolutionModel the closure as an opaque busy event or reviewed exception, rerun the affected period, and document ownership.

Security notes

  • Test recipients must explicitly consent to invitations and cancellation emails.
  • Evaluation exports should use pseudonymous IDs and minimize email/time data.

Stop conditions

  • Stop if any slot is falsely free, a notification surprises a recipient, duplicate events occur, time display differs, cancellation removes changed state, or support cannot reconcile an ambiguous outcome.
10

config

Deploy loopback service with hardened systemd and trusted ingress

caution

Install the systemd unit, root-owned environment file, and HTTPS reverse-proxy routes. Authenticate availability/proposal/confirmation/cancellation as required, derive actor identity at ingress, add CSRF/origin protection for browser actions, enforce IP/account limits and request IDs, and expose only /healthz without dependencies or secrets.

Why this step matters

Hardened deployment preserves code/policy integrity and prevents public clients from forging reviewer identity or bypassing the narrow API through direct runtime access.

What to understand

Use separate secrets for database, Google OAuth, and confirmation HMAC. Rotate without changing client-visible idempotency semantics; overlapping key rotation needs a versioned token-hash plan.

Rate-limit availability more generously than writes, but apply per-account and IP safeguards. A distributed limiter is required for multiple ingress instances.

Health checks report process readiness only; never include calendar ID, scopes, queue/proposal counts, attendee email, token validity, or provider error bodies.

System changes

  • Enables production HTTPS access to bounded booking routes and outbound Google/PostgreSQL connectivity.
  • Runs as an unprivileged service with only the designated writable path.

Syntax explained

ProtectSystem=strict
Makes application/config/system files read-only to runtime.
ReadWritePaths
Restricts filesystem writes to approved operational state.
server-derived actor
Prevents clients from naming another person as confirmer/canceller.
File /etc/systemd/system/appointment-bot.service
Configuration
[Unit]
Description=Reviewed appointment booking bot
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=appointment-bot
Group=appointment-bot
WorkingDirectory=/opt/appointment-bot
EnvironmentFile=/etc/appointment-bot/runtime.env
ExecStart=/usr/bin/node /opt/appointment-bot/src/server.mjs
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/appointment-bot
RestrictSUIDSGID=true

[Install]
WantedBy=multi-user.target
Example output / evidence
appointment-bot.service active (running)
listener: 127.0.0.1:8080
public TLS: active
actor identity: server-derived
request body max: 24 KiB
runtime writable path: /var/lib/appointment-bot

Checkpoint: Inspect deployed trust boundaries

Continue whenOnly proxy reaches loopback, identity/CSRF/rate limits are active, secrets are separated, runtime is unprivileged, and unsupported methods/routes return denial.

Stop whenStop if direct Node access exists, actorId is accepted from public JSON, confirmation tokens appear in URLs/logs, secrets are broad-readable, or generic proxy/calendar routes exist.

If this step fails

Authenticated UI confirmations fail CSRF while API tests pass.

Likely causeBrowser cookies/origin/token integration differs from direct API authentication.

Safe checks
  • Inspect SameSite/origin/CSRF metadata without token values.
  • Replay a disposable same-origin confirmation.

ResolutionCorrect trusted ingress/session protection; do not disable CSRF or accept bearer confirmation token as the sole identity.

Security notes

  • Confirmation token proves possession of a proposal but not identity; require authenticated actor too.
  • Use TLS certificate validation for Google and PostgreSQL and controlled outbound DNS/network policy.

Stop conditions

  • Stop if direct Node access exists, actorId is accepted from public JSON, confirmation tokens appear in URLs/logs, secrets are broad-readable, or generic proxy/calendar routes exist.
11

verification

Monitor Calendar quota, conflict, idempotency, expiry, and audit signals

read-only

Emit structured metadata for availability, proposals, confirmations, cancellations, provider method/status/latency, retries, 403/429 usage limits, 409 recovery, conflict rejection, expired tokens, ETag drift, audit failure, DB pool saturation, notification mode, and duplicate-event invariant. Alert on false success, rising conflicts, quota backoff, or audit/provider disagreement without logging personal fields.

Why this step matters

Distributed booking reliability depends on distinguishing unavailable slots, safe replays, quotas, and ambiguous outcomes; aggregate HTTP success alone cannot prove one appointment and one invitation.

What to understand

Google Calendar quotas are per project and per user/project and operational write limits may apply. Use truncated exponential backoff with jitter and a finite retry budget; do not retry permanent validation errors.

Track retry reason and provider request correlation without attendee/title/token. Alert when proposal confirms but audit fails or provider event exists while local status remains proposed.

Spread scheduled housekeeping rather than creating synchronized bursts. Expire old proposals and delete retained personal data through bounded jobs.

System changes

  • Adds privacy-minimized metrics, alerts, dashboards, and reconciliation ownership.
  • Performs read-only state/service inspection and consumes no calendar write quota.

Syntax explained

403/429 usageLimits
Signals quota throttling that uses finite exponential backoff and jitter.
409 recovered existing
Measures provider idempotency recovery rather than treating it as generic failure.
audit/provider unresolved
Detects ambiguous external outcomes requiring manual reconciliation.
Command
sudo systemctl --no-pager --full status appointment-bot.service && psql -X "$DATABASE_URL" -c "SELECT status,count(*) FROM booking_proposals GROUP BY status ORDER BY status; SELECT event_type,outcome,count(*) FROM booking_audit WHERE occurred_at > now()-interval '24 hours' GROUP BY event_type,outcome;"
Example output / evidence
availability p95: 118 ms
confirm p95: 302 ms
Google 403/429: 0 / 2 (backoff honored)
conflict rejections: 3
409 recovered existing: 1
duplicate event IDs: 0
expired proposals: 14
cancellation ETag drift: 1
audit/provider unresolved: 0

Checkpoint: Exercise alerts and reconciliation

sudo systemctl --no-pager --full status appointment-bot.service && psql -X "$DATABASE_URL" -c "SELECT status,count(*) FROM booking_proposals GROUP BY status ORDER BY status; SELECT event_type,outcome,count(*) FROM booking_audit WHERE occurred_at > now()-interval '24 hours' GROUP BY event_type,outcome;"

Continue whenSynthetic quota, provider-timeout, conflict, ETag, DB, disk, and audit failures produce actionable alerts without personal data or secrets.

Stop whenStop public confirmations when provider/local state diverges, audit is unavailable, quota retry storms occur, duplicate invariant fails, or personal data enters metrics.

If this step fails

Google returns repeated 403/429 while request volume seems low.

Likely causePer-user/project sliding-window or operational write limits may be hit by bursts or shared service-account behavior.

Safe checks
  • Inspect reason, Retry-After/quota context, per-user request distribution, and burst timing.
  • Check whether multiple environments share one Cloud project/user.

ResolutionPause writes, honor finite backoff, spread traffic, isolate test/prod projects, and request quota adjustment only after behavior is efficient.

Security notes

  • Metrics labels never contain attendee email, title, local time, calendar ID, event link, confirmation token, or OAuth details.
  • Monitoring destinations are external systems and need retention/access review.

Stop conditions

  • Stop public confirmations when provider/local state diverges, audit is unavailable, quota retry storms occur, duplicate invariant fails, or personal data enters metrics.
12

command

Export state and prove restore without replaying appointments

caution

Pause new confirmations/cancellations, take an encrypted consistent PostgreSQL backup of proposal/audit tables plus policy/deployment manifest, hash the artifact, and restore into a network-isolated database. Verify status, stable event IDs, token hashes, audit sequence, retention, ownership, and that all outstanding proposed/cancel-proposed rows are expired pending live reconciliation. Restore OAuth/database/HMAC secrets only from managed origins.

Why this step matters

Restored idempotency and status prevent duplicate invitations/deletions, but old proposals cannot safely execute until current Calendar state and policy are re-established.

What to understand

Use an approved encrypted backup platform; the path illustrates content. Assign encryption, restore, retention, deletion, legal-hold, and privacy-incident owners.

A production recovery refreshes each relevant stable event ID through the fixed calendar and reconciles provider event property/ETag before enabling new actions.

Never restore plaintext refresh/HMAC/database secrets from ordinary state or turn unexpired client tokens into reusable recovery credentials.

System changes

  • Creates an encrypted backup and network-isolated restored database.
  • Does not connect to Google or execute any proposal/cancellation during restore.

Syntax explained

--format=custom
Creates a restorable PostgreSQL archive with selected protected tables.
--exit-on-error
Stops restore on the first failure instead of accepting partial state.
expire outstanding
Prevents stale human confirmations from becoming post-recovery calendar writes.
Command
pg_dump --format=custom --no-owner --no-privileges --table=booking_proposals --table=booking_audit "$DATABASE_URL" > /secure-backups/appointment-state-2026-07-29.dump && sha256sum /secure-backups/appointment-state-2026-07-29.dump > /secure-backups/appointment-state-2026-07-29.dump.sha256 && createdb appointment_restore && pg_restore --exit-on-error --no-owner --dbname=appointment_restore /secure-backups/appointment-state-2026-07-29.dump
Example output / evidence
backup SHA256: a4f213...709c
restored proposals: 214
confirmed/cancelled: 137/28
outstanding proposals expired for reconciliation: 49
duplicate provider event IDs: 0
audit sequence gaps: 0
OAuth/HMAC/database secrets in archive: 0
restore network writes: disabled

Checkpoint: Approve isolated restore report

pg_dump --format=custom --no-owner --no-privileges --table=booking_proposals --table=booking_audit "$DATABASE_URL" > /secure-backups/appointment-state-2026-07-29.dump && sha256sum /secure-backups/appointment-state-2026-07-29.dump > /secure-backups/appointment-state-2026-07-29.dump.sha256 && createdb appointment_restore && pg_restore --exit-on-error --no-owner --dbname=appointment_restore /secure-backups/appointment-state-2026-07-29.dump

Continue whenCounts/constraints/audit match, pending actions cannot execute, secrets are absent, retention remains enforceable, and restored environment has no provider route.

Stop whenStop if event IDs/status/audit are missing, outstanding tokens remain executable, secrets are embedded, personal data retention is exceeded, or restored service can reach Calendar.

If this step fails

Restored confirmation would create an event already present in Google.

Likely causeProvider reconciliation was skipped and local confirmed status was lost or stale.

Safe checks
  • Look up stable event ID in isolated reviewed reconciliation.
  • Compare private proposal property and audit.

ResolutionKeep writes disabled, mark canonical local state from verified provider evidence, and issue a new proposal only if the event truly does not exist.

Security notes

  • Backup contains emails and appointment times; classify it as personal operational data.
  • Temporary restore databases and operator exports must be deleted under the same policy.

Stop conditions

  • Stop if event IDs/status/audit are missing, outstanding tokens remain executable, secrets are embedded, personal data retention is exceeded, or restored service can reach Calendar.
13

command

Disable ingress, revoke OAuth, reconcile events, and delete retained data

caution

Disable confirmation/cancellation routes, stop and mask the service, export final audit, reconcile in-flight proposals and stable provider events, revoke the Google OAuth grant/refresh token, rotate the HMAC and database credentials, remove proxy/DNS, then delete calendar fixtures and state only through approved retention. Existing legitimate appointments remain unless each attendee-facing cancellation is separately approved.

Why this step matters

Local shutdown does not revoke Calendar authority, while bulk-deleting legitimate future appointments would surprise attendees and exceed the approved cancellation workflow.

What to understand

Disable public ingress before service stop to avoid accepted-but-unprocessed actions. Record the change window and preserve event/audit correlation.

Revoke OAuth through Google administration and confirm refresh fails safely. Rotate other dedicated secrets and remove service identity grants.

Do not mass-cancel events. Hand off legitimate appointments to a human owner or obtain individual cancellation confirmations with notifications.

System changes

  • Stops and masks local service, removes external OAuth authority, rotates dedicated credentials, and removes ingress.
  • Deletes state/backups after reconciliation while preserving or individually managing legitimate calendar events.

Syntax explained

disable --now + mask
Stops current execution and prevents accidental restart.
OAuth revoke
Removes external Calendar authority that host shutdown cannot remove.
preserve legitimate events
Avoids turning decommissioning into unapproved attendee-facing deletion.
Command
sudo systemctl disable --now appointment-bot.service && sudo systemctl mask appointment-bot.service && sudo ss -lntp | grep ':8080' && printf '%s\n' 'Expected: no listener. Revoke Google OAuth grant and rotate secrets before retention-approved state deletion.'
Example output / evidence
service disabled and masked
listener 127.0.0.1:8080: absent
in-flight proposals: reconciled/expired
Google OAuth grant: revoked
HMAC/database credentials: rotated
legitimate future events: preserved
state deletion date: approved

Checkpoint: Prove booking authority is gone

sudo systemctl disable --now appointment-bot.service && sudo systemctl mask appointment-bot.service && sudo ss -lntp | grep ':8080' && printf '%s\n' 'Expected: no listener. Revoke Google OAuth grant and rotate secrets before retention-approved state deletion.'

Continue whenNo ingress/listener/runtime remains, OAuth is revoked, secrets rotate, in-flight operations reconcile, and every retained event/state copy has an owner and disposition.

Stop whenStop deletion if OAuth status is unknown, in-flight provider outcome is ambiguous, a hold/incident exists, legitimate appointments lack an owner, or backup locations are unknown.

If this step fails

OAuth refresh still succeeds after teardown.

Likely causeOnly local credentials were deleted or the wrong Google grant/client was revoked.

Safe checks
  • Inspect OAuth grant owner/client ID in administration.
  • Verify deployed secret fingerprint and token owner without printing values.

ResolutionRevoke the actual grant, rotate client secret if exposed, monitor Calendar audit, and retain evidence until authority is removed.

Security notes

  • Do not test revocation by printing tokens or repeatedly calling unrelated calendars.
  • Deletion includes logs, backups, restores, browser downloads, support exports, and database replicas.

Stop conditions

  • Stop deletion if OAuth status is unknown, in-flight provider outcome is ambiguous, a hold/incident exists, legitimate appointments lack an owner, or backup locations are unknown.

Finish line

Verification checklist

Verify time and availability boundariesnpm test && printf '%s\n' 'Run spring-gap, fall-overlap, midnight-crossing, lead/horizon, buffers, per-calendar freeBusy error, and unknown-zone fixtures.'Only explicit supported instants produce slots, errors fail closed, availability writes nothing, and every display includes IANA zone plus UTC.
Verify one idempotent confirmed eventprintf '%s\n' 'Create a disposable proposal, confirm with authenticated actor/token/idempotency key, then replay identical confirmation concurrently.'One private Calendar event and one attendee invitation exist with stable ID/private proposal property; replay returns the original event and audit remains coherent.
Verify conflict prevention and ambiguous recoveryprintf '%s\n' 'Insert a conflicting event after proposal; simulate provider success before DB failure; retry both scenarios.'Conflict aborts with fresh alternatives, while ambiguous success recovers the same stable event without another invitation.
Verify reviewed cancellationprintf '%s\n' 'Propose cancellation, alter event to change ETag, verify rejection; create fresh cancellation proposal and confirm.'Changed event is preserved pending review; fresh exact cancellation deletes once, sends updates, records actor, and replay cannot delete another event.

Recovery guidance

Common problems and safe checks

No slots are returned for an open day.

Likely causeFreeBusy error, buffer/lead policy, zone/date conversion, or opaque closure may consume candidates.

Safe checks
  • Inspect per-calendar freeBusy errors.
  • Render candidate instants in business and attendee zones.

ResolutionCorrect authoritative policy/provider access; never treat errors as free.

A local time is rejected during spring transition.

Likely causeThat wall time does not exist in the IANA zone.

Safe checks
  • Check zone transition with Temporal.
  • Show adjacent valid slots.

ResolutionAsk the user to select a valid exact slot; do not normalize forward silently.

A local time is rejected during fall transition.

Likely causeThe wall time occurs twice with different offsets.

Safe checks
  • Inspect both offsets.
  • Confirm requested IANA zone.

ResolutionRequire a different unambiguous slot or a separately designed explicit-offset choice.

Selected slot is outside business policy.

Likely causeCaller submitted localStart not produced by policy or attendee/business date crossed.

Safe checks
  • Convert to business zone.
  • Compare generated allowed slot instants.

ResolutionReject and return fresh availability; never accept arbitrary localStart.

freeBusy reports notFound/internalError.

Likely causeCalendar ID/access/provider failed.

Safe checks
  • Check fixed calendar ID/accessRole.
  • Inspect per-calendar error reason.

ResolutionFail closed, repair access or retry transient failure finitely, then recompute.

Confirmation says slot no longer available.

Likely causeAnother actor booked or buffer changed.

Safe checks
  • Refetch freeBusy.
  • Compare busy interval.

ResolutionExpire proposal and offer fresh alternatives without exposing event details.

Concurrent confirmations both start.

Likely causeDistributed lock/state database is missing or requests target different calendars/DBs.

Safe checks
  • Inspect advisory-lock database and calendar ID.
  • Correlate proposal/idempotency.

ResolutionDisable writes, reconcile events, restore shared serialization, and retest.

Provider returned timeout but event exists.

Likely causeCalendar committed before network response/local state.

Safe checks
  • Get stable event ID.
  • Verify private bookingProposalId.

ResolutionAdopt that event and commit local state; never insert a new ID.

Provider returns 409.

Likely causeStable event already exists or rare collision occurred.

Safe checks
  • Get event by stable ID.
  • Compare private proposal property.

ResolutionRecover matching event; treat mismatch as collision/incident and stop.

Attendee received duplicate invitations.

Likely causeDifferent event IDs or repeated non-idempotent insert occurred.

Safe checks
  • Compare provider IDs and proposal audit.
  • Inspect retry path.

ResolutionPause writes, human-review duplicate cancellation, fix stable ID recovery, notify affected attendee.

Cancellation ETag changed.

Likely causeEvent was edited after cancellation proposal.

Safe checks
  • Refetch current event.
  • Compare attendee/time/organizer and ETag.

ResolutionReject old token and present a fresh exact cancellation for approval.

Cancellation returns 410/not found.

Likely causeEvent was already removed externally.

Safe checks
  • Check stable event ID and local status.
  • Review Calendar audit if authorized.

ResolutionReconcile as already absent, record outcome, and do not delete another event.

OAuth returns invalid credentials.

Likely causeRefresh token revoked/expired or client mismatch.

Safe checks
  • Check credential fingerprints/owner.
  • Inspect 401 without token output.

ResolutionStop writes, reauthorize reviewed scopes, rotate if exposed, and retest disposable calendar.

Quota errors repeat.

Likely causeBurst, per-user/project, or operational limits are reached.

Safe checks
  • Inspect reason and traffic shape.
  • Check shared project/user.

ResolutionHonor finite jittered backoff, spread traffic, isolate environments, and pause confirmations if uncertain.

Restore replays a pending token.

Likely causeOld unconsumed proposals were restored without expiry/reconciliation.

Safe checks
  • Inspect status/expiry.
  • Get stable event IDs in reviewed recovery.

ResolutionExpire all pending actions, reconcile current Calendar, and issue new proposals.

After the procedure

Alternatives and next steps

Consider these alternatives

  • Use Google Calendar appointment schedules for standard public booking.
  • Use a coordinator-approved queue when appointment duration/resources/eligibility require business judgment.
  • Use a dedicated resource reservation system with atomic capacity semantics when one calendar is insufficient.
  • Support rescheduling as cancel-plus-new-proposal rather than generic patch, preserving two explicit human decisions.

Operate it safely

  • Add a versioned policy hash to each proposal so business-hours/zone/notification changes invalidate outstanding confirmations.
  • Add holiday exception ownership and resource-specific calendars through transactional allocation rather than arbitrary calendar IDs.
  • Build an accessible review UI that shows exact local/UTC time, attendee, duration, expiry, and not-reserved/conflict state.
  • Add privacy self-service for viewing/deleting retained proposal data without deleting legitimate calendar events accidentally.
  • Review Google scopes/quotas/events/freebusy behavior, IANA/Temporal data, business policy, notifications, tests, recovery, and support at least every 90 days.

Reference

Frequently asked questions

Does an available slot reserve the time?

No. Free/busy is a read snapshot. The UI must say not reserved; confirmation performs a serialized fresh check and then inserts.

Why reject DST ambiguity instead of choosing?

Choosing an offset silently can book a different instant than the person intended. Explicit rejection keeps the human decision accurate.

Can Google Calendar atomically insert only if free?

Not through this workflow. Local confirmations serialize and recheck immediately, but external writers can race. Monitor and reconcile that narrow window.

Why supply a custom event ID?

One proposal maps to one provider identity, so retries after timeout/409 can retrieve the same event rather than inviting twice.

Why use ETag for cancellation?

It detects changes after the user reviewed cancellation. Changed attendee/time/details need a fresh decision.

Can rescheduling use events.patch?

This reference excludes generic patch. Use confirmed cancellation plus a new confirmed appointment to preserve explicit decisions.

How often should this guide be reviewed?

At least every 90 days and after scope/quota/API, time-zone data, policy, notification, identity, state, or recovery changes.

Recovery

Rollback

The service can immediately stop new actions and revoke OAuth. A mistakenly created event is reversed only through a separately reviewed cancellation that displays attendee/time and sends updates; a mistakenly cancelled event is not silently recreated because attendees may already have acted on the cancellation. Rebook through a new confirmed proposal.

  1. Disable confirmation/cancellation ingress and stop service while preserving proposal/event/audit correlation.
  2. For a mistaken creation, refetch event and ETag, show exact attendee/time, obtain cancellation approval, delete with sendUpdates=all, and record the compensating action.
  3. For a mistaken cancellation, contact the attendee through approved support and create a new availability proposal/confirmation; never recreate automatically with the old token.
  4. Revoke OAuth and rotate HMAC/database credentials for suspected compromise; inspect provider and application audit for unapproved writes.
  5. Restore state only after current Calendar reconciliation; expire old pending tokens and preserve stable ID/consumed/cancelled status.

Evidence

Sources and review

Verified 2026-07-29Review due 2026-10-27
Google Calendar API calendars, timed events, IANA time zones, DST, and recurrenceofficialGoogle Calendar Freebusy query authorization, intervals, calendars, and errorsofficialGoogle Calendar OAuth scopes for owned events and freebusyofficialGoogle Calendar create events, event IDs, attendees, times, and conference behaviorofficialGoogle Calendar event resource IDs, ETags, time zones, visibility, and statusofficialGoogle Calendar API usage quotas, operational limits, and exponential backoffofficialGoogle Calendar API errors including 409 duplicate, 410 gone, and quota handlingofficialRFC 5545 iCalendar date-time, TZID, daylight, and free-busy semanticsofficial