Skip to content

Notifications overview

Knock is the workflow engine for every user-facing notification in the platform. The shared @repo/notifications package owns the bilingual translations (es + en) and the branded HTML email layout — both apps/svelte-web (web triggers) and apps/worker (background-job triggers) call getTranslatedPayload(workflowId, locale, params) to get a ready-to-render { inAppBody, emailSubject, emailBody } payload. Knock dashboard templates render {{ inAppBody }} / {{ emailSubject }} / {{ emailBody }} as-is — no dashboard-side template logic.

flowchart LR
    Web["apps/svelte-web<br/>form action / tRPC"]
    Worker["apps/worker<br/>BullMQ job"]
    StreamWebhook["GetStream webhook<br/>(message.new)"]
    API["apps/api<br/>tRPC mutation"]

    Wrapper["triggerWorkflow()<br/>@repo/server-services"]
    WorkerWrapper["triggerWorkflow()<br/>apps/worker/src/knock.ts"]

    Knock["Knock<br/>(workflow engine)"]

    Feed["In-app feed<br/>(web + mobile)"]
    Email["Email<br/>(via Resend)"]
    Push["Push<br/>(FCM + APNs per app)"]

    Web --> Wrapper
    API --> Wrapper
    StreamWebhook --> Wrapper
    Worker --> WorkerWrapper
    Wrapper --> Knock
    WorkerWrapper --> Knock
    Knock --> Feed
    Knock --> Email
    Knock --> Push

All inter-component communication goes through the wrapped triggerWorkflow helper so observability + Sentry context lives in one place. Email channel is Resend, configured at the Knock dashboard level. Push covers FCM (Android, shared) + APNs per app — the agent app on KNOCK_APNS_CHANNEL_ID, the customer app on KNOCK_APNS_USER_CHANNEL_ID, routed by registerDevice’s app field. The customer APNs channel must be provisioned in the dashboard before iOS customer push delivers.

Workflow keyDomainTriggerRecipientsChannels
new-inquiryChatFirst property chat message creates InquiryAssigned agentin-app + email
new-chat-messageChatGetStream message.new webhookChannel members minus senderemail (digest)
team-invitationTeamsTeam invite createdInvited userin-app + email
tour-requestedToursTour createdAgentin-app + email
tour-rescheduledToursTour PATCH → RESCHEDULEDRecipient (customer or agent)in-app + email
tour-deniedToursTour PATCH → DENIEDTour requesterin-app + email
tour-confirmedToursTour PATCH → CONFIRMEDCustomerin-app + email
tour-cancelledToursTour cancelledOther tour participantin-app + push + email
tour-completedToursTour marked completeCustomerin-app + push + email
lead-assignedLeadsleads.assign succeedsAssigneein-app + email
lead-sla-breachLeadsWorker sla-monitor tier 1 breachAssigneein-app + email
lead-sla-escalationLeadsWorker sla-monitor tier 2 (ADMINs) + tier 3 (LEAD)Admins / team leadin-app + email
lead-ingestion-failedLeadsWorker DLQ write after 3 failed retriesAll adminsin-app + email
saved-search-matchAlertsWorker saved-search-scan cron (*/30 * * * *)Saved-search ownerin-app + email
price-dropAlertsWorker pricing listener (ACTIVE → lower price)Property favoritersin-app + email
listing-status-changeAlertsWorker status listenerProperty favoritersin-app + email
staging-variants-readyListingsWorker sweep: every AI room-staging job on a listing has settledStaging requesterin-app only

The 9 rental-* workflows that previously sat alongside these were removed in May 2026 alongside the Rental Companion deferral — they will be re-authored fresh when the module returns.

The Channels column is the baseline. A June 2026 delivery pass (1) added push to every customer/agent-facing workflow (only lead-ingestion-failed and team-invitation stay in-app + email, and staging-variants-ready — added later — is in-app only), (2) made the notification-style workflows suppress the fallback email once the recipient reads the in-app/push notification (delay 10m → branch-on-read → email) while transactional emails always-send, and (3) gave saved-search-match + price-drop a 1h batch step (anti-spam). The workflow definitions are codified under knock/ and managed with the Knock CLI — see configuration and packages/notifications/FEATURE.md for the per-workflow detail.

ConcernFile
Translations + email layout + escapeHtmlpackages/notifications/src/
Server wrapper (web + apps/api)packages/server-services/src/integrations/knock.ts
Server wrapper (worker)apps/worker/src/knock.ts
Web client SDKapps/svelte-web/src/lib/knock-client.svelte.ts
Web identify callapps/svelte-web/src/routes/+layout.server.ts
tRPC notifications.registerDevice (push)packages/api/src/routers/notifications.ts (native client side lands in a later phase)
Worker trigger surfacesapps/worker/src/leads.ts, apps/worker/src/alerts.ts
GetStream webhookapps/svelte-web/src/routes/api/stream-webhook/+server.ts
import { getTranslatedPayload } from '@repo/notifications';
import { triggerWorkflow } from '@repo/server-services';
const payload = getTranslatedPayload('new-inquiry', recipientLocale, {
customerName: 'Ana López',
propertyAddress: '...',
// ...
});
await triggerWorkflow('new-inquiry', [agentUserId], payload);
  • Per-recipient locale is resolved from the local User.locale column (already populated at signup). The Knock-side getUserLocale is a fallback for users created before locale tracking landed.
  • Worker uses Promise.allSettled when fanning out to multiple admins so a single locale failure doesn’t block the batch.
  • Email subject + body are server-side HTML composed via email-layout.ts (wrapEmailLayout, ctaButton, calloutBox, quoteBlock, propertyCard, detailTable / detailRow). All user-controlled strings go through escapeHtml from @repo/notifications before interpolation — email HTML escaping is OFF in the Knock dashboard because we escape server-side.
SurfaceWhat it does
Webknock-client.svelte.ts — reactive runes for knockUnreadCount / knockNotifications / knockInitialized. Real-time WebSocket via feedInstance.listenForUpdates(). Sonner toast for new items, skipping new-chat-message (Stream handles its own chat UI).
Native mobileThe in-app feed + unread badge land in a later native phase (Knock’s native SDKs) — see Conventions → Mobile (Native). The server-side trigger + channel fan-out are unchanged.

The active enhancement plan in .claude/plans/ covers:

  • Security — signed user identification tokens (the highest-severity open gap).
  • Observability — wrapper for every trigger with Sentry context + Knock delivery webhooks.
  • Mobile parity — customer-app unread badge, foreground toasts, iOS-ready scaffolding, sign-out unregistration, locale resync.
  • Per-workflow improvements — tour reminders (scheduled triggers + cancellation_key), pre-breach SLA warning, digest mode for chat + saved-search-match + price-drop.
  • User preferences — Knock preference groups for chat / tours / leads / alerts / teams / system.

See Configuration for env vars and the Knock dashboard checklist.