Timestamp of the newest visible message
/api/messages/latestOne 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 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.