Shared

Mark the notification bell seen

patch/api/notifications

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).

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

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X PATCH "https://example.com/api/notifications"
{  "ok": true,  "seenAt": "2019-08-24T14:15:22Z"}

What the notification bell shows GET

The next few bookings and the newest unread guest messages, in one read. Polled by `NotificationDataProvider.tsx` every 60 seconds: there is no Supabase Realtime wiring in this product (no `supabase.channel()` calls, no publication on the booking tables), and standing that up for one bell would be a great deal of infrastructure for what is, per tenant, a handful of rows a minute. **Shared, and genuinely so** rather than merely ungated. It calls `requireMember()` with no vertical and then branches on `business_type` itself; upcoming appointments for one engine, upcoming reservations for the other; because one bell serves either product. Naming a vertical here would be wrong, not missing. Both lists are capped at 5 and are a display feed, not a paged resource: there is no cursor, and asking for more is what the Bookings and Inbox screens are for. **An account with no company answers 200 with two empty arrays**, not a 403; the bell renders on every dashboard page and must not be the thing that breaks a half-provisioned session. `seenAt` (migration 0208) is the caller's own "last opened the bell" cursor: bookings/messages/overrides created at or before it render as seen (greyed, not removed) in the dropdown, and only unseen ones count toward its badge. `null` means never opened. Set via PATCH on this same path.

Write the private staff note on a booking or enquiry PATCH

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}.