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
processingand incrementsattempts - Skips if the delivery was already claimed (concurrent safety)
2. Pre-Delivery Checks
Before executing, several checks may skip or reschedule the delivery:
| Check | Channels | Condition | Action |
|---|---|---|---|
| Push subscription | Push | No active subscriptions | Skip (push_subscription_revoked) |
| Quiet hours | Email, Push | Currently in quiet hours | Reschedule to endsAt |
| Read state | Email (chat/mention only) | Notification already read | Skip (notification_read) |
| Hourly cap | hourly_cap_key already sent this hour | Skip (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
- Render HTML and text versions using the registered email renderer
- Create tracking tokens (open, click, unsubscribe) in the database
- Build tracking URLs using
PUBLIC_APP_URL - Send via Resend API with tags for webhook correlation
- Store
provider_message_idfor webhook matching
Email Renderers:
| Renderer | Notification Type | Template |
|---|---|---|
generic | Default fallback | Title + body + CTA button |
chat_missed_message | chat.message | Sender, episode, message preview |
host_notification | booking.requested | Guest details, booking time, custom fields |
episode_invited | episode.invited | Inviter, episode, podcast |
network_response | network.response | Accept/decline status, inviter info |
Push Channel
- Build payload from notification content (title, body, URL, max 140 chars)
- Fetch all active subscriptions for the recipient
- For each subscription:
- Encrypt payload with VAPID/aes128gcm
- Send to the push service endpoint
- Handle responses (201=success, 404/410=revoke, 5xx/429=retry)
- 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
| Outcome | Status | Next Step |
|---|---|---|
| Success | sent or delivered | Record provider metadata, clear errors |
| Transient error (retryable) | failed | Set next_retry_at with escalating delay |
| Permanent error | failed | Insert into notification_delivery_dead_letters |
| Max retries exhausted | failed | Insert into dead letters |
Retry Delays
| Attempt | Delay |
|---|---|
| 1 | 5 minutes |
| 2 | 15 minutes |
| 3 | 60 minutes |
| 4+ | 180 minutes |
Configuration
Wrangler Bindings
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 = 10000Queue Configuration
| Setting | Value | Purpose |
|---|---|---|
max_batch_size | 25 | Messages per invocation |
max_batch_timeout | 30s | Max wait for full batch |
max_retries | 5 | Worker-level retries before DLQ |
dead_letter_queue | notification-deliveries-dlq | Failed message destination |
cpu_ms | 10,000 | CPU time limit per invocation |
Environment Variables
| Variable | Purpose |
|---|---|
RESEND_API_KEY | Resend API authentication |
RESEND_FROM_EMAIL | Sender email address |
PUBLIC_APP_URL | Base URL for tracking links and notification URLs |
VAPID_PUBLIC_KEY | Web Push VAPID public key |
VAPID_PRIVATE_KEY | Web Push VAPID private key |
VAPID_SUBJECT | VAPID subject (mailto: or https: URL) |
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /health, /_health | None | Health check |
Database Tables
| Table | Operations |
|---|---|
notification_deliveries | Claim, update status, retry scheduling |
notifications | Read read_at for read-state checks |
user_profiles | Fetch recipient email, timezone, preferences |
episode_guests | Fetch guest email for guest notifications |
notification_push_subscriptions | List active, mark used/failed, revoke |
notification_email_tracking_tokens | Create open/click/unsubscribe tokens |
notification_delivery_dead_letters | Archive permanent failures |
Observability
[observability.logs]
enabled = true
head_sampling_rate = 1
invocation_logs = true
persist = trueStructured Logging
{
"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 detailsnotification_delivery_skipped-- Skipped with reasonnotification_delivery_retry_scheduled-- Retry with next attempt timenotification_delivery_dead_lettered-- Permanent failure archived
Deployment
cd workers/notification-executor
npx wrangler deploySecrets
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_SUBJECTRelated Documentation
- Notification System -- System overview
- Delivery Pipeline -- Full pipeline architecture
- Push Notifications -- Web Push / VAPID details
- Email Tracking -- Tracking token system
- Notification Scheduler -- Cron worker that feeds this queue
- Workers Overview -- All workers