Notifications — technical overview
Central multi-channel notification gateway: templates, per-patient channel preferences, clinic settings, delivery logs, and a single POST /send entry point that resolves the channel per recipient.
Channels
- email — via clinic SMTP settings (
/smtp-settings, testable). - whatsapp — opt-out prefs (
whatsapp_opt_in_atconsent trail). - sms (roadmap #231 PR1) —
Channel.SMS; resolves topatients.phone(E.164, single source of truth — no separatesms_phonecolumn in v1). Text-only: no attachments. Opt-out like email/whatsapp: explicitsms_enabled=Falseblocks (evenforce_send); a missing prefs row means reachable.sms_opt_in_atmirrors the whatsapp consent trail. Per-clinic cost guard:sms_daily_limit(default 100/UTC day,0= blocked, skips and inbound rows don't consume). An exhausted cap behaves like an unreachable channel: withfallback_enabledthe next connected channel takes the send, and only when none is viable is the row skipped withsms_rate_limited. Email/whatsapp are uncapped flat-rate transports. Delivery itself arrives with thesms_gatewaymodule (pluggable providers); until then SMS resolves but has no transport backend. - push (issue #63) — browser WebPush via
pywebpush(async send, 10 s timeout, 24 h TTL, 4 KB payload cap). One VAPID pair per deployment (DENTALPIN_VAPID_PRIVATE_KEY, env only). Patient flow: a single-use token is minted via API (POST /push/subscribe-tokens, 24 h expiry); the patient opens/p/push/<token>and the browser redeems it with its subscription (GET/POST /public/push/subscribe/<token>, no auth — the token is the auth). 410/404 endpoints prune on send; logs showpush:<n>recipients. System push templates (title + body, es/en) are seeded bynotif_0008; clinic rows per key/locale override them. Service worker:backend/app/modules/notifications/frontend/public/push-sw.js(served at/push-sw.js; uninstalling the module drops it).
API surface
DELETE /api/v1/notifications/templates/{template_id}GET /api/v1/notifications/logsGET /api/v1/notifications/preferences/patient/{patient_id}GET /api/v1/notifications/settingsGET /api/v1/notifications/smtp-settingsGET /api/v1/notifications/templatesGET /api/v1/notifications/templates/{template_id}POST /api/v1/notifications/sendPOST /api/v1/notifications/smtp-settings/testPOST /api/v1/notifications/templatesPOST /api/v1/notifications/testPOST /api/v1/notifications/push/subscriptionsPOST /api/v1/notifications/push/subscribe-tokensGET /api/v1/notifications/public/push/subscribe/{token}POST /api/v1/notifications/public/push/subscribe/{token}PUT /api/v1/notifications/preferences/patient/{patient_id}PUT /api/v1/notifications/settingsPUT /api/v1/notifications/smtp-settingsPUT /api/v1/notifications/templates/{template_id}
Frontend
backend/app/modules/notifications/frontend/pages/settings/notifications.vue→/settings/notifications
Permissions
templates.read, templates.write, preferences.read, preferences.write, logs.read, send, settings.read, settings.write, push.read, push.write
See ./permissions.md for the full role mapping.
Events
- Emits: (none)
- Subscribes:
appointment.cancelled,appointment.scheduled,budget.accepted,budget.sent,invoice.sent,patient.created
See ./events.md for the per-event detail (when the module participates in the event bus).
See also
- Module CLAUDE notes:
backend/app/modules/notifications/CLAUDE.md - Documentation portal contract