Platform Spec: Notifications
Platform Spec: Notifications
Section titled “Platform Spec: Notifications”| Field | Value |
|---|---|
| Layer | _platform — global, cross-cutting. Features emit events; this service delivers. |
| Status | Stub — created from demand across Weekly Summary (§4.4/4.5) and Weekly Goals (§6/OQ-04). Awaiting sign-off + the channel/provider decisions. |
| Owner | TBD (Duncan + external team) |
| Contract | /contracts/_platform/notifications.yaml |
| Consumed by | Weekly Summary (FR-18,19,20,23,24,25), Weekly Goals (US-007/FR-21), future features |
| Governs | in-app + email delivery; one place, no per-feature email code |
Why this exists. Two shipped specs need email + in-app delivery (manager-on-submit, employee-on-response, end-of-week reminders, Monday digests, non-setter reports). Building that per-feature means duplicated send logic, inconsistent templates, and no single place for user preferences. This is platform: features publish a notification; the service decides channels, applies the template, and delivers.
1. Model
Section titled “1. Model”- Event-driven. A feature publishes a notification (type + recipients + payload). It does not know or care about channels — the service resolves them.
- Two notification kinds:
- Transactional — fired by a domain event (summary submitted → manager). Delivered near-immediately.
- Scheduled digest — generated on a schedule (Monday morning emails). The feature
exposes the data (e.g. Weekly Summary’s
weekly-digest); a scheduled job composes and dispatches it. Digest timing shares the week definition / timezone (see Weekly Summary G1).
- Channels:
in_appandemail. Each notification type has a default channel set. v1: all notification types are mandatory — no user opt-out (decided 2026-06-10). The preference model below is designed but deferred post-v1. - Email provider: Mailgun (decided 2026-06-10, pending final confirmation). The send adapter targets Mailgun; DKIM/SPF setup required for deliverability.
- Digest timezone is per-tenant (
admin/tenant-settings), defaultEurope/London(GMT/BST). The Monday digest fires on the tenant’s configured timezone + week-start. - In-app inbox. Every recipient has a feed with unread count and mark-read.
2. Notification types (seed registry)
Section titled “2. Notification types (seed registry)”| Type | Kind | Default channels | Recipient |
|---|---|---|---|
weekly_summary.submitted | transactional | in_app, email | line manager |
weekly_summary.response | transactional | in_app, email | summary owner |
weekly_summary.reminder | scheduled | employee (end-of-week, consolidated w/ goals) | |
weekly_summary.monday_digest | scheduled | managers / senior managers / leadership | |
weekly_goals.non_setter | scheduled | in_app, email | managers (scoped) |
Registry grows per feature; each new type is added here + given a template.
3. Templating
Section titled “3. Templating”Each type has a template (subject + body) for email and a render shape for in-app. v1 templates live in-repo alongside this spec’s implementation. Payload fields are typed per type so templates can’t reference missing data.
4. Non-prod email (dev / staging safety)
Section titled “4. Non-prod email (dev / staging safety)”Email is a pluggable send adapter behind one interface — provider is per-environment config, never code. Build the adapter + the recipient guard from day one.
| Env | Adapter | Why |
|---|---|---|
| Local dev | Mailpit (self-hosted SMTP catch-all, Docker) | captures all mail, sends nothing, free, in-house |
| Staging / shared | Mailtrap (or Mailpit) | team-visible captured inbox; API lets tests assert “email sent” |
| Production | Mailgun | the real send |
Recipient guard (mandatory, environment-enforced) — two independent layers so no single mistake reaches a real inbox:
- Non-prod = redirect/allowlist mode. Even with a real adapter misconfigured, the service rewrites every recipient to a test inbox, or refuses any address not on an allowlist. A bad staging config cannot email real employees.
- Prod credentials live only in prod. Non-prod environments do not hold the Mailgun production key.
GDPR (PR-01/PR-03): Mailtrap is a third-party sub-processor. Weekly Summary emails carry personal data (names, wellbeing scores, manager feedback). Therefore: prefer self-hosted Mailpit for any environment holding real employee content; reserve Mailtrap for synthetic/anonymised content. Seed non-prod with synthetic employees for email testing; use real employees only where content stays in-house (Mailpit). Any real-content use of Mailtrap must be in the data-processing register.
Testing hook: Mailpit/Mailtrap both expose APIs — contract/E2E tests assert a notification was dispatched by reading the captured inbox, tagged by FR ID.
§A. Acceptance Criteria (stub)
Section titled “§A. Acceptance Criteria (stub)”| Ref | Criterion |
|---|---|
| NT-01 | Publishing a notification fans out to exactly the resolved channels for its type, honouring user preferences (except mandatory types). |
| NT-02 | GET /notifications returns the caller’s in-app feed, newest first, with an unread flag; tenant-scoped. |
| NT-03 | GET /notifications/unread-count returns the caller’s unread total. |
| NT-04 | Marking a notification read updates it; read-all clears the caller’s unread set. |
| NT-05 | A failed email delivery is recorded and retried per policy; an in-app notification is unaffected by email failure. |
| NT-06 | Publishing is tenant-scoped; a notification can only target recipients within the publisher’s tenant. |
| NT-07 | In any non-prod environment, dispatch never delivers to a real external address — recipients are redirected to a test inbox or rejected unless allowlisted. |
§B. Data Model Stub
Section titled “§B. Data Model Stub”| Entity | Fields (indicative) | Notes |
|---|---|---|
| Notification | id, tenantId, recipientUserId, type, payload(json), channels[], readAt?, createdAt | The in-app record; also the dispatch unit. |
| NotificationPreference | userId, type, channels[] | Per-user per-type channel opt-in/out. Subject to mandatory types (§C). |
| DeliveryLog | notificationId, channel, status(sent|failed|retrying), provider, attempts, lastError? | Email observability + retry. |
Tenant-scoped throughout (PR-04). Recipients resolved via _platform/identity-and-access
(e.g. “the owner’s line manager”, scoped digest recipients).
§C. Clarifications Needed
Section titled “§C. Clarifications Needed”Resolved 2026-06-10:
Email provider→ Mailgun (pending final confirmation). DKIM/SPF setup required.User preferences vs mandatory→ v1 all types mandatory, no opt-out. Preferences deferred post-v1.Digest timezone→ per-tenant setting (admin/tenant-settings), defaultEurope/London. Resolves the shared week definition home (Weekly Summary G1 / Weekly Goals G2).
Non-blocking:
- Comms audit (Weekly Summary Q4) — inventory existing emails before consolidating the reminders, so nothing valued is silently dropped.
- Template ownership — in-repo (recommended) vs an editable CMS for non-devs.
- Read receipts / digests of in-app — out of scope v1 unless requested.
Out of Scope (v1)
Section titled “Out of Scope (v1)”SMS/push channels; per-user quiet hours; notification batching beyond the defined digests; an editable template CMS.