Skip to content

How notifications travel

The architecture is opaque to most coaches, but understanding the plumbing helps when things look slow or weird.

The pipeline

[Coach edits event] → [worker writes audit_log row]
[fanoutNotifications]
├── if change is "material" (time/location/coach-note/cancel/…)
│ ├── if ≤48h: spawn NotificationWorkflow per family (immediate)
│ └── if >48h: write digest_pending + spawn DigestWorkflow
│ per (family, day) — sleeps until 6pm club tz
└── for each other coach with access + notify-on for
this calendar: fire web push directly (no queue)
NotificationWorkflow (immediate):
├── validate-family (skip if archived)
├── send-telegram (once per unique channel, with retry)
├── enqueue-push (fan out via tscal-push-queue)
└── record-sent (notification_sent for /coach/feedback)
DigestWorkflow (per family per day):
├── sleep until 6pm club tz
├── gather all digest_pending rows for this family
├── send-emails — one per opted-in contact
└── clear-pending

Why ≤48h has no email

Push + Telegram cover the urgency case. Email is reserved for the daily 6pm digest where families want a combined “here’s what changed today.” This also keeps us comfortably under Cloudflare Email Workers’ 1000/day cap.

What if a family has push, Telegram, AND email enabled?

For a ≤48h change: push fires + Telegram posts + no email. They see the change three places (lock screen, Telegram channel, the in-app banner next time they open) but only get one phone buzz from push (Telegram has its own).

For a >48h change: just the daily digest email at 6pm. Family page banner is also there but doesn’t push.

What if a family has none of those?

They see changes the next time they open their family page (the “what’s changed since last visit” banner + the chat panel below). This is the no-notification baseline.

Latency

Immediate sends fire within seconds of the audit row landing — the workflow runs as soon as the queue scheduler picks it up, then the push queue fans out per device in parallel. No 5-min cron lag any more.

Other workflows quietly working in the background

In addition to the immediate + digest pipelines above, the worker runs:

WorkflowWhenWhat it does
DailyBriefWorkflowSpawned at 06:00 UTC; sleeps to each coach’s brief_timePushes today’s events to each coach with brief_time set
RsvpReminderWorkflowSpawned at event create when an RSVP deadline is setPushes pending families at -24h and -1h before close
InviteReminderWorkflowSpawned when an invite email is sentDay-3 and day-6 reminder emails until the invite is claimed
BotHealthWorkflowMonday 13:00 UTCProbes every Telegram channel via getChat; writes a bot-health feedback row when a channel is broken
PushFailureWorkflowTriggered from the push DLQRecords failed_push and falls back to email when the family has zero active push subscriptions left

Each is a durable Cloudflare Workflow — the sleeps survive worker restarts, deploys, and CF runtime upgrades. Status of all instances is visible in the Cloudflare dashboard under Workers → Workflows.

Where to investigate when something looks off

  1. /coach/feedback — first stop. Bot-health alerts, failed push deliveries, and parent-submitted issues all surface here.
  2. Cloudflare Workers Logs (dashboard → Workers → tscal-v2-worker → Logs) — 3-day retention, includes step-by-step output from every workflow run.
  3. Cloudflare Workers → Workflows — every workflow instance with its status, current step, and (for sleeping ones) when it’ll wake.