Primitive spec

Speaker

The outbound communication primitive. Speaker is the single choke point for every notification, reminder, confirmation, receipt, and marketing message the platform sends. It picks the right channel, routes through the right vendor, and enforces Terminus consent before any dispatch — with no override path.

Status: Production drivers shipped — AWS SES (outbound + inbound forwarding), SendGrid, Twilio SMS — alongside the original mock retained for tests. Platform SendGrid (an ops-managed key, with click/open tracking off) is now the default email provider. Consent gate enforced on every dispatch path. The PUSH channel is now real: a Web Push driver (VAPID) plus a device-token registry back browser and mobile push, the operator console ships as an installable PWA, and the first live trigger pings operators when a proposal needs review. June 11 hardening: provider delivery webhooks are signature-verified, and a device-registration IDOR was closed.

What it owns

Speaker owns outbound communication and only outbound communication. Every notification path on the platform — booking confirmations from Seldon, receipts from Hober, statements from Mallow, payment-failed alerts from Payments, marketing campaigns — goes through Speaker. There is no other code path that sends a message.

The single choke point matters because consent is enforced in Speaker, not in the callers. Terminus owns consent state; Speaker calls assertConsent before every dispatch. A bug in Daneel or a careless workflow change cannot accidentally route around the consent gate — the gate is in the layer that holds the network connection.

Concepts

Message
An outbound communication, rendered from a template against a payload, targeted at a Terminus Person. Carries the resolved channel, vendor route, and a delivery-attempt history.
Template
A versioned Radiant asset that renders into the body for a given message class (reservation.confirmed.v1, receipt.email.v3). Templates carry per-channel variants (email body, SMS body, push body) and are localised at render-time.
Channel
The transport: email, SMS, push, in-app, webhook. Channel selection considers the recipient's verified contacts, preferences, the message's urgency class, and cost. A confirmation message on a Person with a verified phone and SMS preference may go SMS first and email as fallback.
Vendor
The provider that actually moves the bytes (SendGrid, Twilio, Resend, OneSignal, custom SMTP). Vendor selection considers tenant configuration, channel, cost, and provider health.
Consent gate
Before any dispatch, Speaker calls TerminusDirectory.assertConsent(personId, scope). Scope is derived from the message class (marketing.email, transactional.sms, etc.). A negative consent terminates dispatch and records the suppression. Transactional consent is implicit for fulfillment-critical messages; marketing requires explicit opt-in.
Delivery attempt
One record per vendor send, retry, bounce, or open. The attempt history is the durable audit trail of what was sent, when, through which vendor, and what came back.
Inbound
Speaker handles inbound too — SMS replies, email bounces, webhook callbacks — threading them back to the originating Message and Person. STOP replies update Terminus consent.
Push & device registry
The PUSH channel is real: a DeviceToken registry (Web / iOS / Android) holds each subscription, and a Web Push driver delivers via VAPID (RFC 8291 / 8292). Registering a device at POST /speaker/v1/devices is idempotent on the token and reactivates a pruned one. Push is consent-gated like every other channel — the browser-permission grant is the opt-in — and the operator PWA enrolls the subscription on install.

API surface

Endpoints are versioned under /speaker/v1/. Most dispatch is event-driven (other primitives emit a message-worthy event; a subscriber drops a Send into Speaker); the HTTP surface is for admin tooling, template management, and external triggers.

Speaker endpoints in the Foundation API reference OpenAPI 3.1 schema for Speaker with request/response shapes, parameters, and a try-it client.
MethodPathPurpose
POST/speaker/v1/messagesSend a message. Body specifies template, recipient, payload, urgency.
GET/speaker/v1/messages/{id}Fetch a Message with its delivery-attempt history.
GET/speaker/v1/messages?personId=&templateId=&from=List sent messages, filtered.
POST / GET/speaker/v1/vendorsManage per-tenant vendor configuration and credentials.
POST / DELETE/speaker/v1/devicesRegister or revoke a push device token (Web / iOS / Android).
GET/speaker/v1/preferences?personId=Resolved channel preferences for a Person (derived from Terminus state).
POST/speaker/v1/webhooks/{vendorType}Inbound webhook from a vendor (delivery, bounce, open, reply). Signature verified.
POST/speaker/v1/inbound/{channel}Inbound message ingress (SMS reply, email-to-action).

Example: send a booking confirmation

Seldon's outbox subscriber drops this when a Booking confirms:

POST /speaker/v1/messages
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: c8a5e2b1-…

{
  "templateAlias":   "reservation.confirmed.v1",
  "recipientPersonId": "per_01JAB7…",
  "urgency":         "transactional",
  "payload": {
    "bookingId": "bk_01JAZB…",
    "offerName": "60-minute haircut",
    "startsAt":  "2026-06-12T09:00:00-04:00",
    "partySize": 1
  }
}

Speaker resolves the template, calls assertConsent(per_01JAB7…, "transactional.email"), picks the channel (email, in this case), routes through the configured vendor, and records the delivery attempt:

→ 202 Accepted   Message  msg_01JAZH…   status=queued

GET /speaker/v1/messages/msg_01JAZH…
→ {
    "id":           "msg_01JAZH…",
    "status":       "delivered",
    "channel":      "email",
    "vendorRoute":  "sendgrid",
    "consentResolved": { "scope": "transactional.email", "version": 7 },
    "attempts": [
      { "vendor": "sendgrid", "at": "2026-06-10T14:31:22Z", "result": "accepted" },
      { "vendor": "sendgrid", "at": "2026-06-10T14:31:24Z", "result": "delivered" }
    ]
  }

How it fits with the rest

flowchart LR
  S[Seldon] --> Sp(Speaker)
  Ho[Hober] --> Sp
  Pa[Payments] --> Sp
  D[Daneel] --> Sp
  R[Radiant] -. templates .-> Sp
  Sp -- assertConsent --> Te[Terminus]
  Sp --> V[Email / SMS / Push / Webhook]
  Inb[Inbound replies] --> Sp
            

Almost everything calls Speaker. Seldon sends confirmations and reminders driven by emission-pipeline events. Hober sends receipts on Order close. Payments sends payment-failed and dispute-opened notifications. Mallow sends statements. Daneel workflows dispatch through Speaker rather than calling vendors directly. Every send re-enters the same consent gate; there is no privileged path. Radiant stores the templates that Speaker renders.