Member notifications
Member inbox, per-category channel preferences, manual staff messages, reminders, and session cancellation.
Members open My account → Notifications for their inbox and Preferences for channel settings. The inbox shows read and unread entries, links to the relevant account page, and supports marking one or all entries read, or one entry unread. All / Unread filters the complete inbox before pagination; changing the filter returns to page 1. Page navigation returns focus and scroll to the inbox heading. Pages contain 20 items, newest first (creation time, then ID descending).
query Notifications($clubId: ID!, $page: Int = 1, $unreadOnly: Boolean = false) {
myNotifications(clubId: $clubId, page: $page, unreadOnly: $unreadOnly) {
items {
id
category
title
detail
startsAt
timeZone
body
href
createdAt
readAt
}
unreadCount
hasMore
}
myNotificationPreferences(clubId: $clubId) {
category
inApp
email
}
}
mutation Preferences($clubId: ID!, $input: NotificationPreferenceInput!) {
setMyNotificationPreference(clubId: $clubId, input: $input) {
category
inApp
email
}
}
mutation Read($clubId: ID!, $id: ID) {
markMyNotificationsRead(clubId: $clubId, id: $id)
}
mutation Unread($clubId: ID!, $id: ID!) {
markMyNotificationUnread(clubId: $clubId, id: $id)
}All operations require member authentication in the requested club. The recipient comes from the
session; the API accepts no recipient ID. Staff cannot use these member operations. IDs must be
UUIDs; page defaults to 1 and accepts 1–10,000. unreadOnly defaults to false (including explicit null); true returns only entries with no readAt. Filtering happens before the 20-item page limit and offset. Alternative sorting is not exposed. readAt is nullable; timestamps are ISO 8601 UTC strings. detail contains a nullable snapshot of the member, reservation or plan context. A reservation's start is not written into detail: startsAt (ISO 8601 UTC, nullable) carries it with the club's IANA timeZone, so the member app formats it in the interface language ("Le Rooftop · 20 oct. 2026, 19:00"); both are null for other notifications. href is a relative
consumer route. unreadCount covers the complete visible inbox, not just the current page.
Omitting id marks all currently unread entries read. Both read-state mutations return whether any row changed. markMyNotificationUnread requires a notification UUID and resets only its readAt to null; it returns false for an already-unread, missing, hidden, or other member’s notification. Neither mutation generates notifications or resends email.
markMyCustomerChatRead also marks read the member's unread chat notifications of that
conversation (href /account/chats/<chatId>), so a club message counts once in the inbox badge.
NotificationPreferenceInput requires category, inApp, and email. Categories are:
| Category | Events |
|---|---|
reservation | Reservation creation, status and detail changes |
proposal | A new or changed staff date proposal |
sponsorship | A sponsored person receives a membership (excluding add-ons) |
guest | The member's guest accepts an invitation |
reminder | A confirmed reservation starts within 24 hours |
waitlist | A place becomes available, or a waiting member books the session |
renewal | A Stripe subscription's paid period ends within 7 days, or its price changes after one month's notice |
session | A booked or waitlisted session occurrence is canceled |
message | A professional sends a reservation chat message |
chat | A professional starts or answers a customer chat |
club | A club administrator sends a manual message from the back office |
credits | A credit debit drops the balance below the club's low balance alert |
Both channels default to enabled. Saving a category updates only that category. In-app settings
apply to future notifications; existing inbox history remains. Email preferences are checked
again before a queued email is claimed. Re-enabling a category does not backfill suppressed events.
The existing reservation confirmation mail journal respects the reservation.email preference;
the notification worker does not send another confirmation email. Notification emails write startsAt like the reservation confirmation email ("Tuesday 27 October 2026 at 10:00", followed by "Times shown in Europe/Paris."), so a member promoted from the waitlist receives both emails with the same date format.
Manual staff messages
Club administrators open a member's profile in the back office and choose Send notification.
The dialog offers three categories: Received package and Lost object prefill an editable
message, and Other requires a custom one. The member receives the message in the club
category through the usual in-app and email channels.
mutation Notify($input: SendPersonNotificationInput!) {
sendPersonNotification(input: $input) {
id
category
title
body
href
createdAt
}
}SendPersonNotificationInput requires personId, clubId, and kind
(received_package, lost_object, or other); body is optional. The caller must be an
authenticated administrator of clubId, and the person must belong to that club. Both IDs
must be UUIDs. The mutation returns the created MemberNotification, or null when the person
is not found in the club.
| Kind | Title | Default body |
|---|---|---|
received_package | Package received | A package addressed to you has arrived at the club. Please collect it at the reception. |
lost_object | Lost object found | An item that may belong to you was found at the club. Please contact the reception to recover it. |
other | Message from your club | None; body is required |
body is trimmed and replaces the default text when present. It is limited to 2,000 characters and
must not contain null characters; an empty or missing body with kind: other returns
BAD_USER_INPUT. Titles and default bodies are stored in English and translated by the member app;
the back office sends body: null when the prefilled text is left unchanged so that translation
still applies. href is always /account/notifications. Each send creates a new entry: there is no
deduplication of repeated manual messages. The club in-app and email preferences apply, and the
email is delivered by the notification worker like any other category.
Scheduling and delivery
Notification records commit in the business transaction independently of club webhook settings.
The existing outbound webhook transport remains available and retains its own selection settings.
The API's scoped worker scans each minute, at most 100 due records per event category, and processes
up to 100 notification emails. Reminder windows are currently fixed at 24 hours and 7 days.
Reservations booked inside the reminder window are eligible on the next scan. Renewal reminders
use paidPeriodEnd, not an inferred anniversary; manual memberships and add-ons are excluded.
An approaching period end is not proof of a successful future charge.
Unique event keys prevent repeated reminder creation, including when channels are disabled. Rescheduling a reservation or advancing a paid period creates a new reminder key. Before email send, canceled/rescheduled reservation reminders and outdated membership reminders are skipped. Expired reminder emails are also skipped. Proposal emails are sent only while the notified proposal version is still current, awaiting acceptance, and its start time is in the future. Replaced, accepted, canceled, or expired proposals are skipped, including on retry. Inbox entries remain historical records.
When a place opens, the waitlist worker books eligible waiting members through the normal
reservation checks. Successful booking removes the waitlist entry and emits waitlist.joined;
the notification links to the resulting reservation and asks the member to check its status and
any payment due. A member booking directly while on the waitlist receives the same notification.
Notification emails are subject <club name> · <title> and greet the member by first name, followed by
the body, the context and a link to the page in the member's club account. Reservation confirmation,
security deposit and reservation invitation emails use the same <club name> · … subject. A
reservation's context names the session title when the reservation belongs to a titled session,
otherwise the category.
"Reservation update" notifications describe the new status in the member app's words: pending
is "Your request was sent to the club.", booked is "Your reservation is booked, payment is due.",
and confirmed and completed say so. A cancellation adds what happened to the price, read from
the reservation when the notification is created: "The price was not refunded." when it was kept
past the cancellation notice, "Your payment is being refunded." after a card refund, or "The price
returned to your credit balance." after a credit refund; otherwise "Your reservation is canceled.". Email requires the configured mail provider and CONSUMER_ORIGIN. The worker commits a sending
claim before contacting the provider. Definite failures retry after one hour; uncertain outcomes
and abandoned sending claims become unknown and are not automatically resent. Provider
acceptance is not inbox-delivery confirmation. These are in-app and email channels; browser/mobile
push and SMS are not configured.
Session occurrence cancellation
mutation CancelSession($clubId: ID!, $categoryId: ID!, $occurrenceId: String!) {
cancelReservationSession(clubId: $clubId, categoryId: $categoryId, occurrenceId: $occurrenceId)
}Club admins and staff assigned to the reservation category can cancel a future occurrence. IDs follow
reservationAvailability: a custom session ID, or weekly definition ID plus :YYYY-MM-DD in club
time. A malformed occurrence ID (not one of those two shapes) is rejected with BAD_USER_INPUT.
Invalid scope or a past occurrence returns false; an already-canceled occurrence returns true.
The back-office session roster exposes Cancel session with a confirmation step.
Cancellation closes that occurrence to bookings/waitlisting, cancels pending/booked/confirmed reservations and notifies their owners, then removes and notifies its waitlist. Other occurrences of a recurring definition remain available. Reservation lifecycle webhooks still fire, but members receive the session-category notification rather than a second reservation-category cancellation. Paid reservations are refunded by a scoped worker using the existing idempotent Stripe refund operation; failures remain discoverable and retry each minute. Session cancellation is effective while refunds are pending. Deleting a session definition is rejected while active reservations, waitlist entries, or unrefunded payments remain; cancel the affected occurrences first.
Undo marking all notifications read
mutation ReadAll($clubId: ID!) {
markAllMyNotificationsRead(clubId: $clubId) {
ids
readAt
}
}
mutation UndoReadAll($clubId: ID!, $receipt: NotificationReadReceiptInput!) {
undoMyNotificationsRead(clubId: $clubId, receipt: $receipt)
}markAllMyNotificationsRead atomically marks all currently unread in-app notifications
for the authenticated member and club, across all pages. It returns the exact changed
UUIDs and their UTC ISO timestamp (readAt); ids is empty when nothing changed.
Pass that receipt to undoMyNotificationsRead to restore those notifications to unread.
Only records still carrying the receipt's timestamp are restored, preserving later
read-state changes. The mutation returns the restored UUIDs and is safe to repeat.
IDs must be valid UUIDs and readAt must be a UTC ISO timestamp with milliseconds;
otherwise the mutation fails with BAD_USER_INPUT.
Both operations require member authentication for the selected club and leave
previously read entries, other members, notification delivery and email unchanged.