Notifications

A notification is one thing one person was told, in one tenant: a title, the rest of the message, and a path within the application it points at, sent by mail as well when the caller asks. The module promises that telling somebody is one call — the row and the request for mail commit with whatever caused them — that nobody’s list is anybody else’s, and that no address, body or link ever travels in an event. The module is modules/notification; this page states nothing that tree does not.

The map

Open the map full screen · source features/notification in the record

What it promises

Entity

Notification in contracts/notification.go: a recipient (a user id with no foreign key behind it), a title of one line, a body, a link that is a path within the application and never an absolute URL, and readAt, empty until the recipient has seen it. A caller hands Notify a Notice — the same four and an Email flag, a request rather than a promise. Nothing writes recipientId but Notify, and nothing writes readAt but MarkRead.

Events

notification.created from Notify; notification.email_requested from Notify when the notice asks for mail and the recipient has an address, carrying two identifiers and a time and nothing else; notification.read from MarkRead, once (contracts/events.go).

Permissions

none. Both routes are httpx.SignedIn(): every caller’s list is their own, and a permission every signed-in person must hold decides nothing.

Service

contracts.Service: Notify, MarkRead, ListFor; the fake and the conformance suite live in contracts/notificationtest/.

Routes and screens

Method Path Does Authorization Publishes

GET

/api/v1/notification/notifications

List my notifications

signed in

POST

/api/v1/notification/notifications/{id}/read

Mark one of my notifications read

signed in

notification.read

  • GET /api/v1/notification/notifications — the caller’s own notifications, newest first; there is no route that lists anybody else’s.

  • POST /api/v1/notification/notifications/{id}/read — marks one of the caller’s own read and publishes notification.read; somebody else’s is not found rather than refused, and marking it read again changes nothing and publishes nothing.

  • No rest.Spec: a Spec’s list route is the whole tenant and these rows are addressed to a person, so the two routes are written by hand in internal/handler.go and scoped by the principal rather than by a permission.

  • Screens: none — with no rest.Spec there is no resource in the register and nothing generated, and no navigation entry, because a nav entry names the permission that decides who sees the link and there is none (module.go). Hand-written pages: none.

Authorization

Authorization Who passes Routes

none: any signed-in member

a session of this tenant

notification-list, notification-read

Events, jobs and subscriptions

Event Published by Handled by

notification.created

a job, hook or command in the module

audit (every event)

notification.email_requested

a job, hook or command in the module

notification, audit (every event)

notification.read

notification-read

audit (every event)

  • Publishes: the three events above; the read route declares notification.read so the boot gate can check it.

  • Jobs: none, written out as a decision: a notification is caused by something happening, which is an event and not the clock (ADR 0004).

  • Subscriptions: notification.email_requestedinternal.SendMail in the worker (internal/mail.go): it reads the row back inside the event’s own tenant transaction, resolves the address through Recipients and the tenant’s host through Hosts, renders notice.tmpl as plain text with the link made absolute on that host, and hands the message to the Mailer. A notice deleted in between is a skip and a log line; a mailer that fails fails the subscription, so the outbox retries it and dead-letters it.

What it needs

Deps field

Interface

Supplied by

Recipients

contracts.RecipientLookup

declared by this module; the recipients adapter over the user module’s Get in apps/platformkit/modules.go. Nil writes every row and sends no mail.

Hosts

contracts.HostLookup

declared by this module; the tenantHosts adapter over the tenant module’s Hosts in the same file, taking the first host.

Mailer

contracts.Mailer

required, or Module panics: notification.SMTP over internal/smtp.go when mail.host is set, notification.NewMailbox() (mailbox.go) when it is not, chosen by mailer(cfg) in the same file.

Secure

bool

!config.Local(cfg.Server.PublicHost), the rule the session cookie’s Secure flag follows: a mailed link is https unless the host is local.

Configuration: none read by the module itself; main builds notification.Mail from mail.host, mail.port, mail.username, mail.password (PLATFORMKIT_MAIL_PASSWORD) and mail.from, and run warns at boot when mail.host is empty.

Who uses it

  • auth — contracts.Notifier, its own name for Notify, to tell somebody in the application that a set-password link was sent, pointing at /auth/reset and never carrying the token; and the same Mailer and HostLookup, to hand the link itself straight to the mail server so the secret is in the mail and in no row, outbox payload or audit event (auth/contracts/auth.go, auth/internal/password.go). An invitation reaches this module that way, through auth’s user.invited subscription, not from the tenant module.

  • A private client module — contracts.Service, which its registry entry requires, to tell a customer about a milestone from one of its subscriptions, with mail; its test takes notificationtest.NewFake().

  • The admin and the native shell do not reach it: it registers no httpx.Resource, so there is nothing for the screens or the catalogue to draw.

Verification

  • go test ./modules/notification/…​ — the fake and the real service pass one conformance suite (conformance.go); the internal tests prove one tenant’s notifications are not another’s, that the worker reads the row back and mails the tenant’s own absolute link, that the outbox carries no address, title, body or link (internal/service_test.go), that a hung mail server is a timeout and not a wait, and that a mailer that fails fails the subscription (internal/smtp_test.go).

  • No browser journey; apps/platformkit/app_test.go raises a notice through the service main holds, lists it and marks it read through the two routes, finds notification.read in the audit trail and the message in the mailbox.

  • Not proven: delivery to a real mail server, including the STARTTLS and authentication branches of SMTP.Send; the subscription against a live NATS fleet.