Files
NextChair/docs/notifications.md
T
Stefan Lazic f7fad3d1a0
CI / ci (push) Canceled after 0s
Appointment details layout
fixed button layout on appointment details page.

added an explainer for all the notifications
2026-09-12 10:01:55 +01:00

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's CustomerProfile.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 (recipientUserId set) — 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_REMINDER is 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's NotificationType enum, and RECIPIENT_KIND in src/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, and reminder-worker.ts (for the self-monitoring alert).
  • Channel resolution / preference enforcement: src/services/notifications/notification.service.ts's resolveChannels/isChannelEnabled.
  • The actual send: src/services/notifications/providers/ (Brevo/ console email, web-push/console push) plus reminder-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 in src/app/(customer)/settings/notifications/page.tsx and src/app/(staff)/staff/settings/notifications/page.tsx.