Skip to content

Notification Executor Worker

The notification executor consumes messages from the notification-deliveries queue and delivers notifications across three channels: email (Resend), push (Web Push/VAPID), and in-app (Supabase Realtime).

Source: workers/notification-executor/

Worker Name: podcasterplus-notification-executor

Architecture

Processing Flow

1. Claim Delivery

For each queue message, the executor:

  • Fetches the delivery record with its parent notification, recipient profile, and push subscriptions
  • Updates status to processing and increments attempts
  • Skips if the delivery was already claimed (concurrent safety)

2. Pre-Delivery Checks

Before executing, several checks may skip or reschedule the delivery:

CheckChannelsConditionAction
Push subscriptionPushNo active subscriptionsSkip (push_subscription_revoked)
Quiet hoursEmail, PushCurrently in quiet hoursReschedule to endsAt
Read stateEmail (chat/mention only)Notification already readSkip (notification_read)
Hourly capEmailhourly_cap_key already sent this hourSkip (hourly_cap_reached)

Quiet Hours Re-evaluation

Quiet hours are checked again at execution time (not just planning time) because the user may have changed their preferences between planning and delivery.

3. Channel Execution

Email Channel

  1. Render HTML and text versions using the registered email renderer
  2. Create tracking tokens (open, click, unsubscribe) in the database
  3. Build tracking URLs using PUBLIC_APP_URL
  4. Send via Resend API with tags for webhook correlation
  5. Store provider_message_id for webhook matching

Email Renderers:

RendererNotification TypeTemplate
genericDefault fallbackTitle + body + CTA button
chat_missed_messagechat.messageSender, episode, message preview
host_notificationbooking.requestedGuest details, booking time, custom fields
episode_invitedepisode.invitedInviter, episode, podcast
network_responsenetwork.responseAccept/decline status, inviter info

Push Channel

  1. Build payload from notification content (title, body, URL, max 140 chars)
  2. Fetch all active subscriptions for the recipient
  3. For each subscription:
    • Encrypt payload with VAPID/aes128gcm
    • Send to the push service endpoint
    • Handle responses (201=success, 404/410=revoke, 5xx/429=retry)
  4. Return aggregate result

In-App Channel

Immediately marks as delivered. The notification record in the database is picked up by Supabase Realtime subscriptions on the client.

4. Result Handling

OutcomeStatusNext Step
Successsent or deliveredRecord provider metadata, clear errors
Transient error (retryable)failedSet next_retry_at with escalating delay
Permanent errorfailedInsert into notification_delivery_dead_letters
Max retries exhaustedfailedInsert into dead letters

Retry Delays

AttemptDelay
15 minutes
215 minutes
360 minutes
4+180 minutes

Configuration

Wrangler Bindings

toml
name = "podcasterplus-notification-executor"
compatibility_date = "2024-12-30"
compatibility_flags = ["nodejs_compat"]

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "a81d477ff9264805989f5a72f0354ee8"

[[queues.consumers]]
queue = "notification-deliveries"
max_batch_size = 25
max_batch_timeout = 30
max_retries = 5
dead_letter_queue = "notification-deliveries-dlq"

[limits]
cpu_ms = 10000

Queue Configuration

SettingValuePurpose
max_batch_size25Messages per invocation
max_batch_timeout30sMax wait for full batch
max_retries5Worker-level retries before DLQ
dead_letter_queuenotification-deliveries-dlqFailed message destination
cpu_ms10,000CPU time limit per invocation

Environment Variables

VariablePurpose
RESEND_API_KEYResend API authentication
RESEND_FROM_EMAILSender email address
PUBLIC_APP_URLBase URL for tracking links and notification URLs
VAPID_PUBLIC_KEYWeb Push VAPID public key
VAPID_PRIVATE_KEYWeb Push VAPID private key
VAPID_SUBJECTVAPID subject (mailto: or https: URL)

Endpoints

MethodPathAuthPurpose
GET/health, /_healthNoneHealth check

Database Tables

TableOperations
notification_deliveriesClaim, update status, retry scheduling
notificationsRead read_at for read-state checks
user_profilesFetch recipient email, timezone, preferences
episode_guestsFetch guest email for guest notifications
notification_push_subscriptionsList active, mark used/failed, revoke
notification_email_tracking_tokensCreate open/click/unsubscribe tokens
notification_delivery_dead_lettersArchive permanent failures

Observability

toml
[observability.logs]
enabled = true
head_sampling_rate = 1
invocation_logs = true
persist = true

Structured Logging

json
{
  "event": "notification_executor_batch_completed",
  "batch_size": 25,
  "processed": 24,
  "errors": 1,
  "duration_ms": 3200
}

Per-delivery logging includes:

  • notification_delivery_sent -- Successful send with provider details
  • notification_delivery_skipped -- Skipped with reason
  • notification_delivery_retry_scheduled -- Retry with next attempt time
  • notification_delivery_dead_lettered -- Permanent failure archived

Deployment

bash
cd workers/notification-executor
npx wrangler deploy

Secrets

bash
npx wrangler secret put RESEND_API_KEY
npx wrangler secret put RESEND_FROM_EMAIL
npx wrangler secret put VAPID_PUBLIC_KEY
npx wrangler secret put VAPID_PRIVATE_KEY
npx wrangler secret put VAPID_SUBJECT

Internal documentation - Not for public distribution