Notification
Portal, email, Telegram & webhook channels
The notification service reaches a user by fanning one message out across the channels you've enabled. Four channels deliver today: portal (an in-app record), email (through your email block), telegram (a bot posting to a configured chat) and webhook (a JSON POST to any HTTP endpoint — Slack, Discord, your own systems).
Messages are templated: any {{placeholder}} in the content is replaced from the event's context first, then from your global templateVariables. This keeps boilerplate (brand name, support URL) in one place.
Notifications are produced by verification-flow notification rules, which fan a flow's rules out to the right recipients on the channels you've enabled — so turning this on powers the whole approval-notification experience.
Channels#
Switch on the delivery surfaces you want. portal and email are simple booleans; the others nest their own credentials so the service can actually connect.
1{2 "notification": {3 "enabled": true,4 "channels": {5 "portal": true,6 "email": true,7 "telegram": { "enabled": true, "botToken": "TELEGRAM_BOT_TOKEN", "chatId": "TELEGRAM_CHAT_ID" },8 "webhook": { "enabled": true, "url": "SLACK_WEBHOOK_URL", "headers": { "x-api-key": "WEBHOOK_API_KEY" } }9 },10 "defaultChannel": "portal",11 "templateVariables": {12 "appName": "Acme",13 "supportUrl": "https://acme.com/support"14 },15 "endpoints": { "enabled": true, "basePath": "/notifications", "sendRoles": ["admin"] }16 }17}enabledbooleanOptionalMaster switch for the whole notification feature. When false or omitted, nothing fires: the verification notification handler returns early (guarded by notificationConfig?.enabled), the REST notification routes are never registered, and the client generator emits no NOTIFICATION_* actions — even though channels.portal defaults to true. DEFAULT_NOTIFICATION_CONFIG.enabled is false and that default is never merged at runtime, so an omitted enabled behaves as false. Set it to true (alongside verification.enabled and a database) to power the approval-notification experience.
falsechannelsobjectOptionalThe set of enabled delivery channels.
portalbooleanOptionalIn-app notifications surfaced via the NotificationCenter component.
trueemailbooleanOptionalEmail delivery — uses the top-level email block (Gmail or Azure) as transport.
falsetelegram{ enabled?, botToken?, chatId?, timeoutMs? }OptionalTelegram delivery via the Bot API. botToken and chatId take env-var names (or literals); every notification is posted to that one configured chat. timeoutMs defaults to 10000.
webhook{ enabled?, url?, headers?, timeoutMs? }OptionalPOST the notification payload as JSON to an external URL — bridge into Slack, Discord or your own systems. headers lets you add auth headers (values are env-var-name-or-literal). timeoutMs defaults to 10000.
Routing & templates#
Pick the default destination and define reusable substitution variables shared by every template.
defaultChannel'portal' | 'email' | 'telegram' | 'webhook'OptionalThe channel fallback for POST /send: when the request body omits `channels`, the service delivers over [defaultChannel ?? 'portal']. It is NOT consumed by the rule-driven fan-out — rules with no explicit channel rows always fall back to portal regardless of this value. Set per-rule channels to route those to louder surfaces.
'portal'templateVariablesRecord<string, string>OptionalGlobal key→value pairs interpolated into every template. Call-time context wins for overlapping keys, then these fill the rest. Ideal for {{appName}}, {{supportUrl}} and the like.
{}Endpoints#
The REST surface clients use to read and manage notifications.
endpoints{ enabled?: boolean; basePath?: string; sendRoles?: string[] }OptionalList, mark-as-read, manage and send in-app notifications. The NotificationCenter component talks to the read routes. Note the documented { enabled: true } default is not auto-applied at runtime (DEFAULT_NOTIFICATION_CONFIG is defined but never merged), and the two sides read an omitted endpoints block differently: the SERVER registers the routes whenever notification.enabled is true, a database is present, and endpoints.enabled !== false (no verification gate), but the client generator additionally requires verification.enabled AND notification.enabled AND notification.endpoints.enabled to all be explicitly truthy, so an omitted endpoints block generates NO frontend NOTIFICATION_* actions. To get the type-safe actions you must set notification.endpoints.enabled: true explicitly (with verification enabled).
{ enabled: true, basePath: "/notifications" }sendRolesstring[]OptionalRoles — in addition to godmin — permitted to call the privileged POST /send and POST /email endpoints. Matched case-insensitively against the x-user-roles header (both sides lowercased in canSend). Empty or omitted means godmin only. Lets an app's own admin panel send notifications/email without anyone holding godmin.
[]Sending (backend-to-backend)#
Two privileged send endpoints let one service push a notification or a transactional email without carrying an email SDK or the provider credentials itself. They are backend-only — the client generator emits NO actions for them.
POST /sendprivileged · portal/email/telegram/webhookOptionalSend a notification to one or more users. Body: { userId | userIds, title, body?, entityName?, entityId?, type?, source?, channels? }. Delivers over the requested channels (defaults to defaultChannel, else portal); type defaults to 'custom'. Recipients are sent with Promise.allSettled so one failure doesn't swallow the rest — the response reports { sent, failed } (failed is the list of user ids that errored). Returns 401 without x-user-id, 403 when the caller isn't godmin or a sendRoles member, and 400 when neither userId nor userIds is supplied.
POST /emailprivileged · transactional emailOptionalSend a transactional email to explicit recipients through the configured provider (Gmail or Azure). Body: { to (string | string[]), subject, html?, text?, replyTo? }. Same privileged gate as /send. Returns 503 when no email provider is configured or available, and 502 when the provider reports a delivery failure. On success returns { success: true, data: null }.
Under the hood — NotificationService#
Beyond the simple send API, the service is the delivery half of the verification engine: it turns a flow's notification rules into actual messages for the right people.
triggerNotificationsrule-driven fan-outOptionalWhen a verification flow fires on_flow_started / on_step_reached / on_approved / on_rejected / on_flow_completed, the service loads the matching notification rules (filtered by node, and by a startsAt/expiresAt window), interpolates their title/body templates and sends to every resolved recipient on the rule's enabled channels.
recipient resolutionuser · role · step_verifier · entity_creator · all_verifiersOptionalA rule's recipients can be concrete users, a role (expanded to all its users), the current step's verifier, the entity's creator (the instance's startedBy), or every verifier in the flow — all de-duplicated into a final user-id set before sending.
portal deliverynotifications tableOptionalA portal notification is a row in the notifications table (user_id, title, body, entity_name, entity_id, type, source, is_seen) — exactly what NOTIFICATION_LIST / UNSEEN_COUNT / MARK_SEEN read and what the NotificationBell renders.
email deliveryEmailServiceOptionalAn email notification looks up the recipient's address and hands a subject/html to the shared EmailService — so it inherits whichever provider (Gmail or Azure) the email block configures, and logs a warning and skips delivery (without failing the notify call) if email isn't available.
telegram & webhook deliveryHTTP sendersOptionaltelegram posts the interpolated title/body to the Bot API sendMessage endpoint for the configured chatId; webhook POSTs a JSON payload (user_id, title, body, entity_name, entity_id, type, source, timestamp) to your URL with any configured headers. Both honor timeoutMs (default 10s) and log failures without breaking the notify call.
From the frontend#
Turning endpoints on generates these ready-made, type-safe actions on the useApiActions hook — the portal's notification bell and center are built entirely from them. No fetch code required.
1const actions = useApiActions();2 3// unread badge4actions.NOTIFICATION_UNSEEN_COUNT.start({5 onAfterHandle: (r) => setCount(r.data.count),6});7 8// list + mark read9actions.NOTIFICATION_LIST.start({ payload: { limit: 20 } });10const markRead = (id: string) =>11 actions.NOTIFICATION_MARK_SEEN.start({ payload: { notification_id: id } });NOTIFICATION_LISTGET · /notificationsPaginated in-app notifications with optional limit, offset and type filter.
NOTIFICATION_UNSEEN_COUNTGET · /notifications/unseen-countThe unread count — drive the bell badge from this.
NOTIFICATION_MARK_SEENPOST · /notifications/:notification_id/seenMark a single notification as read.
NOTIFICATION_MARK_ALL_SEENPOST · /notifications/seen-allMark every notification as read at once.
Related sections