Shared

Write the private staff note on a booking or enquiry

patch/api/booking-note

Its own route rather than a field on any guest-facing handler, so there is no code path where a note can be written by the same request that renders something to a customer. Nothing guest-facing selects internal_note, and the guest-side loader names its columns explicitly so it cannot start doing so by accident.

An empty note clears the column rather than storing a blank string, so "has a note" stays a null check everywhere else.

This is the BOOKING-level note. The other level; the one that follows the person across every booking; is book_customers.notes, written through PATCH /api/clients/{id}.

Authorization

sessionCookie
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.

Every request re-validates it against the Auth server (getUser()), never by decoding the cookie locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified app_metadata.company_id claim and enforced by row-level security; it is never read from request input, on any route, ever.

role (admin / staff) is deliberately not in RLS. It gates specific actions in route code, the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's own check is the enforcement.

In: cookie

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X PATCH "https://example.com/api/booking-note" \  -H "Content-Type: application/json" \  -d '{    "kind": "reservation",    "bookingId": "9a471128-954e-4e64-bde9-e8147015df89"  }'
{  "ok": true}

Mark the notification bell seen PATCH

Fired the instant the bell dropdown opens (`NotificationDataProvider.tsx`'s `markSeen()`), not per item: sets one cursor, `book_org_members.notifications_seen_at`, to now for the caller's own row. Nothing is deleted or marked read by this alone: a message still needs its own `read_at` (PATCH /api/messages) to leave the Inbox nav badge, and that is deliberate, since the bell and the Inbox counter are independent by design (Notification System Requirements, 2026-09-07). No body, no id: always the caller's own `book_org_members` row via session user id, same self-only shape as PATCH /api/notification-prefs, and written through service-role rather than the RLS-scoped client for the same reason that route is: UPDATE on that table was revoked from `authenticated` after migration 0082 (docs/grants-and-rls.md).

Create or replace an intake form (admin) PUT

One form per target, where the target is either a single service or the whole org. `serviceId: null` means "every booking in this org", which is also what a table-reservations org always uses; it has no services. Two *partial* unique indexes in migration 0024 make that unambiguous, and this route upserts against them by reading first: PostgREST's `onConflict` emits a bare `ON CONFLICT (company_id, service_id)`, which Postgres cannot infer against an index carrying a WHERE clause. Written with the caller's own RLS-scoped client, NOT service-role. Unlike `book_notification_templates`, 0024 gives `book_intake_forms` a plain tenant policy, so RLS plus the JWT `company_id` claim already is the boundary. The composite foreign key means a `serviceId` belonging to another org is refused by the database rather than by route code, and surfaces as a 400. **There is no DELETE, deliberately.** `book_intake_responses` references the form `on delete cascade`, so dropping a form would take every answer any guest ever gave with it, and a submitted intake is a record. `active: false` is the retire switch and it is part of this body. Shared by both engines and therefore NOT vertical-guarded: a form asks a guest questions, which is plumbing like customers and notifications, not booking logic.