Skip to main content

notify-sender

notify-sender is the triggers-v2 SDK runtime for notify delivery. It is built from cmd/notify-sender, registered as triggers.NewService("notify"), and shipped together with notify-api inside the shared notify-svc release.

Web-push runtime map

Actions

send_inapp

Per-recipient in-app delivery. The SDK handles ingestion, retry, DLQ, and manifest registration.

Input (SendInAppInput):

FieldTypeRequiredNotes
tenant_idstringyes
template_idintyeslooked up in Mongo templates_inapp
player_idintyesone recipient per invocation
language_idstringnofalls back to template or tenant default
placeholdersmap[string]stringnosubstitution values
vertical / category / subcategorystringnoforwarded into the websocket payload and Mongo record

Result (SendInAppResult): notification_id, delivered.

send_webpush

Per-recipient browser-push delivery. The handler renders a web-push template, resolves active subscriptions from Mongo subscriptions_webpush, upserts activity in Mongo notifications_webpush, and delivers to every active browser subscription for that player.

Input (SendWebPushInput):

FieldTypeRequiredNotes
tenant_idstringyes
player_idintyesone recipient per invocation
template_idintyeslooked up in Mongo templates_webpush
language_enabled_idstringyesrequired for localized content selection
placeholdersmap[string]stringnosubstitution values

Result (SendWebPushResult): notification_id, player_id, status, delivered, invalidated_subscriptions.

Supported notify-api web-push contract

notify-api now has two supported web-push surfaces:

  • Editor-token routes for admin workflows:
    • GET|POST|PUT|DELETE /api/v1/templates/web-push/*
    • POST /api/v1/webpush/send
    • GET /api/v1/webpush/activity
  • Push-platform routes for browser lifecycle traffic coming from a trusted backend:
    • GET /api/v1/webpush/client-config
    • POST /api/v1/service/webpush/subscriptions
    • POST /api/v1/service/webpush/subscriptions/deactivate
    • POST /api/v1/service/webpush/events/browser
    • POST /api/v1/service/webpush/events/semantic

The internal dashboard now uses client-config, subscriptions, subscriptions/deactivate, and browser-event forwarding for browser lifecycle work. Product and UI integrations should follow the same shape through their own same-origin backend routes. They should not use player_id: 0; that sentinel stays reserved for internal dashboard-only smoke usage.

In this workspace today, lumio_admin_dashboard is the only committed browser bridge and service-worker integration. online-services-front is documented separately as the player-facing target integration, but it is still a plan.

Runtime flow by stage

Auth and secrets

Web-push browser lifecycle traffic must use push-platform auth, not the editor token.

  • NOTIFY_API_PUSH_PLATFORM_CALLERS_JSON and NOTIFY_API_PUSH_PLATFORM_CALLERS_JSON_SECRET_NAME are the push-platform auth configuration. They let you scope each caller token to explicit allowed_routes, allowed_source_apps, and optional tenant_ids.
  • NOTIFY_WEBPUSH_VAPID_PUBLIC_KEY / NOTIFY_WEBPUSH_VAPID_PUBLIC_KEY_SECRET_NAME and NOTIFY_WEBPUSH_VAPID_PRIVATE_KEY / NOTIFY_WEBPUSH_VAPID_PRIVATE_KEY_SECRET_NAME are the VAPID pair. GET /api/v1/webpush/client-config is only ready when both are configured and WEB_PUSH_DRY_RUN=false.
  • NOTIFY_WEBPUSH_INTERACTION_TOKEN_SECRET and NOTIFY_WEBPUSH_INTERACTION_TOKEN_SECRET_NAME sign and validate browser interaction tokens used by POST /api/v1/service/webpush/events/browser.
  • NOTIFY_WEBPUSH_ALLOWED_TARGET_HOSTS optionally restricts rendered target_url values to known product hosts. Leave it empty only if scheme-only validation is acceptable for that environment.
  • INAPP_EDITOR_API_AUTH_TOKEN and INAPP_EDITOR_API_AUTH_TOKEN_SECRET_NAME protect GET /api/v1/tenants/, template CRUD, POST /api/v1/templates/in-app/send, POST /api/v1/webpush/send, and activity. The dashboard's NOTIFY_EDITOR_TOKEN must match this value.
  • Any *_SECRET_NAME path requires KEY_VAULT_URL on the process that resolves the secret (notify-api or notify-sender).
  • NOTIFY_WEBPUSH_ALLOW_AUTO_VAPID is local-dev only. When enabled without configured VAPID keys, notify auto-generates a temporary keypair at startup (lost on restart). Do not enable it in shared or production environments, and do not expect it to make GET /api/v1/webpush/client-config ready.

Example caller policy JSON:

{
"callers": [
{
"caller_id": "lumio-admin-dashboard",
"token": "replace-me",
"allowed_routes": [
"GET /api/v1/webpush/client-config",
"POST /api/v1/service/webpush/subscriptions",
"POST /api/v1/service/webpush/subscriptions/deactivate",
"POST /api/v1/service/webpush/events/browser",
"POST /api/v1/service/webpush/events/semantic"
],
"allowed_source_apps": ["lumio_admin_dashboard"]
}
]
}

Shared local compose currently loads a caller policy JSON that only allows lumio_admin_dashboard. Replace that JSON before another same-origin backend calls the push-platform routes locally.

Runtime storage and retention

  • subscriptions_webpush stores browser subscriptions keyed by endpoint and grouped by tenant and player.
  • notifications_webpush stores rendered payload metadata and delivery activity.
  • interaction_events_webpush stores browser and semantic interaction events.

The runtime keeps TTL indexes on both notifications_webpush and interaction_events_webpush. NOTIFY_SENDER_WEBPUSH_RETENTION_DAYS defaults to 30, and the current notify-api boot path also enforces a 30 day interaction-event TTL. Treat 30 days as the supported retention target unless those boot paths are changed together.

Service sources

SourceTransportDefault target
natsNATStriggers.v2.notify.inbound
kafkaKafkatriggers_v2_notify_inbound
httpHTTPbare HTTP derives /v2/notify/inbound; the SDK publishes that canonical path under TRIGGERS_HTTP_PUBLIC_BASE_URL or, when unset, TRIGGERS_HTTP_ADDR

The notify service declaration stays on the canonical SDK ingress targets, including Kafka triggers_v2_notify_inbound. If a released flow needs a non-canonical Kafka target, that override belongs in flow configuration only and the topic must already exist. A missing Kafka retarget fails closed, so ingestion stops instead of silently falling back to the old topic while the process stays up. Notify does not require a repo-owned KafkaTopic manifest or bootstrap sidecar just to declare or consume its canonical Kafka ingress.

Idempotency and recovery

At-least-once: the SDK acks only after the handler returns. Both actions key their Mongo upsert on the SDK idempotency key (sha256(FlowRunID + NodeID)), which is stable across redeliveries of the same flow run. In-app delivery upserts into notifications_inapp; web push upserts into notifications_webpush. Redelivering the same flow run reuses the existing record instead of creating a duplicate.

SDK Redis transport dedup is part of the declared runtime shape, but Redis is only resolved when the service actually enables that Redis-backed capability. Resolution precedence is: explicit TRIGGERS_REDIS_URL, then TRIGGERS_REDIS_URL_SECRET_NAME, then host/password composition where literal TRIGGERS_REDIS_HOST or TRIGGERS_REDIS_PASSWORD beat their corresponding *_SECRET_NAME variants and TRIGGERS_REDIS_DB supplies the DB suffix when the host input does not already include one. Any *_SECRET_NAME path requires KEY_VAULT_URL, and successful resolution backfills TRIGGERS_REDIS_URL for compatibility. Dedup still only collapses rapid redelivery storms; correctness does not depend on Redis. There is no business dedup across two distinct flow runs in the sender; the producer is responsible for firing once per (notification, player).

Notify-sender config now relies on the SDK-owned Redis inputs in sender.env: TRIGGERS_REDIS_HOST_SECRET_NAME, TRIGGERS_REDIS_PASSWORD_SECRET_NAME, and TRIGGERS_REDIS_DB=3.

Error classification and DLQ

send_inapp

OutcomeClassificationEffect
Render OK + websocket deliveredsuccess{notification_id, delivered:true} routed onward
Subscription deactivated/expiredErrSkipForwardack, no forward, no DLQ
Template missing / render / bind failureErrPermanentDLQ, no retry
Mongo write / websocket transient failureretryableSDK backoff retry, DLQ on exhaustion

send_webpush

OutcomeClassificationEffect
Render OK + at least one subscription deliveredsuccessresult_event forwarded with delivered=true
No active subscriptions for the playerErrSkipForwardack, no forward, no DLQ
Template missing / missing language_enabled_id / render failureErrPermanentDLQ, no retry
Subscription lookup / notification write / push transport transient failureretryableSDK backoff retry, DLQ on exhaustion
Only expired subscriptions remainsuccessstatus=subscription_expired, no retry, expired endpoints deactivated

Per-action retry: MaxAttempts=5, InitialBackoff=2s, BackoffFactor=3.0, MaxBackoff=1m. Failures land on triggers.v2.dlq.notify.<action>, which triggers-v2 mirrors into Mongo for the notify-api to surface.

Manifest and data sources

The manifest advertises send_inapp, send_webpush, the three service sources, supports_nodes = {condition, lookup, mapping} (action availability is derived from a non-empty actions[], not a literal action entry in supports_nodes), and four read-only data sources answered on triggers.v2.service.notify.data.<name>:

  • templates
  • tenants
  • push_templates
  • push_template_fields