fixed button layout on appointment details page. added an explainer for all the notifications
8.9 KiB
Notifications reference
A living reference for every notification NextChair sends — what it is,
who gets it, on which channels, and whether the recipient can turn it
off. Unlike the other files in docs/ (dated phase summaries), this one
is meant to be kept current as notifications change; if you add, remove,
or retarget one, update this file in the same change.
Two separate mechanisms
Most of what's on this page goes through the queued notification
system: an event happens, a Notification + one NotificationDelivery
row per applicable channel is created immediately, but the actual
EMAIL/PUSH send only happens when the background processor runs (see
src/services/notifications/reminder-worker.ts, triggered by
POST /api/cron/process-notifications — see the root README's
"Notification delivery" section for how that's scheduled on a real
deployment). IN_APP deliveries have no send step; they're just a
database row the notification bell/centre reads directly.
One-time codes (below, in their own section) are different — they're sent synchronously, the moment they're requested, with no queue at all.
Either way, nothing actually reaches an inbox unless that business has
EMAIL_PROVIDER=brevo configured (Manager's Settings → Email, or a
business's own .env) — with the default EMAIL_PROVIDER=console,
every send just logs to the container's console instead.
Channels
- EMAIL — sent only if the recipient has a resolvable email address
(a registered user's
User.email, or a guest customer'sCustomerProfile.email). - PUSH — sent only if the recipient has at least one active (not revoked) browser push subscription.
- IN_APP — only meaningful for a recipient with a real account
(
recipientUserIdset) — a guest customer never gets one, since they have nothing to sign into.
Preferences — who can turn what off
See src/services/notifications/types.ts's PREFERENCE_CONTROLLED_TYPES
— this is the single source of truth; everything not listed there is
required (always sent on every channel it has a valid target for, no
toggle anywhere). Currently:
- Customer-facing: only
BOOKING_REMINDERis optional. Everything else (confirmations, changes, cancellations, loyalty) always notifies the customer — not finding out your appointment was cancelled is a real risk, not just noise. - Staff- and owner-facing: every type is optional. The owner of a
business is commonly also its only barber — one
StaffProfile, one account — so without this, a single new booking would unconditionally notify that one person twice (STAFF_NEW_APPOINTMENT+OWNER_NEW_BOOKING) on every channel, including in-app, with no way to quiet either copy.
Toggles live at Settings → Notifications — customers see one
"Appointment reminders" group; staff see their own six groups, plus five
more owner-only groups if they hold VIEW_REPORTING (i.e. are actually
an owner — see src/services/notifications/recipients.ts's
getOwnerUserIds, which checks the same permission). A preference with
no saved row yet defaults to on — turning something off is always an
explicit, deliberate action.
Customer-facing
| Type | Fires when | Default subject |
|---|---|---|
BOOKING_CONFIRMED |
A new booking is created and doesn't need approval | "Booking confirmed" |
BOOKING_PENDING_APPROVAL |
A new booking is created but the business requires approval first | "Booking request received" |
BOOKING_APPROVED |
An owner approves a pending booking | "Booking approved" |
BOOKING_RESCHEDULED |
The appointment's time changes (including an owner reassigning staff, which also triggers a reschedule) | "Booking rescheduled" |
BOOKING_CANCELLED |
The appointment is cancelled, by anyone | "Booking cancelled" (carries a "This was a late cancellation." note when applicable — not a separate type) |
BOOKING_BARBER_CHANGED |
An owner reassigns the appointment to a different staff member | "Your barber has changed" |
BOOKING_COMPLETED |
The appointment is marked completed | "Thanks for visiting" |
BOOKING_REMINDER (optional) |
24 hours and 2 hours before a confirmed appointment (each is its own send) | "Upcoming appointment reminder" |
LOYALTY_STAMP_ADDED |
A loyalty stamp is added to their card | "You've earned a loyalty stamp" |
LOYALTY_REWARD_EARNED |
Their stamp count reaches the reward threshold | "You've earned a reward" |
LOYALTY_REWARD_EXPIRING |
A daily sweep finds their reward is nearing its expiry date (fires once per calendar day it's still found, not once total) | "Your reward is expiring soon" |
LOYALTY_REWARD_REDEEMED |
Their reward is applied to an appointment | "Reward applied" |
Staff-facing
| Type | Fires when | Default subject |
|---|---|---|
STAFF_NEW_APPOINTMENT (optional) |
A new appointment is booked with them | "New appointment" |
STAFF_APPOINTMENT_RESCHEDULED (optional) |
One of their appointments is rescheduled | "Appointment rescheduled" |
STAFF_APPOINTMENT_CANCELLED (optional) |
One of their appointments is cancelled | "Appointment cancelled" |
STAFF_APPOINTMENT_REMINDER (optional) |
24 hours and 2 hours before their appointment | "Upcoming appointment reminder" |
STAFF_SCHEDULE_CHANGED (optional) |
An owner edits their working hours/breaks/time-off on their behalf — never fires from the self-service editors, since notifying someone about their own edit would just be noise | "Your schedule was updated" |
STAFF_INVITATION_RECEIVED (optional) |
They're added as a new team member | "You've been invited to join {business}" |
STAFF_APPOINTMENT_CHECKED_IN |
Defined in the schema, has a recipient assigned — but nothing in the codebase currently triggers it. Preference-controlled for forward-compatibility, but has no toggle in the UI since there's nothing to preview | — |
Owner-facing
Sent to every active staff member whose role holds VIEW_REPORTING —
that can be more than one person; there's no single "the owner" column
anywhere in the schema.
| Type | Fires when | Default subject |
|---|---|---|
OWNER_NEW_BOOKING (optional) |
A new booking is made anywhere in the business | "New booking" |
OWNER_BOOKING_CANCELLED (optional) |
A booking is cancelled anywhere in the business | "Booking cancelled" |
OWNER_NO_SHOW (optional) |
A customer is marked as a no-show (staff-facing equivalent doesn't exist — the barber who marked it already knows) | "No-show recorded" |
OWNER_STAFF_INVITATION_ACCEPTED (optional) |
An invited staff member verifies their email and joins | "Invitation accepted" |
OWNER_REPEATED_DELIVERY_FAILURE (optional) |
The background processor itself detects repeated delivery failures that day — a self-monitoring alert, not triggered by a domain event. At most once per business per day | "Some notifications failed to send" |
One-time codes (not part of the system above)
Sent immediately and synchronously — see src/lib/auth/otp.service.ts.
Not queued, not preference-controlled, no in-app record.
| Purpose | Sent to | When |
|---|---|---|
login |
The email being signed in with | Every sign-in attempt |
register |
The email registering | Customer self-registration |
staff-invite |
The invited email | Accepting a staff invitation (/invite/accept) |
install |
The administrator's email | The install wizard's administrator-verification step |
email-change |
The new email address | A signed-in user changing their account email |
Platform-level (Manager, not tied to any business)
- Settings → Email → Send Test Email — a manual, one-off admin action
in the Manager app, unrelated to any business's own notification queue.
See
manager/src/lib/test-email.ts.
Where things live, if you're changing any of this
- What exists / who it's for:
prisma/schema.prisma'sNotificationTypeenum, andRECIPIENT_KINDinsrc/services/notifications/types.ts. - Whether it's optional:
PREFERENCE_CONTROLLED_TYPES, same file. - When it actually fires:
src/services/scheduling/booking-notifications.ts,reminder-scheduling.ts,src/services/loyalty/loyalty-notifications.ts,src/services/staff/staff-notifications.ts,src/services/staff/staff-invitation.service.ts, andreminder-worker.ts(for the self-monitoring alert). - Channel resolution / preference enforcement:
src/services/notifications/notification.service.ts'sresolveChannels/isChannelEnabled. - The actual send:
src/services/notifications/providers/(Brevo/ console email, web-push/console push) plusreminder-worker.ts, which processes the queue. - Settings UI:
src/features/staff/components/notification-preferences-form.tsx(shared by customer/staff/owner), wired up per audience insrc/app/(customer)/settings/notifications/page.tsxandsrc/app/(staff)/staff/settings/notifications/page.tsx.