# Build a booking and scheduling app with an AI coding agent

> Give an AI coding agent a complete one-resource booking app brief, then check identity, contested slots, persistent history and requested email before real appointments.

By BIGCHANGE.AI Editorial

Published: 2026-10-22T20:12:38.164Z
Updated: 2026-10-22T20:12:38.164Z
Canonical: https://bigchange.ai/blog/ai-agent-booking-scheduling-app-guide

![Conceptual charcoal illustration of two people checking blank phones outside a room with one empty orange appointment chair and a wall clock.](https://bigchange.ai/api/media/file/booking-single-resource-contested-slot-hero-v1.png)
AI-generated conceptual illustration by BIG CHANGE.

A service owner can use an AI coding agent to build a one-resource appointment app, then check whether it really prevents double booking. This guide gives Replit Agent a complete build brief, SQL migration and API contract. The result you should seek is a persistent owner calendar, named-customer booking and changes, in-app history, and truthful status for any requested email. The instructions are based on current documentation checked on 22 October 2026. BIG CHANGE has not generated or run this app.

## The big change

- **What changed:** An AI coding agent can take a complete application brief and produce the interface and server code. The owner can specify the booking rules, database constraints and tests before it writes code, using [Replit Agent's Plan mode](https://docs.replit.com/features/agent/overview) to review the work.
- **Why it matters:** For a small service business, the useful outcome is a booking record that survives reloads and competing requests. A visible open slot is only an invitation to try; the database transaction must decide who gets it. The owner still controls enrollment, real availability and customer contact.
- **What to watch:** A working sign-in screen or confirmation email does not establish a safe booking system. Check the generated app's contested-slot behavior, time-zone interpretation, public authentication paths and signed email callback on its actual host before accepting real appointments.

## Before you start

You need authority over one appointment resource, the services and availability you will offer, and the people you will enroll. Choose a business time zone from the IANA list, booking lead and advance limits, and the deadline for customer changes. Decide whether the app's slots are your authoritative calendar. It has no Google or Outlook sync; reconcile any outside calendar yourself and hold real booking if it can allocate the same resource independently. Start with synthetic names and owner-controlled inboxes. Obtain Replit, managed Clerk identity and PostgreSQL access; use Resend only if you have permission to email verified customers from a verified sender domain. Review actual plan costs, data location and retention before entering customer data. [Replit describes separate development and production databases](https://docs.replit.com/features/data-and-storage/development-and-production), but you must inspect the bindings in your own project.

The AI agent writes the app. It does not run inside the finished booking service. No model API or payment integration is required by this design. A "booking confirmed" message must come from a committed database result, not the agent's prose, a button click or a provider email response.

## 1. Give the agent the whole build contract

Open Replit Agent in Plan mode. Give it **Appendix A: build brief**, **Appendix B: SQL migration** and **Appendix C: API and acceptance contract** below together. Ask for a React interface, Express server, managed Clerk authentication and PostgreSQL. Review its plan before allowing code or data changes. Require a minimal public landing page, the generated Clerk sign-in and proxy flow, and a private app for enrolled users. Do not substitute a mock calendar or stop after a booking form.

The expected plan names the owner settings and service editor, finite dated slots, customer booking/history/change screens, durable in-app events, optional requested email and a restricted owner recovery view. It also names the transaction and tests that decide a contested slot. If any part is missing, revise the plan while it is still a plan.

Use an actual Active or Maintenance LTS Node.js release compatible with both the host and locked packages. Node's [release table](https://nodejs.org/en/about/previous-releases) lists 22 and 24 as LTS on the check date and 20 as end of life. Inspect the generated lockfile and the installed Clerk, Resend and time-zone interfaces. Hold the build when the chosen host cannot run that supported combination.

## 2. Set up identity and storage

Apply the migration to a new, empty Development database after reviewing it. It defines member roles, owner settings, services, slots, bookings, history, notice state and audit records. Seed the first owner through authenticated administration with that environment's exact Clerk user ID, which is a string. Application records use UUIDs. The public sign-up flow creates a provider identity only; the owner enrolls each known customer separately after verifying who they are. There is no public owner bootstrap.

The app must let a guest reach the data-free landing and the actual generated Clerk authentication routes. Put the managed Clerk proxy before body parsers. Keep its sign-in, sign-up, callback and static bootstrap paths outside the app's SQL membership and custom mutation-token checks while retaining Clerk's protections. Discover those paths in the generated integration; do not hardcode a presumed proxy path. For every private page and API, check the current Clerk session and active SQL membership. A signed-in but unlisted person still sees no service, slot, booking or owner data. App mutations require the allowed Origin and a session-bound CSRF token. CSRF protection stops a browser request from an unauthorized site from making a signed-in user change app data. [Clerk documents the middleware and optional Frontend API proxy](https://clerk.com/docs/reference/express/clerk-middleware); its middleware attaching authentication state is different from granting app membership.

Expected check: guest authentication completes without a redirect loop; new sign-up remains unlisted; an enrolled customer sees only their own records; a customer cannot use owner routes or guess another booking ID. Missing or wrong Origin/token fails private mutations. Development and published Production have separate identity and data settings. If the published host prevents the generated Clerk transport, hold release rather than opening private data routes.

## 3. Publish real appointment slots

The owner enters a service name and duration, then a finite date and local start time with explicit UTC offset and IANA zone. The server rejects a nonexistent spring-forward time, an offset that does not match the chosen zone, or an omitted offset. For the repeated fall-back hour, the owner selects the intended occurrence. Save the UTC interval and the selected local display, service name and duration. A later settings or service edit must not rewrite that history. [Temporal's time-zone guide](https://tc39.es/proposal-temporal/docs/timezone.html) explains why wall-clock time and an instant need explicit interpretation.

Ask the owner UI to preview the stored interval before publishing. Test adjacent slots and overlapping slots, including slots belonging to different services and two owner requests made at once. Adjacent `[start,end)` ranges may coexist; two open slots that overlap on this one resource must fail the PostgreSQL exclusion constraint. [PostgreSQL documents this range constraint](https://www.postgresql.org/docs/current/rangetypes.html). Blocking or reopening a slot also checks its version and any confirmed booking. An old browser slot list is only a hint; the server and database decide the current state.

## 4. Check booking and changes under contention

Enroll two test customers. Have both try the same slot concurrently. Only one can get a committed booking ID and revision; the other gets a conflict and refreshed availability. Reload the winner's page and confirm that the booking and event history persist. Interleave an identical request key while its first request is waiting on a slot or booking lock. The identical booking, reschedule or cancellation retry must return its already committed result after the wait; changing its binding must conflict. The request key is a unique identifier the browser reuses after a lost response.

Test a block request against a booking in both orders, and test member deactivation and service disable against booking. Whichever change wins its ordered locks determines admission. There must be no confirmed booking on a blocked slot, and no new booking admitted after deactivation or disable won. Rescheduling moves a confirmed booking to a different open slot of the same active service in one transaction. A customer uses the stored absolute change deadline; a new booking uses current lead and advance rules. Set a zero-minute cutoff and 120-minute booking lead. One hour before a confirmed start, an active customer may cancel that booking, although a new booking at that time fails lead-time admission. Disabling the booked service must not prevent its cancellation. A deactivated customer loses self-service; an active owner can cancel that booking with a reason, preserve its history and leave email blocked without contacting the provider.

Change the owner policy, service name/duration and business zone after a booking. Its earlier deadline and slot description must remain in its event history. An old-duration open slot can still be booked at its stored interval while its service remains active. A deliberate reschedule records the current policy for the new appointment and the chosen slot's stored duration. These checks require inspecting persisted rows and the UI after a restart, not merely a screenshot of the form.

## 5. Treat notifications as separate evidence

The booking transaction creates an in-app event and notice record together. `in_app` requires no email call. For requested email, freeze one verified primary recipient, permitted sender, plain-text content, payload hash and unique provider key. An unavailable identity, unverified address, absent permission or deactivated recipient produces a visible `blocked` notice while the booking remains committed. The customer never supplies a recipient in the booking request.

Resend's successful send response means `accepted` for sending. A signed `email.delivery_delayed` callback means delayed; `email.delivered` means acceptance by the recipient mail server; neither proves the person read it. A bounce is recorded separately. [Resend documents its send API](https://resend.com/docs/api-reference/emails/send-email), [24-hour idempotency retention](https://resend.com/docs/dashboard/emails/idempotency-keys), [raw-body signature verification](https://resend.com/docs/webhooks/verify-webhooks-requests) and [the delayed event name](https://resend.com/docs/webhooks/emails/delivery-delayed).

The public HTTPS host must let Resend reach a route-specific raw signed webhook before general JSON parsing. Do not place Clerk session, SQL membership or app CSRF checks ahead of that transport. Verify the original bytes and signature, then match provider ID, frozen recipient, notice tag and event time to one persisted outbox row. It returns no booking data. A provider callback received before the send response must survive restart and replay after exact linkage. A late accepted or delayed event cannot regress a delivered or bounced state. A timeout, lost response or crashed send is `unknown`; inspect exact provider evidence before any retry. Never assume that waiting 24 hours makes a new send safe. Two owner reissue keys racing against one predecessor must produce at most one successor.

Run the email checks only with permitted test identities, verified inboxes, sender and provider account. If a provider state cannot be induced or the published host blocks its signed callback, record it as untested and hold email-enabled release. An invite-only host that rejects Resend cannot support this design's delivery-state callback. The owner retains the booking and can use a separately permitted contact channel while a notice is blocked or unknown.

## 6. Decide whether the whole app is ready

Read the generated source, migrations, dependency lockfile and access checks. Verify the actual published HTTPS host as a guest, a new unlisted identity, an inactive customer, two enrolled customers and the owner. Guest access to a data-free landing and identity flow must work while all booking, availability, service and admin data remain private. Check valid and invalid signed callbacks without app CSRF, persistence after restart, separate Production data and identity, fees, backups, retention and rollback. Record each result. A missing race, zone, access or notice check is a hold on the whole real-customer release. The owner decides whether to activate it after those actual results; this article records documentation checks only.

The related [private client portal guide](https://bigchange.ai/blog/private-client-portal-project-document-exchange-guide) covers projects and documents. This appointment app has its own availability, conflict and notification contract.

## Appendix A: copyable build brief

Give the coding agent this entire brief, Appendix B and Appendix C as one task. Replace owner choices with your authorized business values. Keep credentials and customer identities out of the prompt until you are working in the permitted environment.

This is a documentation-only reference for a business owner to give an AI coding agent. It reports no built app, test booking, email, calendar connection or release. The owner supplies a real service, operating hours, named consenting customers, a permitted contact channel and the exact Replit/Clerk/Resend accounts. Start with synthetic names and owner-controlled addresses. Inspect actual plan charges, data location, retention, service terms and consent requirements before using real customer data.

Build one persistent app for **one service resource** (for example one appointment desk), with owner-managed services and dated availability slots, customer booking and history, safe reschedule/cancel, owner changes, and explicit in-app/email notification states. The app's PostgreSQL slots are the authoritative calendar; it neither reads nor changes Google/Outlook calendars. The owner must keep any outside calendar in sync manually or HOLD real use when external conflicts would matter. Do not present an exported calendar or a calendar-only mockup as the booking system.

Use Replit Agent in Plan mode with this entire brief, the SQL schema and API contract delivered alongside it. Implement React UI, Express server, Replit-managed Clerk identity and PostgreSQL. Use a supported Active or Maintenance LTS Node.js runtime (22 or 24 as listed on 22 October 2026), only if the actual Replit development and published environments and locked dependencies support it; Node 20 is EOL. The reader inspects generated source, lockfile and runtime. HOLD if the supported combination or required Replit features are unavailable. Pin the actual installed `@clerk/express`, PostgreSQL client, Express, React, `@js-temporal/polyfill` and `resend` versions and check each documented method against those versions. No package installation or app execution is claimed here.

### Owner choices and authority

The first owner is seeded by authenticated database administration with the exact Clerk user ID in that environment. There is no public owner bootstrap or anonymous booking. Keep a data-free public landing page and the actual generated managed-Clerk proxy, sign-in, sign-up, callback, required authentication bootstrap and static paths accessible without app SQL membership or custom app CSRF; retain the provider's security controls. Identity sign-up never enrolls a customer or owner. Discover the exact generated paths and locked SDK configuration instead of assuming a proxy route. The owner enrolls a specific existing Clerk user as a customer after checking identity; a browser email or name never creates membership. Private customer and owner pages and APIs check the current Clerk session and active SQL role on the server. Mount the actual managed-Clerk proxy before body parsers; keep route-specific raw signed Resend webhook handling before general JSON parsing. Neither provider transport nor the webhook may sit behind session-required, SQL-membership or custom app-CSRF guards. Clerk authentication-state middleware may run without requiring a session; the webhook requires its provider signature. A customer sees only their own bookings, history and notification state. An owner sees all service slots and bookings for this one business. Neither can change an unrelated customer's record through a guessed ID. Private app routes require parameterized SQL, and app browser mutations require allowed Origin and a session-bound CSRF token. Authentication bootstrap and signed webhook transport remain outside those custom app mutation checks. Clerk sign-in does not substitute for app membership.

The owner creates one or more active services with a name and 15–240 minute duration. They set one IANA time zone for the business and publish **finite dated slots**, each with a local date/time **and explicit UTC offset**. The server rejects an invalid zone, nonexistent local time or offset that does not match that zone, and stores the exact UTC start/end plus the selected local display, service name and elapsed duration. A fall-back repeated hour is allowed only when the owner explicitly chooses which offset/occurrence. Service duration is elapsed minutes, so an appointment crossing a clock change still occupies its exact UTC interval. A changed service duration, name or zone affects new slots only; an existing open slot stays bookable at its stored interval/duration while its service remains active. Existing confirmations/history render the stored slot zone/offset and name, not today's settings. The owner chooses booking lead time, maximum advance window and customer cancellation/reschedule cutoff (example defaults: 2 hours, 60 days and 24 hours), displays the actual policy, and saves it in versioned SQL settings. The committed booking snapshots its cutoff/deadline; a later settings edit does not silently change that customer's current change rights. Do not silently fabricate opening hours or time zones.

The owner can create, inspect, block or reopen open slots. A booked slot cannot be blocked or edited until the owner has explicitly cancelled or rescheduled its booking. Booking and owner blocking acquire the same slot row lock and recheck after waiting; a block cannot leave a newly confirmed booking on a blocked slot. All open slots across services share the resource and cannot overlap; back-to-back `[start,end)` slots are allowed. PostgreSQL exclusion and unique constraints must reject concurrent conflicts. The customer sees actual open slots only after a fresh server check, then books one exact slot. The confirmation is the committed booking ID, slot time, service and revision returned by the server, not the click or an email. On reload the customer sees current booking and immutable history. Two customers racing for one slot cannot both confirm. An identical lost-response retry returns the committed result even after it waits for the winner's lock; a changed binding conflicts. A changed or blocked slot is unavailable even if an old browser page shows it.

Rescheduling moves one confirmed booking to one other currently open slot of the same active service for an active enrolled customer in a single transaction and records both historical slot/policy snapshots. The new appointment receives the then-current policy snapshot and absolute self-change deadline; a settings edit alone never changes an already committed deadline. Cancelling changes it to cancelled and frees the slot while retaining history. An active customer can cancel their own confirmed booking until its stored deadline, even when the current booking lead window is already closed; cancellation does not depend on present service availability. An active owner can cancel a booking for a deactivated customer or disabled service with an audited reason to clear the resource, while the customer remains denied private app access and self-service. Owner-initiated changes use separate audited actions and present a clear in-app event to that customer. They cannot delete history to hide a change. A customer's self-service changes follow its booking's stored deadline; owner override requires an entered reason visible in the audit, not a silent bypass. A late request or stale revision returns a clear conflict/policy response, never an unrecorded change.

### Confirmation and notification boundaries

Every committed booking change produces a durable in-app confirmation and an outbox row in the same SQL transaction. The customer chooses `in_app` or **requested email** at booking; owner changes inherit that choice. For email, the server retrieves the customer's current **verified primary** Clerk email address, checks active recipient membership, the selected Resend sender domain and business permission, then freezes one recipient, sender, subject and plain-text body for that event. A legitimate owner cancellation for a deactivated customer still commits its history, but its email is `blocked` with no provider request while recipient authority is absent. If identity, verification, consent, sender authorization or provider account access is uncertain, preserve the booking change and show `notification blocked`; never send to a browser-supplied address or silently show “sent.” The owner can see the blocked state and contact the customer through an independently permitted channel. Messages contain only the service name, exact local time with zone/offset, booking reference and change type; no private notes or full customer list.

Use Resend's documented single-email API with a unique persisted idempotency key and one recipient. Keep its API key and webhook signing secret server-only. A successful API response is **accepted for sending**, not delivered or read. A signature-verified `email.delivery_delayed` event changes the internal state to `delayed`; `email.delivered` means delivery to the recipient mail server and `email.bounced` means bounce. Neither `delivered` nor `accepted` proves an inbox read. Persist the verified webhook's minimal recipient/tag/provider/event-time binding so a pre-response event can be replayed after restart. Send responses and webhook handling lock the same outbox row; late accepted/delayed events cannot regress terminal delivery states. A timeout or lost send response becomes `unknown`: reconcile the exact request, provider logs/event and recipient before a retry. Resend documents 24-hour idempotency retention; never blindly reuse a key after that interval or change the payload under it. One owner-reviewed notice reissue may succeed from the latest conclusively unsent/rejected or bounced notice; its predecessor can have only one successor, so two different retry keys cannot create parallel sends. Do not let email failure roll back or fabricate the committed booking.

The published public HTTPS host must allow the actual managed authentication transport and signed Resend webhook while protecting all booking/member/admin routes through Clerk and SQL membership. Use a public Replit host only with this app-level restriction; the webhook itself accepts verified raw signed payload only and reveals no booking data. A host-wide invite restriction that blocks Resend is unsuitable for requested-email notifications. Check access from a guest and an unlisted account. If the owner cannot provide this boundary, the actual sender domain, verified customer address, notification permission, or truthful delivery state, HOLD email-enabled real release. Do not claim an app, email or external calendar was actually connected or tested by these instructions.

### Reader-facing delivery

The React UI needs an owner settings/services page, dated slot editor with a reviewed zone/offset preview, bounded owner booking list with reasoned changes and notification recovery, and a customer service/slot picker, confirmation page, own booking history, reschedule/cancel controls and plain notification state. Show conflicts from a fresh server result and do not optimistically label a booking confirmed. The owner must be able to see held/unknown notices without exposing other customers or recipient addresses in customer views.

Sources for this brief: [Replit Agent](https://docs.replit.com/features/agent/overview), [Replit managed Clerk](https://docs.replit.com/features/auth-and-identity/clerk-auth), [Replit SQL environments](https://docs.replit.com/features/data-and-storage/development-and-production), [Clerk Express authentication](https://clerk.com/docs/reference/express/get-auth), [PostgreSQL range constraints](https://www.postgresql.org/docs/current/rangetypes.html), [Temporal time-zone handling](https://tc39.es/proposal-temporal/docs/timezone.html), [Resend send API](https://resend.com/docs/api-reference/emails/send-email), [Resend idempotency](https://resend.com/docs/dashboard/emails/idempotency-keys), [Resend webhook verification](https://resend.com/docs/webhooks/verify-webhooks-requests), [Resend delayed delivery](https://resend.com/docs/webhooks/emails/delivery-delayed), and [Node release status](https://nodejs.org/en/about/previous-releases).

## Appendix B: SQL migration for a new empty PostgreSQL database

Review and apply this to the reader-owned empty Development database. Verify the corresponding Production migration and data setup before release. The SQL below is reproduced literally from the reviewed reference.

```sql
-- Reference migration for a NEW empty PostgreSQL database only. Inspect it
-- before applying separately in the reader's Development and Production DBs.
-- One business owner, one physical booking resource, named enrolled customers.
-- Application UUIDs come from the server; browser IDs are never authority.
BEGIN;

CREATE TABLE app_member (
  id uuid PRIMARY KEY,
  clerk_user_id text NOT NULL UNIQUE,
  role text NOT NULL CHECK (role IN ('owner','customer')),
  active boolean NOT NULL DEFAULT true,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE business_settings (
  singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
  zone_id text NOT NULL CHECK (char_length(zone_id) BETWEEN 3 AND 100),
  booking_lead_minutes integer NOT NULL CHECK (booking_lead_minutes BETWEEN 0 AND 10080),
  max_advance_days integer NOT NULL CHECK (max_advance_days BETWEEN 1 AND 365),
  self_change_cutoff_minutes integer NOT NULL CHECK (self_change_cutoff_minutes BETWEEN 0 AND 10080),
  version integer NOT NULL DEFAULT 1 CHECK (version > 0)
);

CREATE TABLE service (
  id uuid PRIMARY KEY,
  name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 100),
  duration_minutes integer NOT NULL CHECK (duration_minutes BETWEEN 15 AND 240),
  active boolean NOT NULL DEFAULT true,
  version integer NOT NULL DEFAULT 1 CHECK (version > 0),
  created_at timestamptz NOT NULL DEFAULT now()
);

-- A finite dated slot is the app's canonical calendar. UTC instants are
-- authoritative. zone_id and local_start_text preserve what the owner chose.
-- All open slots share ONE resource, so even different services cannot overlap.
CREATE TABLE availability_slot (
  id uuid PRIMARY KEY,
  service_id uuid NOT NULL REFERENCES service(id) ON DELETE RESTRICT,
  starts_at timestamptz NOT NULL,
  ends_at timestamptz NOT NULL,
  zone_id text NOT NULL CHECK (char_length(zone_id) BETWEEN 3 AND 100),
  local_start_text text NOT NULL CHECK (char_length(local_start_text) BETWEEN 20 AND 100),
  service_name_snapshot text NOT NULL CHECK (char_length(service_name_snapshot) BETWEEN 1 AND 100),
  duration_minutes_snapshot integer NOT NULL CHECK (duration_minutes_snapshot BETWEEN 15 AND 240),
  state text NOT NULL CHECK (state IN ('open','blocked')),
  version integer NOT NULL DEFAULT 1 CHECK (version > 0),
  created_at timestamptz NOT NULL DEFAULT now(),
  CHECK (ends_at > starts_at),
  CHECK (ends_at = starts_at + make_interval(mins => duration_minutes_snapshot)),
  CONSTRAINT open_slots_never_overlap EXCLUDE USING gist
    (tstzrange(starts_at, ends_at, '[)') WITH &&)
    WHERE (state = 'open')
);
CREATE INDEX slot_time_state ON availability_slot(starts_at, state);

CREATE TABLE booking (
  id uuid PRIMARY KEY,
  slot_id uuid NOT NULL REFERENCES availability_slot(id) ON DELETE RESTRICT,
  customer_id uuid NOT NULL REFERENCES app_member(id) ON DELETE RESTRICT,
  status text NOT NULL CHECK (status IN ('confirmed','cancelled')),
  notification_channel text NOT NULL CHECK (notification_channel IN ('in_app','email')),
  policy_version_snapshot integer NOT NULL CHECK (policy_version_snapshot > 0),
  self_change_cutoff_minutes_snapshot integer NOT NULL CHECK (self_change_cutoff_minutes_snapshot BETWEEN 0 AND 10080),
  self_change_deadline_at timestamptz NOT NULL,
  request_key text NOT NULL UNIQUE CHECK (char_length(request_key) BETWEEN 32 AND 80),
  request_binding_sha256 char(64) NOT NULL,
  revision integer NOT NULL DEFAULT 1 CHECK (revision > 0),
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);
-- The database, not an availability read or browser state, breaks the race.
CREATE UNIQUE INDEX one_confirmed_booking_per_slot
  ON booking(slot_id) WHERE status = 'confirmed';
CREATE INDEX booking_customer_status ON booking(customer_id,status,updated_at DESC);

CREATE TABLE booking_event (
  id uuid PRIMARY KEY,
  booking_id uuid NOT NULL REFERENCES booking(id) ON DELETE RESTRICT,
  revision integer NOT NULL CHECK (revision > 0),
  action text NOT NULL CHECK (action IN ('book','reschedule','cancel','owner_reschedule','owner_cancel')),
  request_key text NOT NULL UNIQUE CHECK (char_length(request_key) BETWEEN 32 AND 80),
  request_binding_sha256 char(64) NOT NULL,
  actor_member_id uuid NOT NULL REFERENCES app_member(id) ON DELETE RESTRICT,
  old_slot_id uuid REFERENCES availability_slot(id) ON DELETE RESTRICT,
  new_slot_id uuid REFERENCES availability_slot(id) ON DELETE RESTRICT,
  owner_reason text,
  policy_version_snapshot integer NOT NULL CHECK (policy_version_snapshot > 0),
  self_change_cutoff_minutes_snapshot integer NOT NULL CHECK (self_change_cutoff_minutes_snapshot BETWEEN 0 AND 10080),
  self_change_deadline_at timestamptz NOT NULL,
  old_slot_snapshot jsonb,
  new_slot_snapshot jsonb,
  created_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE (booking_id,revision),
  CHECK ((old_slot_snapshot IS NULL OR jsonb_typeof(old_slot_snapshot) = 'object') AND
         (new_slot_snapshot IS NULL OR jsonb_typeof(new_slot_snapshot) = 'object')),
  CHECK ((action = 'book' AND old_slot_snapshot IS NULL AND new_slot_snapshot IS NOT NULL) OR
         (action IN ('reschedule','owner_reschedule') AND old_slot_snapshot IS NOT NULL AND new_slot_snapshot IS NOT NULL) OR
         (action IN ('cancel','owner_cancel') AND old_slot_snapshot IS NOT NULL AND new_slot_snapshot IS NULL))
);

-- One immutable recipient/content/idempotency binding per booking revision.
-- Provider acceptance is distinct from verified mail-server delivery.
CREATE TABLE notification_outbox (
  id uuid PRIMARY KEY,
  booking_event_id uuid NOT NULL REFERENCES booking_event(id) ON DELETE RESTRICT,
  notice_revision integer NOT NULL CHECK (notice_revision > 0),
  predecessor_outbox_id uuid UNIQUE REFERENCES notification_outbox(id) ON DELETE RESTRICT,
  reissue_actor_member_id uuid REFERENCES app_member(id) ON DELETE RESTRICT,
  recipient_member_id uuid NOT NULL REFERENCES app_member(id) ON DELETE RESTRICT,
  recipient_email text,
  from_email text,
  subject text,
  text_body text,
  request_sha256 char(64),
  idempotency_key text UNIQUE,
  reissue_request_key text UNIQUE,
  reissue_binding_sha256 char(64),
  state text NOT NULL CHECK (state IN
    ('not_requested','blocked','ready','sending','unknown','accepted','delayed','delivered','bounced','rejected')),
  provider_email_id text UNIQUE,
  attempted_at timestamptz,
  state_changed_at timestamptz NOT NULL DEFAULT now(),
  error_class text,
  UNIQUE (booking_event_id,notice_revision),
  CHECK (predecessor_outbox_id IS NULL OR predecessor_outbox_id <> id),
  CHECK ((notice_revision = 1 AND predecessor_outbox_id IS NULL AND reissue_actor_member_id IS NULL AND reissue_request_key IS NULL AND reissue_binding_sha256 IS NULL) OR
         (notice_revision > 1 AND predecessor_outbox_id IS NOT NULL AND reissue_actor_member_id IS NOT NULL AND reissue_request_key IS NOT NULL AND reissue_binding_sha256 IS NOT NULL)),
  CHECK (state IN ('not_requested','blocked') OR
    (recipient_email IS NOT NULL AND from_email IS NOT NULL AND
     subject IS NOT NULL AND text_body IS NOT NULL AND
     request_sha256 IS NOT NULL AND idempotency_key IS NOT NULL))
);
CREATE INDEX notification_outbox_state ON notification_outbox(state,state_changed_at);

CREATE TABLE notification_webhook_event (
  provider_event_id text PRIMARY KEY,
  provider_email_id text NOT NULL,
  event_type text NOT NULL,
  recipient_email text,
  booking_notice_tag text,
  event_created_at timestamptz,
  matched_outbox_id uuid REFERENCES notification_outbox(id) ON DELETE RESTRICT,
  state text NOT NULL CHECK (state IN ('pending','processed','hold')),
  received_at timestamptz NOT NULL DEFAULT now(),
  processed_at timestamptz,
  error_class text,
  CHECK (state = 'hold' OR
    (recipient_email IS NOT NULL AND booking_notice_tag IS NOT NULL AND event_created_at IS NOT NULL)),
  CHECK (state <> 'processed' OR matched_outbox_id IS NOT NULL)
);

CREATE TABLE app_audit (
  id uuid PRIMARY KEY,
  actor_member_id uuid REFERENCES app_member(id),
  action text NOT NULL CHECK (action IN
    ('enroll','deactivate','service_change','slot_create','slot_block','slot_unblock',
     'book','reschedule','cancel','owner_reschedule','owner_cancel',
     'notice_dispatch','notice_hold','notice_reconcile','notice_reissue')),
  subject_id uuid,
  created_at timestamptz NOT NULL DEFAULT now()
);
COMMIT;

-- First owner is seeded by authenticated administration with the exact
-- verified Clerk user ID in THIS environment. No public bootstrap route.
-- INSERT INTO app_member(id,clerk_user_id,role) VALUES ($1,$2,'owner');
```

## Appendix C: API, transactions and reader acceptance

These are build requirements and documented checks, not observed results. The reader's generated app must implement the whole contract and inspect its installed dependency interfaces before using real customers. Store secrets only in server environment variables. Use HTTPS, fixed return origins, `Cache-Control: no-store` on private responses and parameterized SQL with one checked-out connection per transaction.

### Identity, settings and time model

Preserve the actual Replit-generated managed Clerk proxy, sign-in/sign-up, callbacks and necessary authentication bootstrap/static routes. Discover their exact paths and configuration from the generated integration and locked SDK; do not assume one fixed proxy path or replace managed credentials. Mount the managed Clerk proxy and `clerkMiddleware()` before all body parsers. It may attach authentication state without requiring a session. Keep a minimal public landing page free of booking, availability, service, member, owner, confirmation and notification data and without automatic sign-in redirection. Provider authentication routes remain outside app SQL membership and custom app Origin/CSRF checks while retaining provider and transport security. Public sign-up grants no app membership or owner role. For every private app page and browser API, use `getAuth(req)` and query the current active `app_member` row by verified Clerk `userId`; reject a guest 401, signed-in unlisted/inactive user 403 and customer attempt at owner routes 403. The first owner is seeded by authenticated administration, not an HTTP bootstrap. Owner enrollment accepts an exact existing Clerk user ID, verifies it with the installed backend `clerkClient.users.getUser(userId)` and the human's identity, then inserts one customer row. No self-assigned owner or anonymous booking. For private app browser mutations require an exact allowed Origin and a session-bound CSRF token: server issues an HMAC-signed token containing the current Clerk session ID, user ID, expiry and nonce; verify signature, expiry and both authenticated IDs before POST/PATCH, with the signing key server-only and rotation handled by short token expiry. Reject missing/mismatched Origin or token. IDs in paths or bodies are selectors only; authorize every joined record again on the server.

The single `business_settings` row records the owner's IANA zone, booking lead, advance window and self-change cutoff. Owner changes increment its version; expose the current policy to customers and explain that existing bookings keep their committed cutoff. The server parses each proposed owner slot start with the locked `@js-temporal/polyfill` `Temporal.ZonedDateTime.from` using a full local timestamp, explicit offset and `[IANA/Zone]`, with offset rejection, then converts to a UTC Instant. Reject a nonexistent local time, mismatched offset, unsupported zone or omitted offset; at a repeated fall-back hour require the owner to choose the actual offset. Compute `ends_at` as the start Instant plus the service's elapsed minutes. Persist UTC start/end, selected zone, local text, service name and elapsed duration on that slot. Render an existing booking/event from its stored slot snapshot, including the selected zone and offset, even after owner zone, service name or duration edits. New slots use current settings and service; old-duration open slots remain bookable under their stored duration if the service is active, and a reschedule may select one of those old-duration slots of the same service. Re-read actual package/Node time-zone support and hold if the generated code cannot reproduce this interpretation. Do not parse a browser locale date into an unstated server time zone.

For one physical resource, every `availability_slot` in state `open` participates in the PostgreSQL `tstzrange(...,'[)')` GiST exclusion constraint, across **all services**. It rejects overlapping open slots even when two owner requests race. A slot is a finite dated appointment opportunity, not an inferred weekly recurrence. The owner may publish multiple dates in one reviewed batch, but each must pass the same per-row checks; a PostgreSQL exclusion failure rolls back that batch or the failed individual slot. A blocked slot may overlap another but cannot be reopened if the exclusion rule would fail. The owner cannot block, delete or edit a slot with a confirmed booking; cancel/reschedule the customer booking explicitly first. A service-duration or business-zone edit does not silently rewrite existing UTC slots.

### Route contract

| Route | Required behavior |
| --- | --- |
| `GET /api/services` | Enrolled users receive enabled services with actual name/duration and current booking policy; no private member data. |
| `GET /api/me/csrf` | Authenticated active member gets a short-lived session-bound mutation token in a private no-store response. |
| `GET /api/slots?from=&to=&serviceId=` | Enrolled users receive a bounded 60-day UTC range of current open, unbooked, future slots for one enabled service, with local zone/offset and slot version. This is a hint, not a reservation. |
| `GET /api/me/bookings` and `GET /api/me/bookings/:id` | Customer's own current bookings, status, immutable event history and safe notification state; owner uses a separate scoped admin list. Foreign ID 404. |
| `POST /api/me/bookings` | Body `{slotId,expectedSlotVersion,notificationChannel,requestKey}`. Check current membership/slot/service/time policy and atomically create exactly one confirmed booking, event and outbox row. Return committed ID/revision and notification state. |
| `POST /api/me/bookings/:id/reschedule` | Body `{newSlotId,expectedNewSlotVersion,expectedRevision,requestKey}`. Same customer, same service, before cutoff; atomic move to current open new slot, history and new outbox. |
| `POST /api/me/bookings/:id/cancel` | Body `{expectedRevision,requestKey}`. Same customer, before cutoff; mark cancelled, preserve history, create outbox. |
| `GET /api/admin/bookings` | Owner-only bounded list with exact customer/service/slot, event and notification state; no bulk email addresses in customer UI. |
| `GET /api/admin/settings`, `GET /api/admin/services`, `GET /api/admin/slots?from=&to=` | Owner-only current policy, versioned services and bounded 60-day dated slots including blocked/booked states for reviewed edits. |
| `POST /api/admin/customers` and `PATCH /api/admin/customers/:id/deactivate` | Owner-only exact identity enrollment/deactivation and audit. Deactivation stops app access but does not cancel existing booking or assert notification. |
| `POST /api/admin/services`, `PATCH /api/admin/services/:id`, `PATCH /api/admin/settings` | Owner-only `{name,durationMinutes,active}` service or `{zoneId,bookingLeadMinutes,maxAdvanceDays,selfChangeCutoffMinutes}` settings; PATCH includes current `expectedVersion`. Validate ranges and audit. Existing booked slots remain unchanged. |
| `POST /api/admin/slots`, `PATCH /api/admin/slots/:id/block`, `PATCH /api/admin/slots/:id/unblock` | Owner-only create `{serviceId,localStartWithOffsetAndZone}` or state change `{expectedVersion}`. Current service duration and database overlap rule apply; a confirmed booking prevents block. On a lost owner-create response, re-read the bounded slot list before retrying. |
| `POST /api/admin/bookings/:id/reschedule` and `/cancel` | Owner-only explicit `reason`, `expectedRevision`, `requestKey`; reschedule also takes `newSlotId,expectedNewSlotVersion`. Same atomic transition and customer notification history; no direct row edit. |
| `POST /api/admin/notifications/:id/dispatch` | Owner-only exact ready outbox row. Initiate one frozen provider request after atomically claiming it. No client-triggered resend or arbitrary address. |
| `POST /api/admin/notifications/:id/reconcile` | Owner-only exact provider-ID/readback/log-based resolution. Unknown outcome is never blindly resent. Audit result. |
| `POST /api/admin/notifications/:id/reissue` | Owner-only body `{requestKey,priorNoticeRevision}`. Creation of one new notice revision/key after a definitely unsent blocked/rejected state or a reviewed bounced state, fresh verified recipient and permission. Never reissue an unknown send without conclusive reconciliation. |
| `POST /api/resend/webhook` | Public raw-body endpoint, verified `svix-*` signature and endpoint secret, idempotent event receipt. No Clerk/CSRF but no booking data response. |

### Atomic booking and change protocol

The browser creates an unpredictable 32–80 character request key for each booking mutation and reuses it after a lost response. Authenticate current actor/membership, validate the request shape and compute an immutable canonical binding of actor ID, action, booking ID if any, chosen slot ID/version if any, expected booking revision and notification choice. Look up `booking` and `booking_event` by key **before** checking mutable slot, cutoff or revision; a same-key/same-binding row returns its committed result, changed binding returns 409. For a miss, enter the transaction and shared locks below, then **repeat** the exact key/binding lookup after waits and before every mutable denial. If the winner committed while this caller waited, return it even if slot is now occupied or the old revision/cutoff changed. The database unique request keys are the final race guard. On a unique violation, rollback the failed PostgreSQL transaction before a new read-only lookup of the exact key/binding; return the winner or the correctly scoped conflict. Never insert a second event/outbox or send twice. A different key competing for an occupied slot or stale revision receives 409.

Use PostgreSQL `READ COMMITTED` and a consistent lock hierarchy for **all** booking, owner slot/service/settings and membership changes: relevant `app_member` rows (including the acting owner and affected customer) by UUID, then singleton `business_settings`, then `service` rows by UUID, then involved `availability_slot` rows by UUID, then the `booking` row. A preliminary unlocked booking/slot read may discover candidate IDs but grants no authority. After the locks, reread the **action-specific** actor/customer, settings, service, slot and booking predicates with a fresh statement snapshot; if the candidate row relationships changed while waiting, rollback and restart the bounded transaction from the key lookup. Owner deactivation locks the acting owner and target customer member rows before changing `active`; owner policy edit locks the active owner then settings, service edit locks the active owner then service, and slot block/unblock locks the active owner then slot, then rechecks its version and a fresh confirmed-booking query before state change. New booking and reschedule **into a target slot** require an active enrolled customer, active actor, enabled target service, current lead/advance admission window and current open/unbooked target slot after lock waits. Cancellation has **no target admission**: the actor must be an active authorized owner or the active enrolled customer who owns the confirmed booking, with expected revision and either the customer's stored absolute deadline or the owner's recorded override reason. Cancellation does not reapply current lead/advance, require the old service to be active or the old slot to remain open. An active owner may cancel a confirmed booking for a deactivated customer or disabled service to clear it; the deactivated customer cannot use self-service. Deadlock/serialization errors roll back and retry a bounded number of times with the **same** key and fresh predicates; if still unresolved, return a safe retryable error without a confirmation. The range exclusion and confirmed-slot unique index remain final database guards, but do not replace these cross-table checks.

For a new book, acquire the shared locks, recheck the key and all current predicates, including selected slot version, state, service ID, lead/advance interval and any confirmed booking. Use the database server's current time, not browser time, for policy. Snapshot the settings version, cutoff minutes and absolute deadline (`slot.starts_at - cutoff minutes`) on `booking`; copy them to immutable `booking_event`. Insert `booking(status='confirmed')`, `booking_event(action='book',revision=1)` with `new_slot_snapshot`, `notification_outbox` and audit in the same transaction, then commit. Each event's slot snapshot contains slot ID, service ID/name, UTC start/end, selected zone/local text and elapsed duration, all copied from the locked slot. The partial unique confirmed-slot index decides a simultaneous double-book: a genuinely different key loses with 409 and fresh availability. Do not enqueue email or return confirmation before commit. A clear SQL/constraint failure rolls back all records.

For reschedule/cancel, first read the target booking only to identify candidate rows; lock and revalidate by the shared hierarchy above, including the booking row last. Repeat key lookup **before** ownership/status/revision/cutoff denial. Recheck actor authority, confirmed status, expected revision and the booking's **stored** absolute self-change deadline for customer self-service, not today's settings cutoff. Reschedule must use a different open, unbooked slot of the **same active service**, with an active enrolled customer, that slot's stored elapsed duration, valid time and expected version. Update the one booking's slot/revision; snapshot the **current** settings version/cutoff and new absolute deadline for its new appointment only, while the old event keeps the prior policy/deadline. Insert an immutable event with old/new slot IDs and full old/new slot snapshots, updated policy snapshot, outbox and audit. Cancel leaves the booking's current policy snapshot and records an event with old slot snapshot/new null; its prior slot becomes available only after commit. A customer can cancel only their own booking while active and before their stored deadline, even if the current booking lead time exceeds the time remaining. An active owner can cancel a deactivated customer's or disabled service's confirmed booking with an audited reason, regardless of lead/advance or old-slot availability; this owner action does not restore the customer's app access. For that deactivated recipient, keep the in-app event but set the email outbox to `blocked` with no provider request unless recipient membership and communication authority are restored through the normal owner-controlled process. The confirmed-slot unique index resolves a competing book or reschedule. Owner override bypasses the customer's stored cutoff only with a recorded reason, while still obeying history and notification rules. A customer cannot cancel somebody else's booking or switch a service by picking a new slot ID. Owner service or slot changes use their own optimistic versions and 409 on stale state. No hidden hard delete of a real booking, slot or event is exposed.

### Immutable notice, exact recipient and delivery states

Each committed `booking_event` has one initial outbox row (`notice_revision=1`); later owner-reviewed reissues create a new revision under the same event and preserve every prior row. For `in_app`, state is `not_requested` and the durable event is the customer's confirmation. For requested email, retrieve the customer's current Clerk User server-side before committing the event, find `primaryEmailAddressId` in `emailAddresses`, require the corresponding email object's verified state per the locked Clerk SDK, and check the owner's sender domain/permission. Freeze the one recipient, configured sender, plain-text subject/body, outbox UUID tag, SHA-256 of the exact provider payload and a unique key `booking-notice/<outbox-uuid>` (well under Resend's 256-character limit). Never accept `to`, `from`, customer ID or message text from the booking request. If verification or permission is unavailable, commit booking/event with outbox `blocked`, no provider request and a visible owner/customer recovery state. Recheck active membership and that the frozen recipient is still the verified primary address **at dispatch**; if it changed, block the old payload and require an owner-reviewed new notice revision/key, never silently retarget it. Reissue is permitted only after confirmed no-send, an explicit bounce or an owner-reviewed recipient correction, with current verified address and communication permission; an unknown send remains held until conclusive provider reconciliation. The reissue API requires an owner-generated request key bound to event, prior revision and recipient; a repeated request returns the existing revision, a changed binding 409.

For notice reissue, authorize the owner, validate key and requested predecessor ID/revision, then look up `reissue_request_key` **before** looking up today's Clerk email or provider permission. Compare stored actor, booking event, predecessor, revision and frozen recipient/binding; an identical retry returns the existing notice even if primary email later changed, and a different binding is 409. For a miss, lock the booking event **first**, then all its notice rows in ascending revision, and repeat that immutable-key check after any wait. The selected predecessor must be the latest revision with no successor; it must be conclusively unsent, rejected or bounced under the stated review rule. Check fresh verified recipient/permission only for this new revision, freeze its payload and key, and insert its `predecessor_outbox_id`, actor, binding, `notice_revision = predecessor + 1` and audit in one transaction. The unique predecessor foreign key permits **at most one** successor even if two distinct owner keys race. On a uniqueness error rollback, reread exact key; same binding returns existing, different key against superseded predecessor returns 409. Never fork notices from an old revision or auto-send both. An unresolved `unknown` predecessor cannot be reissued.

The post-commit handler can attempt a ready notice immediately; the owner dispatch route recovers ready rows after a process crash without a new scheduled job. Atomically change one ready row to `sending` before a provider call; concurrent dispatch finds no ready row. On restart, a stale `sending` row is an unknown outcome, not ready: owner reconciliation inspects the exact provider request/log before any same-key retry, and records `unknown` when delivery cannot be established. The exact supported Node call shape is:

```javascript
const result = await resend.emails.send({
  from: frozenFrom,
  to: [frozenRecipient],
  subject: frozenSubject,
  text: frozenText,
  tags: [{ name: 'booking_notice', value: String(outboxId) }]
}, { idempotencyKey: frozenKey });
```

Check the actual locked Resend SDK's `{data,error}` shape and require `data.id` before recording `accepted` with its provider email ID. A definitive validation rejection becomes `rejected` with a safe error class; a timeout, process crash after claim, unclear 5xx or lost response becomes `unknown`, without replacing the key or changing frozen payload. A same-key/same-body retry is allowed only within Resend's documented 24-hour retention after exact provider/log reconciliation and owner action. After 24 hours, the owner must resolve the exact request and recipient from provider records; if inconclusive, keep `unknown` and use a separately consented manual contact path. An accepted response means Resend accepted the API request, not delivery.

After the actual managed Clerk proxy/middleware and before normal JSON parsing, mount route-specific `express.raw({type:'application/json',limit:'256kb'})` for `/api/resend/webhook`. This public provider transport must not require a Clerk session, app membership or custom app Origin/CSRF; retain its provider controls and do not exempt any other app API. Verify the **raw** text and `svix-id`, `svix-timestamp`, `svix-signature` with the configured endpoint secret using the locked `resend.webhooks.verify` method; reject invalid signature 400. In one transaction deduplicate the verified `svix-id` and persist its parsed event type, provider email ID, event creation time, single recipient, `booking_notice` tag, candidate outbox ID if safely matched, processing state and receipt time. Store no signing secret or raw body. Resend webhook `data.tags` is an **object**; normalize its `booking_notice` value to a UUID. Resend GET `tags` is an **array** of `{name,value}`; normalize it separately rather than using object access. Match provider email ID (when known), frozen one recipient and tag to the same outbox. A malformed or mismatched binding is `hold`, never a mutation of another recipient.

Webhook processing, send-response recording and owner reconciliation all lock the **same outbox row `FOR UPDATE`** and use conditional transitions. The verified receipt is first inserted/deduplicated and committed; subsequent processing locks the matched outbox **before** locking/updating a pending receipt, avoiding an outbox/receipt lock inversion with send-response replay. If a signed webhook arrives before send response, retain the parsed verified binding as `pending`; after provider ID linkage or restart, the post-response handler or owner reconciliation route scans/replays pending rows by exact outbox tag/provider ID/recipient under that lock and marks processed only after the committed transition. If the provider ID remains unknown, keep pending/hold for owner investigation. A late send response may fill the exact provider ID and advance `sending`/`unknown` to `accepted`, but cannot replace `delayed`, `delivered` or `bounced` with `accepted`. The documented Resend `email.delivery_delayed` event advances `accepted`/`unknown` to internal state `delayed`; a verified `email.delivered` advances to terminal `delivered`; a verified `email.bounced` advances to terminal `bounced`. Duplicate events are no-ops. A delayed/accepted event after a terminal event is recorded but cannot regress it; contradictory terminal events go to owner `hold` for exact provider reconciliation, not an automatic overwrite. For out-of-order or inconsistent events, retrieve the **exact** known provider email via `resend.emails.get(providerEmailId)` and compare `last_event`, recipient and normalized tags before resolving; ambiguity stays `unknown`/`hold`. Provider-delivered means recipient mail-server acceptance, not inbox read. See [Resend's delayed-delivery event](https://resend.com/docs/webhooks/emails/delivery-delayed).

### Errors and acceptance matrix to run in the reader's own environment

401 guest; 403 unlisted/inactive/role violation; 404 foreign booking/notice; 409 occupied/stale slot, duplicate changed binding, late or concurrent change; 422 invalid service, zone/offset, range or request; 503 database/identity/provider uncertainty. Booking returns a committed state even if email is `blocked`, `ready`, `unknown` or `rejected`; UI never describes those as delivered. Retain a safe owner audit/request ID and exact provider object IDs only in restricted records.

Before real use, the reader checks with synthetic identities: owner creates valid slots across two services; overlapping open slots and a repeated-hour offset mismatch fail; a spring-forward nonexistent time fails; adjacent slots succeed; two customers race for one slot and only one booking commits; reload shows the same booking/history; duplicate same-key request returns the same result; altered key binding fails; customer A cannot read/change B's booking; customer reschedules to one open same-service slot and frees the old slot; cancellation frees it but retains history; stale/late/self-change and wrong-service requests fail; owner reasoned change records history; owner cannot block booked slot; inactive member loses access; missing/wrong Origin or CSRF token fails. A failed notice must leave the booking confirmed and visible, while a provider-accepted notice remains `accepted` until verified delivery; unknown send, duplicate signed webhook, invalid signature, wrong recipient/tag, bounced and delayed events keep their exact states. A conclusively unsent or bounced notice can be reissued once with a new key only after owner review; concurrent/same-key reissue returns one revision, changed binding fails, unknown-send reissue fails. Verify a guest can open the data-free landing and generated sign-in/bootstrap without a redirect loop or app membership/CSRF rejection. A new identity completes supported authentication but gains no booking/customer/owner privileges. Verify guest/unlisted/inactive denial of private data on the published public host while the signed webhook remains reachable without Clerk session or app CSRF; invalid signatures cause no mutation or data disclosure. Check the actual generated proxy precedes all parsers, raw webhook handling precedes general JSON, and Development/Production use distinct identity stores. Verify SQL persistence across restart and separate Development/Production identities and data. Record actual results or HOLD; none were run for this reference.

Specifically interleave identical-key book, reschedule and cancel calls so the second waits on the first's lock: each must return the same committed booking/event after a post-wait key recheck, while a different binding conflicts. Race block versus book in both orders and deactivation/service-disable versus book; after commit there must be no confirmed booking on a blocked slot or booking admitted after an inactive-member/disabled-service change won. Set stored customer cutoff to zero and current lead to 120 minutes: the active customer's own cancel one hour before start succeeds, while a fresh booking into a slot one hour away fails admission. Disable the booked service: the active customer can still cancel the confirmed booking, while new booking/reschedule into that service fails. Deactivate its customer: self-service fails, but the active owner can cancel with reason, preserved history and `blocked` email/no provider send. Edit the cutoff, service name/duration and business zone after booking; history retains its slot/policy snapshots and original absolute deadline, while a deliberate reschedule adopts the newly snapshotted policy and the chosen slot's stored duration. For email, verify pre-response signed webhook persistence and replay after restart, late accepted response and reordered `email.delivery_delayed` event without terminal-state regression, and provider GET array tags versus webhook object tags. Two different reissue keys against one predecessor yield at most one successor; replaying an old predecessor fails, while same-key retry after a primary-address change returns its original frozen notice. These are **reader-run checks**, not results observed for this guide.

## Sources & further reading

The [Replit Agent manual](https://docs.replit.com/features/agent/overview) documents Plan mode and agent-led development; it does not establish that this particular app has been built. The [managed Clerk integration](https://docs.replit.com/features/auth-and-identity/clerk-auth) and [Clerk Express middleware reference](https://clerk.com/docs/reference/express/clerk-middleware) inform authentication setup; actual generated paths and installed SDK behavior need inspection. [Replit database environments](https://docs.replit.com/features/data-and-storage/development-and-production) and [published access options](https://docs.replit.com/features/publishing/private-deployments) explain why public provider transport and private app records need separate checks. [PostgreSQL range constraints](https://www.postgresql.org/docs/current/rangetypes.html) support the overlap guard, while the exact transaction and lock design here is a proposed implementation contract. [Resend idempotency](https://resend.com/docs/dashboard/emails/idempotency-keys), [signature verification](https://resend.com/docs/webhooks/verify-webhooks-requests) and [delayed delivery](https://resend.com/docs/webhooks/emails/delivery-delayed) document provider behavior; no email or callback has been sent or observed for this guide.

## Sources

- [Replit Agent overview](https://docs.replit.com/features/agent/overview) — Plan mode, not app outcome
- [Replit managed Clerk Auth](https://docs.replit.com/features/auth-and-identity/clerk-auth) — Actual generated proxy and auth routes require inspection
- [Clerk Express middleware](https://clerk.com/docs/reference/express/clerk-middleware) — Auth state and Frontend API proxy, distinct from app membership
- [Replit development and production databases](https://docs.replit.com/features/data-and-storage/development-and-production) — Separate environment data; actual bindings require inspection
- [Replit publishing access](https://docs.replit.com/features/publishing/private-deployments) — Public host versus Invite only transport
- [PostgreSQL range types](https://www.postgresql.org/docs/current/rangetypes.html) — Exclusion constraints for overlapping ranges
- [Temporal time zones](https://tc39.es/proposal-temporal/docs/timezone.html) — Explicit local offset and zone interpretation
- [Node releases](https://nodejs.org/en/about/previous-releases) — 22 and 24 LTS, 20 EOL on check date
- [Resend send email](https://resend.com/docs/api-reference/emails/send-email) — Single-email API; installed SDK requires checking
- [Resend idempotency](https://resend.com/docs/dashboard/emails/idempotency-keys) — 24-hour retention
- [Resend webhook verification](https://resend.com/docs/webhooks/verify-webhooks-requests) — Raw signed request verification
- [Resend delayed delivery](https://resend.com/docs/webhooks/emails/delivery-delayed) — Exact email.delivery_delayed event identifier
