Email Tracking
Notification emails include tracking tokens for open detection, click tracking, and one-click unsubscribe. Tracking is implemented via the notification API routes and integrated with Resend webhooks.
Source: src/lib/notifications/email-tracking.ts, src/api/routes/notifications/index.ts
Architecture
Tracking Tokens
Tokens are cryptographically random 24-byte values encoded as base64url (no padding). Each email generates three tokens:
| Type | Purpose | Response |
|---|---|---|
open | Detect when email is opened | 1x1 transparent GIF |
click | Track link clicks with redirect | HTTP 302 to destination URL |
unsubscribe | One-click preference update | HTML confirmation page |
Token Schema
interface TrackingToken {
token: string; // 24-byte random, base64url
delivery_id: string; // Associated delivery record
token_type: 'open' | 'click' | 'unsubscribe';
destination_url?: string; // For click tokens: original link
metadata?: {
notification_type?: string; // For unsubscribe: which preference to disable
};
consumed_at?: string; // Set when token is used
expires_at: string; // 30-day expiry
created_at: string;
}Token Lifecycle
- Created during email rendering (3 tokens per email)
- Stored in
notification_email_tracking_tokenstable - Consumed on first use (
consumed_atset, prevents replay) - Expired after 30 days (cleaned up by scheduler worker)
Tracking URLs
URLs are constructed using the app's base URL:
Open: /api/notifications/email/open/{token}
Click: /api/notifications/email/click/{token}
Unsubscribe: /api/notifications/email/unsubscribe/{token}URL Helpers
import {
buildNotificationEmailTrackingPath,
buildNotificationEmailTrackingUrl
} from '$lib/notifications/email-tracking';
// Relative path
buildNotificationEmailTrackingPath('open', token);
// → '/api/notifications/email/open/abc123...'
// Full URL
buildNotificationEmailTrackingUrl('click', token, 'https://app.podcasterplus.com');
// → 'https://app.podcasterplus.com/api/notifications/email/click/abc123...'Open Tracking
When an email client loads images, it requests the tracking pixel:
Endpoint: GET /api/notifications/email/open/:token
- Validate token exists, is
opentype, not expired - Mark token as consumed (
consumed_at = now) - Update delivery status to
opened(withopened_attimestamp) - Return 1x1 transparent GIF
// Pixel response helper
createNotificationTrackingPixelResponse();
// → Response with transparent GIF, correct headers, no-cacheTracking Limitations
Email open tracking relies on image loading, which is blocked by some email clients. Use Resend webhooks as a complementary signal.
Click Tracking
Email links are wrapped to pass through the click tracker:
Endpoint: GET /api/notifications/email/click/:token
- Validate token exists, is
clicktype, hasdestination_url, not expired - Mark token as consumed
- Update delivery status to
clicked(withclicked_attimestamp) - HTTP 302 redirect to
destination_url - Falls back to app homepage if destination URL is missing
Unsubscribe
One-click unsubscribe disables the specific notification type:
Endpoint: GET /api/notifications/email/unsubscribe/:token
- Validate token exists, is
unsubscribetype, not expired - Extract
notification_typefrom token metadata - Map notification type to preference key (e.g.,
booking.requested→email_new_booking) - Update
user_profiles.notification_preferencesto set that key tofalse - Mark token as consumed
- Return HTML confirmation page
The unsubscribe page is a self-contained HTML page rendered by renderNotificationUnsubscribeHtml().
Resend Webhooks
Resend sends webhook events for email lifecycle, providing server-side tracking that complements token-based tracking:
Endpoint: POST /api/notifications/webhooks/resend
Webhook Events
| Resend Event | Delivery Status | Timestamp Field |
|---|---|---|
email.sent | sent | sent_at |
email.delivered | delivered | delivered_at |
email.opened | opened | opened_at |
email.clicked | clicked | clicked_at |
email.bounced | bounced | failed_at |
email.complained | failed | failed_at |
email.failed | failed | failed_at |
Status Hierarchy
Delivery status never downgrades. The chooseDeliveryStatus() function enforces this order:
pending < queued < processing < sent < delivered < opened < clickedIf the current status is opened and a delivered webhook arrives (late), the status stays opened.
Webhook Verification
Webhooks are verified using Resend's Svix-based signature:
svix-id: msg_xxx
svix-timestamp: 1234567890
svix-signature: v1,base64signatureThe RESEND_WEBHOOK_SECRET environment variable is required for signature verification.
Rate Limiting
The webhook endpoint is rate-limited to 300 requests per minute per IP to prevent abuse.
Email Renderer Integration
When the email channel executor renders an email, it:
- Creates three tracking tokens (open, click, unsubscribe) via the store
- Builds tracking URLs using the app's base URL
- Passes URLs to the email renderer:
- Open pixel: Injected as an
<img>tag at the bottom of the email - Click wrapper: CTA button links are wrapped through the click tracker
- Unsubscribe link: Added to the email footer
- Open pixel: Injected as an
- Resend also receives tags (
delivery_id,notification_id,notification_type) for webhook correlation
Token Cleanup
Expired tokens (older than 30 days) are automatically cleaned up by the notification-scheduler worker during each cron run, in batches of 100.
Related Documentation
- Notification System Overview -- Architecture and channels
- Delivery Pipeline -- How email delivery works
- Resend Email Service -- Email provider integration
- Notification API Reference -- Full endpoint details