Skip to content

PRD 11 — Notifications

Status: draft · Priority tier: P1 · Size: L · Depends on: auth/user (existing), PRD 01 (business settings, sender, locations, staff/roles), PRD 02–05 (bookings), PRD 06 + 08 (plans, sales)

1. Summary

Transactional email for customers (35 events, per-event on/off, editable text) and for staff (event toggles plus per-person recipients), a sender identity with domain verification, one email theme for branding, and a sent history with delivery and open status. Extends the existing internal/mail (SMTP) and internal/template (embedded OTP template); OTP login mail stays as-is and is not tenant-editable.

2. Problem & goals

Studios must tell customers about bookings, payments and memberships without manual work, in the studio's own voice and from the studio's own address. Goals:

  • Every listed domain event produces at most one email per recipient, within 60 s of the event (p95).
  • Owner can turn an event off, change subject/title/message and send a test in <= 5 clicks.
  • 100% of sends recorded in history with status (queued, sent, delivered, bounced, failed) and opened where the provider reports it.
  • Zero sends from an unverified sender domain (falls back to platform sender). Non-goals: marketing campaigns and automations (PRD marketing), drag-drop email builder, push/WhatsApp, full HTML editing of templates.

3. Users & roles

Owner/admin (configures everything), staff (receives staff notifications; may view history if permitted), front desk (view history), customer (receives email; no UI here except unsubscribe-exempt transactional notice).

4. Scope

In (v1)

  • must: 35 customer events with default toggles; global enable/disable per event.
  • must: event template editor: subject, title, custom message (max 500), placeholders, live preview, test email.
  • must: sender email + domain verification (DNS records, status), fallback platform sender.
  • must: staff notification events + recipients (staff x category).
  • must: email theme (logo, logo height, 3 colours) applied to all templates.
  • must: sent history (customer emails) with filters + CSV export.
  • should: delivery / bounce / open tracking via provider webhooks.
  • should: per-booking "events log" entry linking to the sent email (S209l).
  • should: quiet failure handling: retries with backoff, dead-letter status. Later (v2+)

  • could: SMS (templates per event, provider integration, per-customer consent), WhatsApp.

  • could: localisation of templates; per-location sender.
  • could: history tabs for Campaign / Automation once marketing exists. Out

  • Original's "Powered by" footer: whether we show our own is a pricing-tier decision (TBD).

  • Reverse-engineering the original's provider; we pick our own (SMTP now, API provider later).

5. User stories

  • US-11-01 As an owner, I want to switch individual customer emails on or off so that customers get only what I want.
  • US-11-02 As an owner, I want to edit subject, title and a custom message with placeholders so that emails sound like my studio.
  • US-11-03 As an owner, I want to send myself a test email so that I can check it before saving.
  • US-11-04 As an owner, I want mail to come from my own address and domain so that it is trusted and not spam-filed.
  • US-11-05 As an owner, I want one logo and colour theme for all emails so that branding is consistent.
  • US-11-06 As an owner, I want to choose which staff get which kinds of internal alerts so that the right person sees new bookings or failed payments.
  • US-11-07 As front desk, I want to look up whether a customer got an email and whether it was opened so that I can answer "I never got it".
  • US-11-08 As an owner, I want to export history as CSV for audits.

6. Functional requirements

ID requirement pri evidence
FR-11-01 Define a fixed catalogue of 35 customer events (below) in code, each with key, category, description, default status, default subject/title, allowed placeholders must S156b
FR-11-02 Per-business per-event override: status, subject, title, custom message; unset = default must S156b, S156c
FR-11-03 Customer list screen: categories rail (All, Classes, Appointments, Courses, Access, Payment, Pricing Plan), Show All/Active/Inactive, search by name, inline toggle, row opens editor must S156b
FR-11-04 Toggle persists immediately, with optimistic UI and rollback on error must S156b
FR-11-05 Editor: status, subject, title, custom message (optional, max 500, plain text with line breaks), Save/Cancel; Save disabled until a change must S156c
FR-11-06 Placeholders in subject/title/message: [class] [date] [time] [location] plus per-event additions (customer, appointment, course, plan, amount); unknown placeholder rejected on save; empty value renders as empty string must S156c
FR-11-07 Live preview with sample data, "preview as event" switcher, desktop/mobile width should S156c
FR-11-08 Send Test Email to the current user's address, with sample data, marked "[Test]" in subject, not written to customer history (written as type=test) must S156c
FR-11-09 Email theme: logo upload (JPG/PNG/GIF <= 500 KB), logo height (px, default 60, bounded 20-120), button text, button background, outlined-button colours (hex); live preview; applies to all templates must S156d
FR-11-10 Template body is composed server-side from an event layout + theme; custom message inserted escaped (html/template); no raw HTML input must S156c; internal/template
FR-11-11 Optional featured image and footer text in email theme could S85b
FR-11-12 Sender settings: sender email (default noreply@), Save; status badge (Unverified/Pending/Verified) must S156
FR-11-13 Domain verification: on sender-domain entry, show DNS records to add (SPF, DKIM, optionally DMARC), "Verify" button re-checks; status Authenticated/Pending/Failed must S156 (status only observed)
FR-11-14 Until verified, send from platform address with reply-to = sender email; once verified, From = sender email must derived
FR-11-15 Staff events catalogue (list below; 20 non-marketing events) with per-event toggle, grouped in accordions must S156e, S156g
FR-11-16 Recipients: list of staff with category checkboxes (select all), Add/Edit/Remove; a recipient gets an event only if its category is ticked and the event is on must S156e, S156h
FR-11-17 Staff emails use a plain internal layout (not customer theme); link back to the relevant record should derived
FR-11-18 Dispatch: domain services emit an event (key, business, recipient, payload); a dispatcher checks status, renders, enqueues; services never call mail directly must derived
FR-11-19 Async delivery via a DB-backed queue with retries (3, exponential), idempotency key (event key + entity id + recipient) must derived
FR-11-20 Reminder events (class/appointment/course, 24 h before) scheduled by a job; rescheduling/cancelling the booking supersedes the pending reminder must S156b
FR-11-21 Sent history: rows time, recipient email, event, subject, status, opened; search by email, filter by event and date range (default today), paginate, Export CSV (current filter) must S156f
FR-11-22 Delivery and open tracking via provider webhook (delivered, bounced, complained, opened); open pixel opt-in per business should S156f (not observed with data)
FR-11-23 "See Sent History" link from the editor opens history pre-filtered to that event should S156c
FR-11-24 Booking/transaction detail shows an events log with sent emails (type, time) should S209l
FR-11-25 Suppression: hard-bounced or complained addresses are skipped and logged as suppressed should derived
FR-11-26 Failed/blocked sends are visible in history with reason; no silent drops must derived
FR-11-27 Existing OTP email path moves onto the same sender/queue but keeps fixed platform template; not editable, not toggleable, not in history list should internal/auth/delivery.go
FR-11-28 SMS: channel field on template and history reserved in the schema; no UI in v1 could CSV: no SMS in Notification

Customer events (default ON unless noted):

  • Classes (10): booking confirmation; confirmation from waitlist; waitlist confirmation; waitlist expired; cancelled; late cancelled; no-show (OFF); waitlist cancelled; reminder 24 h; updated.
  • Appointments (7): confirmation; cancelled; late cancelled; no-show (OFF); reminder 24 h; updated; pre-requisite appointment completed (OFF).
  • Courses (4): confirmation; reminder 24 h; updated; cancelled.
  • Access (2): checked in; check-in cancelled.
  • Payment (9): payment confirmation; pay later; refunded; waived; voided; failed; approved; declined; pending approval.
  • Pricing plan (3): membership renewal paid; renewal failed; membership cancelled.

Staff events (default state not observed; assume ON, see open questions):

  • Onboarding: new sign-up. Forms: new contact form submission.
  • Bookings: class new booking; class auto-added from waitlist; class cancelled; class late cancelled; class auto-cancelled by system; class cancelled by staff; appointment new/updated/cancelled; course new/cancelled.
  • Payments: new payment; membership renewal payment; overdue payment settled; failed recurring charge; card updated.
  • Pricing plan: membership cancelled.
  • Marketing (quota events 75%/100%, campaign stopped): deferred to the marketing PRD; catalogue entries added there.

7. Screens

our screen modelled on states
Notifications hub (sender card + 3 cards) S156 loading, filled, sender unverified, domain pending/failed
Customer notifications list S156b loading, filled, filter-empty, error + retry
Event editor S156c pristine, dirty, saving, validation error (bad placeholder, >500), test sent
Email theme editor S156d default, dirty, upload error (size/type)
Staff notifications S156e/g collapsed, expanded, no recipients (empty), saving
Add/Edit recipient modal S156h empty, filled, duplicate staff error
Sender domain verification panel new (S156 badge only) none, pending with records, verified, failed
Sent history S156f empty ("no data for filter"), filled, loading, error

8. Flows

  • Edit one email: list -> row -> change subject -> Save (3 clicks) -> optional Send Test (1 click). Original: same.
  • Toggle event: 1 click on list.
  • Verify domain: hub -> enter sender email -> Save -> copy records -> (DNS outside product) -> Verify.
  • Add recipient: Add -> choose staff -> tick categories -> Add (4 clicks).
  • Edge: editing an event while it is off still saves; booking cancelled after reminder queued cancels the reminder; sender changed after verify resets to Unverified; test email fails if mail not configured (MAIL_URL empty discards sends, UI must say "mail not configured", not "sent").

9. Data model (guesses marked)

  • notification_setting(business_id, event_key, channel='email', enabled, subject, title, message, updated_by, updated_at) PK (business, event_key, channel). Catalogue itself is code, not a table.
  • email_theme(business_id, logo_asset_id, logo_height, btn_text, btn_bg, btn_outline, footer_text?) (one row).
  • sender_identity(business_id, email, domain, status, dns_records json, verified_at, provider_ref) (guess).
  • staff_notification_setting(business_id, event_key, enabled); staff_notification_recipient(id, business_id, staff_id, categories text[]).
  • email_outbox(id, business_id, event_key, entity_type, entity_id, recipient, subject, body_ref, status, attempts, next_attempt_at, provider_msg_id, error, idempotency_key unique, created_at).
  • email_event(outbox_id, type delivered|bounced|complained|opened, at, meta) (guess).
  • email_suppression(business_id, address, reason, at) (guess). Migrations follow db/migrations/NNNNN_<plural>.sql; domain package internal/notification (singular).

10. Business rules & edge cases

  • A disabled event sends nothing and writes no history row.
  • Recipient is the customer's login/contact email; customers without email are skipped with history status "no address".
  • Custom message is escaped; line breaks preserved; max 500.
  • Subject max 150 chars after placeholder expansion; truncation not silent: validation error on save for template, truncate at send with log in history.
  • Time in reminders and body uses the business/location time zone.
  • Duplicate events (retry/webhook replay) collapse via idempotency key.
  • Late-cancel vs cancel decided by cancellation policy (service PRD); notification only reacts.
  • Deleting a staff member removes recipients.
  • Never log message bodies or secrets (provider keys, DNS tokens).

11. Permissions

action owner admin staff front desk
Edit templates, theme, sender, toggles yes yes (role-matrix key "Settings > Notifications": edit) no no
View settings yes yes view-only if granted no
View sent history yes yes if granted if granted (view)
Receive staff notifications configurable per person
Routes live in the use: [auth] group; matrix granularity follows the staff-roles PRD (open question).

12. Acceptance criteria

  • Given default settings, when the list loads, then 35 events are shown and exactly 3 are inactive (class no-show, appointment no-show, pre-requisite appointment completed).
  • Given class confirmation is on, when a customer books, then one email is queued within 60 s and a history row exists.
  • Given the event is off, when the trigger occurs, then nothing is queued.
  • Given a subject with [bogus], when saving, then it is rejected with a message naming the placeholder.
  • Given a custom message of 501 chars, then Save is blocked.
  • Given an unverified sender domain, when an email is sent, then From is the platform address and Reply-To is the sender.
  • Given a webhook "bounced" for a message, then history shows Bounced and later sends to that address are suppressed.
  • Given a recipient with only "Payments" ticked, when a new class booking occurs, then they get no email; when a payment is received, they do.
  • Given a logo of 600 KB, then upload is rejected with the size rule.
  • Given a history filter by date range and event, when Export CSV, then the file contains only matching rows.
  • Given MAIL_URL empty, when Send Test Email is clicked, then the UI reports mail is not configured.

13. Dependencies & open questions

  • Depends on event sources from bookings, sales, memberships, check-in modules; on file/media storage for logo; on staff/roles.
  • Open: sending provider for production (SMTP only today; webhooks for delivery/open need an API provider); domain verification mechanism (provider API vs manual DNS check); staff-event default states (not observed); exact staff event list for appointments "waitlist" etc.; whether staff recipients can be non-staff addresses; open-tracking privacy/consent; SMS provider and pricing tier (TBD); email-sending quotas per plan (pricing tier: TBD); what history shows for the sent body (store rendered HTML vs re-render); retention period.
  • Recon gaps: delivery/open states were never seen with data; sender-verification workflow not observed beyond badges; staff email bodies not observed.

14. Evidence

See evidence/evidence.md.