First Class Modules

Reservations

Turns a bookable product purchase into one secured workflow: contact identity, capacity-protected reservation, protected charge, calendar event, tenant-user assignment, resource booking, and scheduled before/after-service follow-up.

Open raw .md
Runtime key
module.reservations
Experience
Sell, schedule & follow up
Security
Server-enforced

Use Reservations for appointments, classes, tours, consultations, rentals, services, and other products that must be paid for, placed on a calendar, assigned to a tenant user, connected to a customer contact, and followed up consistently.

Note

The bundled renderer key is module.reservations. First-class means BuildWithHQ supplies a native, typed, secured runtime experience inside normal app provisioning and the signed-in user's existing permissions.

What it does

Turns a bookable product purchase into one secured workflow: contact identity, capacity-protected reservation, protected charge, calendar event, tenant-user assignment, resource booking, and scheduled before/after-service follow-up.

Key capabilities

  • Define bookable products with duration, price/currency, party limits, concurrent capacity, buffers, hold time, and default location, resource, assignee, and follow-up timing.
  • Create or safely reuse an authorized Contact and preserve the purchaser's name, email, phone, and notes.
  • Create a capacity-protected Reservation and linked Calendar event, with an optional Resource booking and an active tenant user assigned to deliver the service.
  • Queue the existing idempotent charge-intent pipeline using a protected payment-method reference; the API never receives card numbers, security codes, processor credentials, or raw payment tokens.
  • Keep paid bookings pending and private until the charge worker reports success, then promote the reservation, event, resource, and follow-ups transactionally.
  • Track Pending Payment, Requires Action, Confirmed, Checked In, Completed, Cancelled, No Show, and Payment Failed with optimistic concurrency and an audit timeline.
  • Schedule separate pre-service and post-service follow-up date/time, recipient email, subject, message, delivery state, attempts, sent time, and outbound-message reference.
  • Use idempotency keys to make checkout retries safe and prevent duplicate contacts, charges, calendar events, and reservations.
  • Let the tenant account owner choose disabled, optional, or required collection; creation, manual, or completion timing; full or deposit collection; partial-payment rules; currency; minimum charge; and paid-before-completion enforcement.
  • Give users with the existing billing capability a secured balance, payment history, charge history, and protected charge action for the reservation record.

Reservation purchase flow

StepCanonical resultImportant behavior
Choose product, time, and assigneeProduct + UTC window + eligible tenant userCall GET /api/modules/reservations/assignees with product, start time, and optional location; pass the selected optionId as assignedUserId to purchase. Duration, buffers, party size, location, and capacity are revalidated inside the transaction.
Identify customerContactAn authorized exact email match may be reused; otherwise one Contact is created and linked.
Reserve serviceReservation + Calendar event + optional Resource bookingA paid event stays a private payment hold until the charge succeeds.
ChargeBilling charge intentThe existing worker uses a protected method; retries use the same idempotency key.
Confirm and serveLifecycle + audit eventsCalendar, resource, and follow-up state remains synchronized.
Follow upPreService and PostService jobsEach has its own time, email, message, attempts, result, and evidence.

Tenant API

  • GET/POST /api/modules/reservations/products lists and manages the catalog.
  • GET /api/modules/reservations lists a secured schedule window; GET /api/modules/reservations/{recordId} returns detail and history.
  • GET /api/modules/reservations/assignees returns at most 100 eligible tenant users for the selected product, UTC start, and optional location. Use its selected ID as assignedUserId in purchase or reschedule; the server rechecks eligibility and conflicts on write.
  • POST /api/modules/reservations/purchase performs idempotent contact, calendar, reservation, and charge orchestration.
  • PUT /api/modules/reservations/{recordId}/schedule reschedules with the current revision and a freshly selected assignee. Purchase and reschedule can return 409 reservations_conflict when capacity, assignee availability, or the revision changes; refresh choices rather than blindly retrying.
  • PUT /api/modules/reservations/{recordId}/status applies a lifecycle transition.
  • PUT /api/modules/reservations/{recordId}/followups creates or reschedules a before/after-service follow-up.
  • GET/PUT /api/modules/payments/settings reads or changes the tenant-owned Reservations and Work Orders policies.
  • GET /api/modules/payments/records/{recordId} returns the secured balance/history; POST /api/modules/payments/records/{recordId}/charges queues an idempotent protected charge.
  • GET /api/modules/payments/methods lists display-only metadata for active consented methods; connector endpoints accept external secret references, never reusable credentials.

Reservation product prices and tenant payment policies currently use cents. Two-decimal charge currencies such as USD and EUR are supported. JPY, KRW, CLP and other zero-decimal currencies are rejected until the full API, storage and browser amount contract supports their minor units. Product writes return reservations_unsupported_currency; payment policy and direct-charge writes return tenant_payments_unsupported_currency.

Common uses

  • Paid consultations and professional-service appointments.
  • Tours, classes, lessons, events, and capacity-limited sessions.
  • Installations, inspections, salon/wellness visits, rentals, and service calls.
  • Free appointments that still need contact, calendar, assignment, capacity, and follow-up.

How it connects

Reservations reuse Contacts, Calendar, Resources, the billing charge-intent/payment ledger, Record Relations, and the communications worker boundary. The Reservation owns lifecycle; payment, contact, calendar, and message records remain canonical and linked instead of copied into a shadow subsystem.

Where applicable, its records use the universal RecordId conventions so they can participate in secured relationships, activity history, favorites, dynamic fields, notifications, Inbox attention, and global search without copying the source record.

Security and data boundary

List/detail access is inherited from secured universal Records. Product management and mutations require current DataRole capability. Locations and contact targets are revalidated in the user's scope; assignees must be active users assigned to the same app and selected location. Product capacity and opt-in consultation/appointment assignee exclusivity are rechecked transactionally on purchase, reschedule, confirmation, and delayed payment success. Existing shared-capacity products remain non-exclusive by default. Identity comes from verified server context. The tenant account owner controls payment policy and connector references; collection requires the existing CanBillRecords capability plus record scope. Raw card data and provider credentials are never accepted or persisted by Reservations.

  • The authenticated service derives the SaaS app, tenant account, user, DataRole, and location scope; browser identifiers are never authorization proof.
  • The page editor composes React components with validated data bindings. Those bindings call typed runtime APIs, whose application services execute reviewed stored procedures.
  • List, search, detail, relation, activity, favorite, and write operations reapply their required server-side permissions.

Add it to an app

  1. As the tenant account owner, configure an external payment-connector secret reference and choose separate Reservations and Work Orders collection policies; never add card fields to a page.
  2. Add module.reservations to an in-SaaS page and place that shared page in each intended user-type menu.
  3. Create each product with duration, price, capacity/party rules, buffer, hold, location/resource/assignee, and follow-up timing.
  4. Grant existing DataRole, CanBillRecords, and location access to users who may view, collect, sell, assign, check in, complete, cancel, or follow up.
  5. Exercise free, disabled, optional, required, deposit, manual, and paid-before-completion paths plus idempotent replay, full capacity, payment success/failure/Requires Action, calendar/resource synchronization, and both follow-ups before publishing.

Copy/paste the complete Reservations experience

The native block is the safest full starting point. It includes the product catalog, sell-and-reserve form, secured schedule, detail, lifecycle actions, follow-ups, linked payment panel, and bounded server pagination. Paste this page document into Raw JSON mode:

{
  "blocks": [
    {
      "_id": "reservations-workspace",
      "_type": "module.reservations",
      "props": {},
      "children": []
    }
  ]
}

The browser supplies only filters and action targets. SaaS, account, user, DataRole, location scope, billing capability, and database routing come from the authenticated server context.

Custom unstyled schedule and detail

This component asks SQL for one authorized time window and page. It does not download a tenant's complete schedule and reduce it in React. The window must be positive and no wider than 732 days; the page size is capped at 500.

import { FormEvent, useEffect, useMemo, useState } from "react";
import {
  reservationStatuses,
  type ReservationDetail,
  type ReservationList,
  type ReservationStatus,
} from "@buildwithhq/module-sdk";
import { getReservation, listReservations } from "./api";

const emptyResult: ReservationList = {
  contractVersion: 1,
  pageNumber: 1,
  pageSize: 100,
  totalRecords: 0,
  totalPages: 0,
  items: [],
};

type ReservationQueueProps = {
  assignedUserId?: string;
  locationId?: string;
};

export function ReservationQueue({ assignedUserId = "", locationId = "" }: ReservationQueueProps) {
  const window = useMemo(() => ({
    start: new Date(Date.now() - 30 * 86_400_000).toISOString(),
    end: new Date(Date.now() + 365 * 86_400_000).toISOString(),
  }), []);
  const [draftSearch, setDraftSearch] = useState("");
  const [search, setSearch] = useState("");
  const [status, setStatus] = useState<ReservationStatus | "">("");
  const [page, setPage] = useState(1);
  const [result, setResult] = useState<ReservationList>(emptyResult);
  const [selected, setSelected] = useState<ReservationDetail | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);
    listReservations(
      window.start, window.end, search, status, controller.signal,
      assignedUserId, locationId, page, 100,
    )
      .then(setResult)
      .catch((caught: unknown) => {
        if (!controller.signal.aborted) {
          setError(caught instanceof Error ? caught.message : "Reservations could not be loaded.");
        }
      })
      .finally(() => { if (!controller.signal.aborted) setLoading(false); });
    return () => controller.abort();
  }, [assignedUserId, locationId, page, search, status, window]);

  function submitSearch(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPage(1);
    setSearch(draftSearch.trim());
  }

  async function open(recordId: string) {
    setError(null);
    try { setSelected(await getReservation(recordId)); }
    catch (caught) {
      setError(caught instanceof Error ? caught.message : "The reservation could not be loaded.");
    }
  }

  return <section className="reservation-queue" aria-busy={loading}>
    <h1>Reservations</h1>
    <form role="search" onSubmit={submitSearch}>
      <label>Search<input value={draftSearch} onChange={e => setDraftSearch(e.target.value)} /></label>
      <label>Status<select value={status} onChange={e => { setPage(1); setStatus(e.target.value as ReservationStatus | ""); }}>
        <option value="">All</option>
        {reservationStatuses.map(value => <option key={value}>{value}</option>)}
      </select></label>
      <button disabled={loading}>Search</button>
    </form>
    {error && <p role="alert">{error}</p>}
    <ul>{result.items.map(item => <li key={item.recordId}>
      <button type="button" onClick={() => void open(item.recordId)}>
        <strong>{item.reservationNumber} - {item.productName}</strong>
        <span>{new Date(item.startUtc).toLocaleString()} - {item.contactName}</span>
        <small>{item.status} - {item.partySize} attendee(s)</small>
      </button>
    </li>)}</ul>
    <nav aria-label="Reservation pages">
      <button disabled={loading || result.pageNumber <= 1} onClick={() => setPage(value => value - 1)}>Previous</button>
      <span>Page {result.pageNumber} of {Math.max(1, result.totalPages)} - {result.totalRecords} reservations</span>
      <button disabled={loading || result.pageNumber >= result.totalPages} onClick={() => setPage(value => value + 1)}>Next</button>
    </nav>
    {selected && <article>
      <h2>{selected.reservationNumber} - {selected.productName}</h2>
      <p>{selected.contactName} - {selected.contactEmail || "No email"}</p>
      <p>{selected.status} - {selected.paymentStatus || "No payment required"}</p>
      <h3>Follow-ups</h3>
      <ul>{selected.followups.map(item => <li key={item.reservationFollowupId}>
        {item.followupType}: {new Date(item.scheduledUtc).toLocaleString()} - {item.deliveryStatus}
      </li>)}</ul>
    </article>}
  </section>;
}

Create and update bookable products

Product update is a full optimistic replacement. Start from the current product, preserve every value, apply the deliberate edits, and send its latest updatedUtc. Duration is 5–43,200 minutes; buffers 0–10,080; capacity and party size 1–100,000; hold 1–1,440 minutes.

import type { ReservationProduct, ReservationProductWrite } from "@buildwithhq/module-sdk";
import { saveReservationProduct } from "./api";

export async function createConsultation() {
  return saveReservationProduct({
    productKey: "consultation",
    productName: "Consultation",
    description: "A one-hour consultation",
    durationMinutes: 60,
    bufferBeforeMinutes: 15,
    bufferAfterMinutes: 15,
    priceCents: 12500,
    currencyCode: "USD",
    concurrentCapacity: 1,
    maxPartySize: 1,
    holdMinutes: 15,
    preServiceFollowupMinutes: 1440,
    postServiceFollowupMinutes: 1440,
    preServiceSubject: "Your consultation is tomorrow",
    postServiceSubject: "Thank you for meeting with us",
    isActive: true,
  });
}

export const productReplacement = (current: ReservationProduct): ReservationProductWrite => ({
  reservationProductId: current.reservationProductId,
  expectedUpdatedUtc: current.updatedUtc,
  productKey: current.productKey,
  productName: current.productName,
  description: current.description || "",
  durationMinutes: current.durationMinutes,
  bufferBeforeMinutes: current.bufferBeforeMinutes,
  bufferAfterMinutes: current.bufferAfterMinutes,
  priceCents: current.priceCents,
  currencyCode: current.currencyCode,
  concurrentCapacity: current.concurrentCapacity,
  maxPartySize: current.maxPartySize,
  defaultLocationId: current.defaultLocationId,
  defaultResourceId: current.defaultResourceId,
  defaultAssignedUserId: current.defaultAssignedUserId,
  holdMinutes: current.holdMinutes,
  preServiceFollowupMinutes: current.preServiceFollowupMinutes,
  postServiceFollowupMinutes: current.postServiceFollowupMinutes,
  preServiceSubject: current.preServiceSubject || "",
  postServiceSubject: current.postServiceSubject || "",
  isActive: current.isActive,
});

export async function changeProductPrice(current: ReservationProduct, priceCents: number) {
  return saveReservationProduct({ ...productReplacement(current), priceCents });
}

Purchase once, retry safely

Generate one idempotency key when checkout begins, persist it with that checkout attempt, and reuse it for ambiguous network retries. Do not create a new key until the user starts a genuinely new purchase. An existing Contact must be in scope; otherwise supply enough contact fields for the transaction to create one. There is no separate promise-of-availability endpoint: purchase is the authoritative transactional capacity check, and a 409 means the client should offer another time.

import type { ReservationPurchase } from "@buildwithhq/module-sdk";
import { purchaseReservation } from "./api";

export function beginCheckout(input: Omit<ReservationPurchase, "idempotencyKey">) {
  const idempotencyKey = crypto.randomUUID();
  const request: ReservationPurchase = { ...input, idempotencyKey };

  // Keep `request` in checkout state. Calling retry() again is safe after a timeout.
  return {
    idempotencyKey,
    retry: () => purchaseReservation(request),
  };
}

const checkout = beginCheckout({
  reservationProductId: "PRODUCT-GUID-FROM-LIST",
  startUtc: "2026-10-10T18:00:00.000Z",
  timeZoneId: "America/Los_Angeles",
  partySize: 1,
  assignedUserId: "ACTIVE-TENANT-USER-GUID",
  locationId: "AUTHORIZED-LOCATION-GUID",
  firstName: "Ada",
  lastName: "Lovelace",
  email: "[email protected]",
  paymentMethodId: "PROTECTED-CONSENTED-METHOD-ID",
  preServiceFollowupUtc: "2026-10-09T18:00:00.000Z",
  preServiceFollowupEmail: "[email protected]",
  postServiceFollowupUtc: "2026-10-11T18:00:00.000Z",
  postServiceFollowupEmail: "[email protected]",
});

export const purchase = () => checkout.retry();

Change state and schedule follow-up

Both actions use optimistic concurrency. Re-fetch detail after every successful mutation before enabling another action; never replay an old timestamp automatically. The server enforces the lifecycle and may refuse completion when the tenant payment policy requires a paid balance.

import type { ReservationDetail } from "@buildwithhq/module-sdk";
import { getReservation, setReservationStatus, updateReservationFollowup } from "./api";

export async function checkIn(current: ReservationDetail) {
  await setReservationStatus(current.recordId, {
    expectedUpdatedUtc: current.updatedUtc,
    status: "CheckedIn",
  });
  return getReservation(current.recordId);
}

export async function scheduleThankYou(current: ReservationDetail) {
  await updateReservationFollowup(current.recordId, {
    expectedUpdatedUtc: current.updatedUtc,
    followupType: "PostService",
    scheduledUtc: new Date(Date.now() + 86_400_000).toISOString(),
    recipientEmail: current.contactEmail || "",
    subject: "Thank you",
    bodyText: "Thank you for visiting us.",
  });
  return getReservation(current.recordId);
}

Optional starter styling

.reservation-queue { width: 100%; max-width: none; }
.reservation-queue form { align-items: end; display: flex; gap: .75rem; }
.reservation-queue form label { flex: 1; }
.reservation-queue input, .reservation-queue select { box-sizing: border-box; width: 100%; }
.reservation-queue > ul { list-style: none; margin: 1rem 0; padding: 0; }
.reservation-queue > ul button { background: transparent; border: 0; display: grid; gap: .25rem; padding: .75rem 0; text-align: left; width: 100%; }
.reservation-queue nav { align-items: center; display: flex; gap: 1rem; }
.reservation-queue article { border: 1px solid var(--line, #d8dee8); margin-top: 1rem; padding: 1rem; }
@media (max-width: 700px) { .reservation-queue form { align-items: stretch; flex-direction: column; } }

Exact data path

PurposeMethod and routeStored-procedure contract
Product catalogGET /api/modules/reservations/productssp_ReservationProducts_ListSecured
Create/replace productPOST /api/modules/reservations/productssp_ReservationProducts_UpsertSecured
Windowed scheduleGET /api/modules/reservationssp_Reservations_ListSecured; exact count, 1–500 rows/page
Detail/historyGET /api/modules/reservations/{recordId}sp_Reservations_GetSecured
Transactional purchasePOST /api/modules/reservations/purchasesp_Reservations_PurchaseSecured
Lifecycle actionPUT /api/modules/reservations/{recordId}/statussp_Reservations_SetStatusSecured
Follow-upPUT /api/modules/reservations/{recordId}/followupssp_ReservationFollowups_UpdateSecured

Paid purchases stay in a held/private state until the billing worker reports success; Calendar, Resource, Reservation, and follow-up visibility are promoted together. Payment method identifiers are protected, consented references. Card numbers, security codes, processor credentials, connection strings, and tenant database names never belong in page JSON or React props.

See the complete React component catalog for all generated blocks, or the headless guide for an application outside the tenant shell.

Build a professional Reservations dashboard

These six registry-backed presentation blocks let a designer turn the secured Reservations API into a complete admin page without writing a chart, grid, status badge, empty state, or timeline from scratch. The normal operational API begins at /api/modules/reservations; a dashboard-wide count or trend should come from a separate purpose-built presentation binding so the browser never downloads a broad record population to calculate one number.

The ZIP contains this feature's fictional design composition at pages/first-class/reservations-sample.json, its executable authenticated page at pages/first-class/reservations-live.json, and the catalog-driven FirstClassPresentationGallery.tsx. Use the sample only for visual design. The live page calls the real native API and reviewed stored procedures under the signed-in user's scope.

Payload kindRuntime blockUseful Reservations projection
metric-setcore.metric-stripUpcoming, confirmed, payment-held, and exception counts
entity-listcore.entity-listNext authorized appointments with contact and time
progress-listcore.progress-listDistribution by lifecycle, product, or payment state
series-chartcore.series-chartPurchases, check-ins, completions, and cancellations
data-gridcore.presentation-gridOne bounded schedule page with protected payment state
timelinecore.timelinePurchase, payment, confirmation, check-in, completion, and follow-up

Copy/paste design preview: all six blocks

This complete static page document renders immediately in Puck/Monaco and is useful while styling a template. Its names, counts, dates, and IDs are fictional design fixtures; static preview values are not live tenant facts.

Copy the complete six-block page JSON
{
  "blocks": [
    {
      "_id": "reservations-metrics",
      "_type": "core.metric-strip",
      "props": {
        "title": "Reservation operations",
        "asOfUtc": "2026-09-04T18:00:00Z",
        "items": [
          {
            "key": "upcoming",
            "label": "Upcoming",
            "value": 64,
            "format": "number",
            "tone": "primary"
          },
          {
            "key": "confirmed",
            "label": "Confirmed",
            "value": 48,
            "format": "number",
            "tone": "success"
          },
          {
            "key": "payment",
            "label": "Awaiting payment",
            "value": 7,
            "format": "number",
            "tone": "warning"
          },
          {
            "key": "attention",
            "label": "Need attention",
            "value": 3,
            "format": "number",
            "tone": "danger"
          }
        ]
      },
      "children": []
    },
    {
      "_id": "reservations-recent",
      "_type": "core.entity-list",
      "props": {
        "title": "Upcoming reservations",
        "hasMore": true,
        "items": [
          {
            "id": "reservations-sample-1",
            "recordId": "reservations-record-1",
            "primary": "Strategy consultation",
            "secondary": "Maya Chen - Sep 8, 10:00 AM",
            "status": {
              "key": "confirmed",
              "label": "Confirmed",
              "tone": "success"
            },
            "trailing": "1 guest"
          },
          {
            "id": "reservations-sample-2",
            "recordId": "reservations-record-2",
            "primary": "Strategy consultation - Follow-up",
            "secondary": "Maya Chen - Sep 8, 10:00 AM - Updated two hours ago by the assigned owner",
            "status": {
              "key": "in-review",
              "label": "In review",
              "tone": "primary"
            },
            "trailing": "Today"
          },
          {
            "id": "reservations-sample-3",
            "recordId": "reservations-record-3",
            "primary": "Strategy consultation - West region",
            "secondary": "Maya Chen - Sep 8, 10:00 AM - Related to three visible records at the Reno location",
            "status": {
              "key": "on-track",
              "label": "On track",
              "tone": "success"
            },
            "trailing": "3 related"
          },
          {
            "id": "reservations-sample-4",
            "recordId": "reservations-record-4",
            "primary": "Strategy consultation - Customer response",
            "secondary": "Maya Chen - Sep 8, 10:00 AM - Waiting for an external response before work can continue",
            "status": {
              "key": "scheduled",
              "label": "Scheduled",
              "tone": "warning"
            },
            "trailing": "Tomorrow"
          },
          {
            "id": "reservations-sample-5",
            "recordId": "reservations-record-5",
            "primary": "Strategy consultation - Regional operations review with a deliberately long title",
            "secondary": "Maya Chen - Sep 8, 10:00 AM - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
            "status": {
              "key": "needs-attention",
              "label": "Needs attention",
              "tone": "danger"
            },
            "trailing": "Review",
            "tertiary": "Long-content fixture: verify keyboard focus, wrapping, narrow columns, and mobile overflow before publishing."
          },
          {
            "id": "reservations-sample-6",
            "recordId": "reservations-record-6",
            "primary": "Strategy consultation - Completed preview",
            "secondary": "Maya Chen - Sep 8, 10:00 AM - Closed after review with its related evidence retained",
            "status": {
              "key": "complete",
              "label": "Complete",
              "tone": "success"
            },
            "trailing": "Closed"
          }
        ]
      },
      "children": []
    },
    {
      "_id": "reservations-bystatus",
      "_type": "core.progress-list",
      "props": {
        "title": "Reservations by state",
        "items": [
          {
            "key": "confirmed",
            "label": "Confirmed",
            "value": 48,
            "maximum": 86,
            "displayValue": "48",
            "tone": "success",
            "status": {
              "key": "confirmed",
              "label": "Confirmed",
              "tone": "success"
            }
          },
          {
            "key": "pending",
            "label": "Pending payment",
            "value": 7,
            "maximum": 86,
            "displayValue": "7",
            "tone": "warning",
            "status": {
              "key": "pending",
              "label": "Pending payment",
              "tone": "warning"
            }
          },
          {
            "key": "completed",
            "label": "Completed",
            "value": 31,
            "maximum": 86,
            "displayValue": "31",
            "tone": "primary",
            "status": {
              "key": "completed",
              "label": "Completed",
              "tone": "primary"
            }
          }
        ]
      },
      "children": []
    },
    {
      "_id": "reservations-trend",
      "_type": "core.series-chart",
      "props": {
        "title": "Booking activity",
        "variant": "bar",
        "defaultPeriodKey": "d7",
        "periods": [
          {
            "key": "d7",
            "label": "7 days",
            "labels": [
              "Fri",
              "Sat",
              "Sun",
              "Mon",
              "Tue",
              "Wed",
              "Thu"
            ],
            "series": [
              {
                "key": "primary",
                "label": "Purchased",
                "tone": "primary",
                "values": [
                  8,
                  5,
                  4,
                  12,
                  15,
                  11,
                  17
                ]
              },
              {
                "key": "secondary",
                "label": "Completed",
                "tone": "success",
                "values": [
                  6,
                  4,
                  3,
                  9,
                  12,
                  10,
                  14
                ]
              }
            ]
          }
        ]
      },
      "children": []
    },
    {
      "_id": "reservations-table",
      "_type": "core.presentation-grid",
      "props": {
        "title": "Reservation schedule",
        "columns": [
          {
            "key": "service",
            "label": "Service",
            "type": "text",
            "align": "left"
          },
          {
            "key": "contact",
            "label": "Contact",
            "type": "text",
            "align": "left"
          },
          {
            "key": "start",
            "label": "Start",
            "type": "date",
            "align": "left"
          },
          {
            "key": "status",
            "label": "Status",
            "type": "status",
            "align": "left"
          },
          {
            "key": "payment",
            "label": "Payment",
            "type": "status",
            "align": "left"
          }
        ],
        "rows": [
          {
            "id": "reservations-row-1",
            "recordId": "reservations-record-1",
            "cells": {
              "service": "Strategy consultation",
              "contact": "Maya Chen",
              "start": "2026-09-08T17:00:00Z",
              "status": {
                "key": "confirmed",
                "label": "Confirmed",
                "tone": "success"
              },
              "payment": {
                "key": "paid",
                "label": "Paid",
                "tone": "success"
              }
            }
          },
          {
            "id": "reservations-row-2",
            "recordId": "reservations-record-2",
            "cells": {
              "service": "Strategy consultation - Follow-up",
              "contact": "Maya Chen",
              "start": "2026-09-04T15:42:00Z",
              "status": {
                "key": "in-review",
                "label": "In review",
                "tone": "primary"
              },
              "payment": {
                "key": "in-review",
                "label": "In review",
                "tone": "primary"
              }
            }
          },
          {
            "id": "reservations-row-3",
            "recordId": "reservations-record-3",
            "cells": {
              "service": "Strategy consultation - West region",
              "contact": "Maya Chen",
              "start": "2026-09-04T12:18:00Z",
              "status": {
                "key": "on-track",
                "label": "On track",
                "tone": "success"
              },
              "payment": {
                "key": "on-track",
                "label": "On track",
                "tone": "success"
              }
            }
          },
          {
            "id": "reservations-row-4",
            "recordId": "reservations-record-4",
            "cells": {
              "service": "Strategy consultation - Customer response",
              "contact": "Maya Chen",
              "start": "2026-09-03T21:07:00Z",
              "status": {
                "key": "scheduled",
                "label": "Scheduled",
                "tone": "warning"
              },
              "payment": {
                "key": "scheduled",
                "label": "Scheduled",
                "tone": "warning"
              }
            }
          },
          {
            "id": "reservations-row-5",
            "recordId": "reservations-record-5",
            "cells": {
              "service": "Strategy consultation - Regional operations review with a deliberately long title",
              "contact": "Maya Chen - This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
              "start": "2026-09-03T16:31:00Z",
              "status": {
                "key": "needs-attention",
                "label": "Needs attention",
                "tone": "danger"
              },
              "payment": {
                "key": "needs-attention",
                "label": "Needs attention",
                "tone": "danger"
              }
            }
          },
          {
            "id": "reservations-row-6",
            "recordId": "reservations-record-6",
            "cells": {
              "service": "Strategy consultation - Completed preview",
              "contact": "Maya Chen",
              "start": "2026-09-02T19:14:00Z",
              "status": {
                "key": "complete",
                "label": "Complete",
                "tone": "success"
              },
              "payment": {
                "key": "complete",
                "label": "Complete",
                "tone": "success"
              }
            }
          }
        ],
        "page": {
          "pageNumber": 1,
          "pageSize": 6,
          "totalRecords": 64,
          "totalIsExact": true,
          "hasMore": true
        }
      },
      "children": []
    },
    {
      "_id": "reservations-timeline",
      "_type": "core.timeline",
      "props": {
        "title": "Reservation lifecycle",
        "hasMore": true,
        "items": [
          {
            "id": "reservations-event-1",
            "recordId": "reservations-record-1",
            "occurredUtc": "2026-09-04T17:58:00Z",
            "title": "Reservation confirmed",
            "description": "Payment succeeded and the calendar hold was promoted.",
            "actor": "Billing worker",
            "tone": "success"
          },
          {
            "id": "reservations-event-2",
            "recordId": "reservations-record-2",
            "occurredUtc": "2026-09-04T15:42:00Z",
            "title": "Reservation confirmed - Follow-up",
            "description": "Payment succeeded and the calendar hold was promoted. Updated two hours ago by the assigned owner.",
            "actor": "Avery Patel",
            "tone": "primary"
          },
          {
            "id": "reservations-event-3",
            "recordId": "reservations-record-3",
            "occurredUtc": "2026-09-04T12:18:00Z",
            "title": "Reservation confirmed - West region",
            "description": "Payment succeeded and the calendar hold was promoted. Related to three visible records at the Reno location.",
            "actor": "Sam Rivera",
            "tone": "success"
          },
          {
            "id": "reservations-event-4",
            "recordId": "reservations-record-4",
            "occurredUtc": "2026-09-03T21:07:00Z",
            "title": "Reservation confirmed - Customer response",
            "description": "Payment succeeded and the calendar hold was promoted. Waiting for an external response before work can continue.",
            "actor": "Maya Chen",
            "tone": "warning"
          },
          {
            "id": "reservations-event-5",
            "recordId": "reservations-record-5",
            "occurredUtc": "2026-09-03T16:31:00Z",
            "title": "Reservation confirmed - Regional operations review with a deliberately long title",
            "description": "Payment succeeded and the calendar hold was promoted. This deliberately longer supporting line verifies wrapping, truncation, responsive spacing, and dense dashboard behavior.",
            "actor": "Automation",
            "tone": "danger"
          },
          {
            "id": "reservations-event-6",
            "recordId": "reservations-record-6",
            "occurredUtc": "2026-09-02T19:14:00Z",
            "title": "Reservation confirmed - Completed preview",
            "description": "Payment succeeded and the calendar hold was promoted. Closed after review with its related evidence retained.",
            "actor": "Jordan Lee",
            "tone": "success"
          }
        ]
      },
      "children": []
    }
  ]
}

Copy/paste React composition

Copy InteractivePresentationComponents.tsx and its optional CSS from the Professional Foundation Developer Kit, then add this module-specific composition. It is semantic and unstyled by default; pass styled after importing interactive-presentation-components.css for the supplied polished foundation. Either version accepts only a bounded already-authorized document and fails closed through the shared strict parsers.

import {
  EntityList,
  MetricStrip,
  PresentationGrid,
  ProgressList,
  SeriesChart,
  Timeline,
} from "./InteractivePresentationComponents";

export interface ReservationsPresentationData {
  readonly metrics: unknown;
  readonly recent: unknown;
  readonly byStatus: unknown;
  readonly trend: unknown;
  readonly table: unknown;
  readonly timeline: unknown;
}

export interface ReservationsPresentationProps {
  /** Pass only the already-authorized presentation document returned by the API. */
  readonly data?: ReservationsPresentationData | null;
  readonly loading?: boolean;
  readonly error?: boolean;
  readonly styled?: boolean;
  /** Record identity is navigation context; the detail API must authorize it again. */
  readonly onOpenRecord?: (recordId: string) => void;
}

export function ReservationsPresentation({
  data,
  loading = false,
  error = false,
  styled = false,
  onOpenRecord,
}: ReservationsPresentationProps) {
  if (error) return <p role="alert">The Reservations presentation could not be loaded.</p>;
  if (loading || !data) return <p role="status">Loading Reservations presentation...</p>;

  return (
    <main className={styled ? "bwhq-api-example" : undefined}>
      <header>
        <p>Reservations</p>
        <h1>Reservation operations</h1>
        <p>Upcoming services, named consultant availability, booking state, payment readiness, capacity, and follow-up work.</p>
      </header>

      <MetricStrip data={data.metrics} styled={styled} />
      <div className={styled ? "bwhq-api-example__split" : undefined}>
        <ProgressList data={data.byStatus} styled={styled} />
        <EntityList data={data.recent} styled={styled} onOpenRecord={onOpenRecord} />
      </div>
      <SeriesChart data={data.trend} styled={styled} />
      <PresentationGrid data={data.table} styled={styled} onOpenRecord={onOpenRecord} />
      <Timeline data={data.timeline} styled={styled} onOpenRecord={onOpenRecord} />
    </main>
  );
}

Copy the live, authenticated module page

This document has no placeholder key and needs no invented endpoint. Save it to a page and add that page to a User Type menu. The registered native component calls /api/modules/reservations, uses the current tenant session, and preserves the module's real list, detail, create/update, pagination, empty, loading, and error behavior. Dynamic Records discovers the organization's real tenant-owned modules when no module key is configured.

Copy the executable live page
{
  "blocks": [
    {
      "_id": "reservations-live",
      "_type": "module.reservations",
      "props": {},
      "children": []
    }
  ]
}

The server derives SaaS app, organization, user, DataRoles, locations, module-specific membership/privacy, and any AI-read gate from verified identity. A returned identifier can select a detail target, but the detail or write endpoint authorizes it again. Page layout, status, tone, totals, action names, and identifiers never grant authority.

Tip

Start with one product and one assigned team. Decide whether capacity counts people or simultaneous services, set realistic buffers and hold time, configure payment separately, and test duplicate checkout, payment failure, Requires Action, cancellation, no-show, and both follow-ups.

Important

A renderer being bundled in the tenant application does not make its data visible in every app. The server returns only components and records authorized for the current app and signed-in user; unavailable or unauthorized blocks fail closed.

Capability review: 2026-09-14. For exact current technical availability, use the generated API Map and first-class module inventory.