Shared

Timestamp of the newest visible message

get/api/messages/latest

One created_at, nothing else: the newest book_booking_messages row this login can see (guest message or staff reply alike). /inbox (MessagesClient.tsx) polls this every 45 seconds while the tab is visible and compares it with the value the page rendered from, calling router.refresh() only when they differ. Replaces a blind full-page refresh on the same interval. Same RLS-scoped client as the inbox's own read, so the two agree exactly when nothing is new.

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

curl -X GET "https://example.com/api/messages/latest"
{  "latest": "2019-08-24T14:15:22Z"}

Mark a thread read PATCH

Marks this booking's unread GUEST messages as read. Separate from POST so opening a conversation does not require writing one. The booking is looked up before the update, and an id that is not this org's is a 404. The number of messages actually flipped is deliberately NOT reflected in the status: a thread of your own with nothing currently unread matches zero rows and is still a 200, because marking a read thread read is idempotent, not an error.

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.