Definitive offline reference for operators running Maison Relay — the messaging backbone for the Maison Doclar ecosystem. Covers campaigns, audience filtering, WhatsApp templates, FFmpeg asset rendering, inbound keywords, reports, forms, host dashboards, and environment configuration.
Overview
Maison Relay is the messaging backbone for the Maison Doclar ecosystem. It ingests guests, builds audiences, runs WhatsApp/SMS/email campaigns, pre-renders personalised assets to Backblaze B2, dispatches via Twilio and Resend, handles inbound RSVP keywords, and generates host reports.
Relay connects to Gateway (sync/writeback), md-assets (access cards + personalised video completion), Tally (forms), and Video Utility (compliant masters registered into Assets). Operator UI: APP_BASE_URL.
Campaign execution runs on BullMQ workers backed by Redis. The prepare phase resolves the audience and pre-renders personalised assets to B2. The send phase dispatches per-guest messages with batch delays, send windows, and channel fallback. The complete phase finalises the campaign, generates reports, and sends operator alerts.
Dashboard
The home dashboard shows a cross-event summary via GET /api/events/dashboard-overview: active event count, campaigns currently sending, messages sent today (UTC day), and overall delivery rate.
The Active Events table lists each non-archived event with campaign count, send/delivery totals, RSVP rate, and status. Click a row (or Open) to set that event as active and jump to Campaigns.
Archive calls POST /api/events/:id/archive (sets archivedAt; guest data is preserved). Archived events appear in the collapsible section; Unarchive clears archivedAt. The sidebar event list excludes archived events unless GET /api/events?includeArchived=true.
Guests
The Guests nav section lists all guests for the active event. Import via CSV (POST /api/events/:eventId/guests/import) or manual create. AI data cleaning flags invalid contacts before campaigns.
Guest detail panel: RSVP status, household, suppression state, 24h conversation window (lastInboundAt), dietary notes, check-in/attendance. Guest Portal button issues a 72h magic link (POST /api/guest-portal/link).
Check-in stats banner: GET /api/events/:eventId/checkin-stats (refreshes every 30s).
Analytics
Per-event cross-campaign analytics: GET /api/events/:eventId/analytics (requires operator auth).
Returns aggregate sends, delivery and read rates, RSVP confirmed/declined/pending counts, channel breakdown, and per-campaign summaries. The Analytics nav section renders summary cards, channel and campaign tables, and an inline RSVP bar chart.
Audit Log
Operator actions are recorded in admin_audit_log. Query via GET /api/system/audit-log with optional entityType, adminUsername, startDate, endDate, limit (default 50), and offset.
Each entry includes action (e.g. event.archived, template.update), entityType, entityId, beforeState, and afterState JSON snapshots. The System UI shows a compact diff of up to three changed fields.
Campaigns
A campaign targets an audience segment (or inline filter), binds a message template, optionally attaches personalised assets, and sends via a primary channel with optional fallback.
Lifecycle statuses
| Status | Meaning |
|---|---|
draft | Being configured. Not queued for send. |
scheduled | Approved configuration; will start at scheduledAt or on manual launch. |
sending | Prepare and/or send workers are active. |
paused | Send queue paused; no new dispatches until resumed. |
completed | All sends finished (sent, failed, suppressed, or skipped). |
cancelled | Terminated by operator; no further sends. |
Valid transitions
draft→scheduled,cancelledscheduled→sending,cancelled,draft(revert)sending→paused,completed,cancelledpaused→sending,cancelledcompletedandcancelledare terminal
Scheduling
Set scheduledAt (UTC) and transition to scheduled. The campaign scheduler polls and moves eligible campaigns to sending when the time arrives and send-window rules allow.
Pause, resume, cancel
- Pause — transition
sending→paused. In-flight jobs finish; new sends wait. - Resume — transition
paused→sending. - Cancel — allowed from
draft,scheduled,sending, orpaused. Pending sends are stopped.
Transitioning to sending runs validation: audience non-empty, template present, WhatsApp templates approved (if applicable), and test-send gate satisfied (testSendStatus must not be none).
If a campaign appears stuck in sending, use POST /api/system/queues/campaign/:campaignId/recover (see Troubleshooting).
Campaign detail — Monitor tab
Open a campaign and select the Monitor tab. While status is sending, live stats poll every five seconds via GET /api/campaigns/:id/live-stats.
- Stat cards — Total, Delivered, Failed, Send rate (messages sent in the last 60 seconds), and Delivery % (colour-coded: green ≥90%, gold ≥70%, red <70%).
- Progress bar — Delivered count vs total sends.
- Delivery rate sparkline — Last ten polling samples (sending campaigns only).
- Activity log — Latest 30 send events with guest name, channel, status, and timestamp. Skipped and suppressed rows show the worker
lastErrorreason (e.g. Skipped: delivered within 24h, Deferred: outside send window).
For draft or scheduled campaigns, stats are static (no polling or sparkline).
Failed Sends tab
When any sends have status failed, a Failed Sends tab appears with a triage table: guest, channel, error (truncated with full text on hover), attempt count, next retry time, and Retry Now per row (POST /api/campaigns/:id/sends/:sendId/retry). Retry All Failed requeues each failed send sequentially.
Pre-flight checklist
For draft and scheduled campaigns, a checklist card loads from GET /api/campaigns/:id/preflight: audience size, template and WhatsApp approval, send window, asset config, and validator errors/warnings. Green ✓, amber ⚠, red ✗. Footer text indicates ready to send, warnings to review, or blocking errors.
Resume confirmation banners
While sending, blue and amber banners summarise completed vs queued sends and warn when prepareAttempts > 1 (campaign was restarted).
Duplicate campaign
POST /api/campaigns/:id/duplicate creates a draft copy named {original} (copy) with the same audience, template, channels, send window, and asset config (not send history or schedule).
Graceful asset degradation
Before dispatch, Relay performs a 3-second HEAD check on image/video URLs. If the asset is unreachable, the send continues without media, logs dispatch.asset_unreachable, and records assetDegraded: true on the send's providerResponse.
During pre-warm, per-guest render failures do not block the campaign. If more than 90% of guests fail asset generation, the campaign is paused and a system alert is written.
Access card campaigns
When messageType is access_card and the primary channel is WhatsApp, Relay attaches the guest's card image as MediaUrl. It uses send.imageUrl when set; otherwise GET {ASSETS_BASE_URL}/api/assets/cards/{nucleusEventId}/{nucleusGuestId} with ASSETS_API_KEY bearer auth. Generate cards in Assets before launching.
Guest portal links
From the guest detail panel, Guest Portal creates a 72-hour magic link (POST /api/guest-portal/link) for self-service RSVP and dietary updates.
WhatsApp Templates
WhatsApp messages outside the 24-hour session window require a Meta-approved template registered via the Twilio Content API. Relay stores the Content SID (HX…) on each message_templates record.
Admin UI workflow (create → submit → approve)
When editing a WhatsApp template in Relay, the WhatsApp Template Manager panel replaces manual SID entry with three accordion steps:
- Create in Twilio (shown when no Content SID yet) — friendly name, body (supports
{{1}}variables), variable map, optional media URL, and category. Create in Twilio Content API callsPOST /api/twilio/content-templates/createand stores the returnedHX…SID (pending_creation). Save the template in Relay first so atemplateIdexists. - Submit for Meta Approval (shown when SID exists but not approved) — Meta template name (lowercase + underscores) and category. Submit for Approval calls
POST /api/twilio/content-templates/:sid/submit-approval(submitted). - Approval Status — displays SID, status badge, rejection reason if any, and Check Status (
GET /api/twilio/content-templates/:sid/approval-status?templateId=). Whenapproved, WhatsApp campaigns using this template may entersending.
API-only alternative: link an existing SID via POST /api/twilio/content-templates/link with { templateId, twilioContentSid } (must start with HX).
List all Content templates: GET /api/twilio/content-templates.
Approval statuses
| Status | Meaning |
|---|---|
pending_creation | SID assigned; not yet submitted to Meta. |
submitted | Waiting on Meta / Twilio approval. |
approved | Template cleared for outbound WhatsApp campaigns. |
rejected | Meta rejected — fix copy/category and resubmit. |
| (empty) | No Content SID linked — session-window freeform only. |
sending unless the bound template has twilioApprovalStatus === 'approved' and a valid twilioContentSid. Unapproved templates are blocked at validation time.
Utility quality checker
Before submitting to Meta, run POST /api/templates/:id/check-utility. The checker scores template copy 0–100 against utility classification rules (promotional language, personalisation variables, CTA type). Score ≥ 70 is required to submit via POST /api/templates/:id/submit-whatsapp.
Import from Twilio console
Templates created directly in Twilio can be synced with GET /api/templates/twilio-sync and linked via POST /api/templates/twilio-import with { contentSid, variableMapping }.
Rejection reasons
| Code | Fix |
|---|---|
PROMOTIONAL_CONTENT | Remove offers; reframe as service message (attendance, access, schedule). |
MISSING_VARIABLE | Add {{1}} guest personalisation. |
CALL_TO_ACTION_MISMATCH | Use utility CTA: Confirm attendance, Download access card. |
INVALID_FORMAT | Variables must start at {{1}} and increment. |
Content Template Status Callback
In Twilio console → WhatsApp Senders → your sender → set Content Template Status Callback URL to:
https://relay.maisondoclar.com/api/webhooks/twilio/content-approval
Relay updates template approval status and sends operator alerts on approve/reject.
Campaign Operations
Preflight
POST /api/campaigns/:id/preflight resolves the audience with duplicate-number handling (first / all / flag), household dedup (primary_only / all_members), optional Termii DND check for +234 numbers, cost estimate, and Twilio balance snapshot.
Resend targeting
failed— original-round failed sendsundelivered_24h— stillsentafter 24 hours- Optional
channeloverride for alternate-channel resend (SMS fallback, etc.)
Archive
POST /api/campaigns/:id/archive on completed/cancelled campaigns aggregates metrics into archiveMetricsSnapshot, deletes individual campaign_sends rows, and sets archivedAt. Use when operational detail is no longer needed but summary metrics must be preserved.
Data Retention
Daily maintenance (01:00 UTC) prunes old records. Defaults:
| Data | Default retention |
|---|---|
| Delivered campaign sends | 90 days |
| Failed/suppressed/skipped sends | 30 days |
| Read guest message history | 7 days |
| Info system events | 14 days |
| Gateway event log | 30 days |
Configure via GET/PATCH /api/system/retention-config. Campaign archive is manual and preserves metrics independently of automatic retention.
Templates
Message templates define copy per channel and message type. When editing an existing template, the Performance section loads GET /api/templates/:id/analytics: campaign count, total sends, delivery and failure rates, top failure reasons (up to three), and last-used date.
HTML email templates
Email templates support Plain text or HTML mode. HTML body is stored in bodyHtml with isHtmlTemplate; plain bodyTemplate is sent as the text fallback via Resend. Use Preview / Mobile preview (sandboxed iframe) before saving.
SMS templates
SMS editor shows live character and segment counts (GSM-7: 160/153; Unicode: 70/67). Colour indicates cost risk: green (1 segment), amber (2), red (3+). Include opt-out copy such as Reply STOP to unsubscribe. smsCharCount and smsSegmentCount are saved on the template record.
Email campaigns use Resend. Templates support plain text always; optional HTML mode stores bodyHtml with isHtmlTemplate and sends HTML + text fallback.
Editor: toggle Plain/HTML, preview in sandboxed iframe, mobile width preview. Dispatch uses Resend with both html and text parts when configured.
Delivery status may update via POST /api/webhooks/resend when RESEND_WEBHOOK_SECRET is set.
SMS
SMS templates show a live character and segment counter (GSM-7: 160/153; Unicode: 70/67). Colour: 1 segment green, 2 amber, 3+ red.
On save, smsCharCount and smsSegmentCount persist on the template. Include opt-out language (e.g. Reply STOP to unsubscribe) for compliance.
Dispatch via Termii using TERMII_API_KEY and TERMII_SENDER_ID. Nigerian DND registry is checked when preflight DND option is enabled.
Send Time Optimisation
In the campaign create/edit form, use AI Send Time next to the send window fields. This calls GET /api/ai/send-time?eventId= and suggests a start–end window in WAT (West Africa Time) based on recent delivery histograms for that event, optionally refined by AI when the SEND_TIME_OPTIMISATION flag is enabled.
Click Apply to populate the send window start and end inputs. Send-window rules still apply at dispatch time; jobs outside the window are deferred with lastError: Deferred: outside send window.
Audience & Segmentation
Audience filters
Campaigns target guests via a saved segment or an inline filterDefinition JSON object. Filters can include RSVP status, source, tags, channel eligibility (valid phone/email), table assignment, and custom fields. The audience resolver runs at prepare time and materialises campaign_sends rows.
Suppression lists
Suppressed guests are excluded from sends. Reasons stored on suppression_lists:
| Reason | Typical cause |
|---|---|
opted_out_all | Guest opted out of all channels (preference centre or admin). |
opted_out_channel | STOP keyword or channel-specific opt-out (e.g. WhatsApp). |
admin_suppressed | Operator manually suppressed the guest. |
duplicate | Duplicate contact detected during import. |
invalid_contact | Phone/email failed validation or provider unreachable codes. |
Household grouping
Guests sharing a householdId are treated as one unit for WhatsApp and SMS deduplication. Only the primary household contact receives the message; other members are skipped with household_dedup.
Cross-campaign 24-hour deduplication
Before dispatch, Relay checks whether the guest already has a delivered send on the same channel within the last 24 hours (any campaign). If so, the current send is marked skipped automatically. This prevents message fatigue across concurrent or back-to-back campaigns.
To investigate: query campaign_sends for the guest, channel, and recent sentAt timestamps.
Asset Generation
Personalised images and videos are rendered server-side with FFmpeg drawtext (same approach as md-assets). Cloudinary pre-warm has been removed — assets are written to Backblaze B2 before the campaign enters sending.
Overlay presets
Presets define text overlay behaviour:
- Zones —
horizontalZone(left/center/right),verticalZone(upper/middle/lower), pixel offsets - Colour —
colorHex(default goldB79F85) - Auto-size rules — array of
{ max_chars, font_size }matched to guest display name length - Video timing — optional
videoOverlayStartS,videoOverlayDurationS
Font file: /app/fonts/InstrumentSans-SemiBold.ttf (bundled in Docker image).
Manage presets via GET/POST /api/assets/overlays.
B2 storage path
relay/{eventId}/campaigns/{campaignId}/{guestId}.{ext}
Public URL format: https://f005.backblazeb2.com/file/md-media-assets/{key}
Pre-render before sending
During campaign prepare, the asset worker downloads source media from B2, applies drawtext per guest, uploads the rendered file, and attaches URLs to send rows. Failures surface in prepare logs before any Twilio/Resend dispatch.
Assets service integration
When the Assets service completes a personalised video render job, it automatically notifies Relay at POST /api/campaigns/asset-job-complete. Relay updates guest_assets and campaign_sends with guest video URLs. The operator does not need to manually transfer URLs between apps. Requires ASSETS_API_KEY (or default Bearer internal) on the Assets side.
Per-guest preview
GET /api/campaigns/:id/preview/:guestId
Returns the resolved message body and asset URLs for one guest — use to verify placeholders and overlay output before launch. Campaign-level summary: GET /api/campaigns/:id/preview.
Inbound Messages
Inbound WhatsApp messages arrive at POST /api/webhooks/twilio (signature-validated, rate-limited to 60 requests/minute per IP). Relay matches From to guests.phoneE164, processes keywords, logs every message, and optionally writebacks opt-outs to the Gateway.
Keyword handling
| Keyword | Action |
|---|---|
STOP | Opt out of WhatsApp; add opted_out_channel suppression; Gateway delivery writeback (OPT_OUT); confirmation message sent. |
START | Remove WhatsApp from optedOutChannels; resubscribe confirmation sent. |
HELP | Send help text (STOP/START instructions). |
YES | Confirm RSVP (blocked after rsvpCloseDate); household pending members updated to yes; custom confirm message from event settings. |
NO | Decline RSVP (same close-date rules); household pending members updated to no; change-from-yes acknowledgement. |
DIETARY: … | Store text after DIETARY: in guests.dietaryNotes; confirmation echoes the note. |
Configure RSVP close date and custom messages via sidebar ⚙ Event settings → PATCH /api/events/:id/rsvp-settings. Daily send budget and max sends per guest are enforced in the send worker.
Inbound log (admin UI)
The Inbound nav section lists messages via GET /api/events/:eventId/inbound-messages with channel/keyword filters, colour-coded actionTaken badges, and pagination.
Conversation window
Each inbound message sets lastInboundAt and conversationWindowOpen (24-hour session). Guest detail shows window status. STOP closes the window.
Non-keyword messages
Messages that do not match a keyword are stored in inbound_messages with keywordDetected: null and passed to AI intent classification (classifyInboundMessage) for operator review and future automation.
Guest Self-Service Portal
Operators generate a link with POST /api/guest-portal/link (guestId, eventId). The URL is {HOST_DASHBOARD_BASE_URL}/guest/{token} (72-hour JWT).
Guests can confirm or decline RSVP (unless rsvpCloseDate has passed), save dietary notes, and view their access card image when available.
Public API: POST /api/guest-portal/rsvp and POST /api/guest-portal/dietary with the token in the body.
Check-in Tracking
Gateway webhooks checkin.created set guests.attended = true. attendance.reconciled updates attendance on guest records and writes attendanceOutcome on matching send records.
The Guests view shows check-in stats from GET /api/events/:eventId/checkin-stats: totals, no-show, walk-in, rate, and recent check-ins (refreshes every 30 seconds).
Reports
When a campaign completes, Relay can generate PDF reports with metrics charts and an AI narrative summary.
Report types
| Type | Audience | Content |
|---|---|---|
internal | Operators | Full delivery metrics, failures, channel breakdown, suppression stats. |
host | Event hosts | Curated, host-safe view — high-level outcomes without internal diagnostics. |
PDF storage
PDFs upload to B2 at reports/{campaignId}-{timestamp}.pdf. Generate via POST /api/reports/generate (BullMQ report-generate queue).
Magic links
Share reports with hosts without dashboard login:
POST /api/reports/:id/magic-link— create link (30-day expiry)POST /api/reports/:id/resend-link— new link with 72-hour expiry; revokes prior tokenDELETE /api/reports/:id/magic-link— revoke
The Reports UI shows link status (active with expiry, expired, or never sent). Resend Link opens a modal to copy the URL or open WhatsApp with a pre-filled message to the host.
Scheduled delivery
In Event settings, enable Auto-generate host report: set hours after event end (default 24) and delivery time in WAT (default 09:00). Saved via PATCH /api/events/:id/rsvp-settings (reportScheduleTime, reportScheduleHoursAfterEvent).
A daily job at 08:00 WAT checks completed campaigns; if the host report has no active magic link, generation is queued automatically.
Forms & RSVP
Tally webhook
Tally form submissions POST to Relay's Tally webhook endpoint. Requests are verified with TALLY_SIGNING_SECRET (HMAC). Mapped fields update guest records per the form configuration.
Form tokens
Prefilled Tally links use signed tokens with configurable tokenExpiryHours (default 168). Tokens can embed guest name, phone, and guest ID as visible or hidden fields.
Field mapping
Each form_config stores fieldMapping — a JSON object mapping Tally field keys to Relay guest columns (e.g. RSVP status, dietary notes, plus-ones).
Duplicate submission handling
onDuplicate policy per form:
overwrite— latest submission winsreject— ignore duplicateflag_for_review— queue for operator review
Host Dashboards
Host dashboards give event hosts a read-only, branded view of campaign metrics without operator credentials.
Magic link access
Operators generate a JWT magic link per dashboard:
POST /api/dashboards/:id/magic-link
URL format: {HOST_DASHBOARD_BASE_URL}/host/{token}
Token hash is stored in host_dashboard_tokens for revocation auditing.
Module configuration
Each dashboard record defines which modules are visible (e.g. RSVP summary, send stats, recent inbound). Configure modules in the admin UI before issuing links.
Token expiry and revocation
- Expiry follows
tokenExpiryDayson the dashboard (JWTexpclaim). - Revoke by deleting the token record or rotating dashboard settings; expired links show "Link Unavailable".
- Re-issue a fresh magic link from the dashboard admin screen.
Environment Health
The admin UI Env Health section (and GET /api/system/env-health) reports configuration readiness without exposing secret values.
| Status | Meaning |
|---|---|
| ok | Variable is set and does not match placeholder heuristics |
| placeholder | Value looks like a template (contains your-, changeme, example, etc.) — replace before production |
| missing | Variable is unset |
Health score 0–100 = percentage of variables in ok state. Checked variables include database, JWT secrets, Redis, Twilio, B2, Resend, Anthropic, superadmin passwords, Assets URL, and Gateway credentials.
WhatsApp Business Quality
System → WhatsApp Sender Quality calls GET /api/twilio/sender-quality (Twilio Conversations API).
Displays quality rating, numeric score, status, and recommendation. Score colours: green ≥80, gold ≥50, red below.
Conversation windows: inbound messages open a 24h session (conversationWindowOpen) for session-message sends. Template messages required outside the window.
Send budget: per-event dailySendBudget and maxSendsPerGuest in Event settings reduce spam risk.
Dedup: 24h cross-campaign delivery dedup and household dedup protect quality rating and user experience.
System
The System nav section includes:
- Test contacts — three numbers for mandatory test sends before launch
- Queue management — depths, drain failed jobs, requeue/recover by campaign ID
- Integration info — webhook URLs, Gateway status
- WhatsApp sender check — basic number lookup
- Sender quality — see WhatsApp Business Quality
- System events — severity filter on
system_events - Audit log — see Audit Log
Env Health is a dedicated nav item: GET /api/system/env-health with 0–100 score.
Environment Variables
Configuration is validated at startup in src/lib/config.ts. Missing required variables prevent boot.
Required
| Variable | Purpose |
|---|---|
DATABASE_URL | PostgreSQL connection string |
JWT_SECRET | Operator session JWT signing |
HOST_DASHBOARD_JWT_SECRET | Host portal and report magic-link JWT signing |
HOST_DASHBOARD_BASE_URL | Public base URL for /host/{token} links |
REDIS_URL | BullMQ campaign, asset, report, and status workers |
TWILIO_ACCOUNT_SID | Twilio account |
TWILIO_AUTH_TOKEN | Twilio auth + webhook signature validation |
TWILIO_WHATSAPP_FROM | WhatsApp-enabled sender (e.g. whatsapp:+44…) |
B2_KEY_ID | Backblaze application key ID |
B2_APPLICATION_KEY | Backblaze application key secret |
B2_BUCKET_NAME | Target bucket (md-media-assets) |
B2_ENDPOINT | S3-compatible endpoint host |
RESEND_API_KEY | Resend email API |
RESEND_FROM_EMAIL | Default From address |
RESEND_WEBHOOK_SECRET | Resend webhook signature verification |
APP_BASE_URL | Public Relay URL (webhooks, CORS, Twilio callback URL) |
ANTHROPIC_API_KEY | AI features (inbound intent, narratives, suggestions) |
Optional
| Variable | Purpose |
|---|---|
TWILIO_MESSAGING_SERVICE_SID | Twilio Messaging Service for SMS |
SMS_FROM_NUMBER | SMS sender when not using messaging service |
ALLOWED_ORIGINS | CORS allowlist (comma-separated; falls back to APP_BASE_URL) |
ADMIN_ALERT_EMAIL | Operator email for campaign-complete alerts |
GATEWAY_BASE_URL | Gateway API for guest sync and writeback |
GATEWAY_API_KEY | Gateway bearer token |
GATEWAY_WEBHOOK_SECRET | HMAC for inbound Gateway webhooks |
AXIOM_API_KEY | Structured logging to Axiom |
AXIOM_DATASET | Axiom dataset name |
TALLY_SIGNING_SECRET | Tally webhook HMAC (required when forms enabled) |
PORT | HTTP listen port (default 3000) |
SUPERADMIN_1_PASSWORD | Bootstrap operator password (seed script) |
SUPERADMIN_2_PASSWORD | Second bootstrap operator password |
ASSETS_BASE_URL | Assets API base (default: https://assets-api-production-f80c.up.railway.app) |
ASSETS_API_KEY | Bearer token for Assets API (access card lookup, asset-job-complete; may match Gateway key) |
Troubleshooting
| Issue | Fix |
|---|---|
Campaign stuck in sending |
Check Redis is running and REDIS_URL includes valid credentials. Inspect BullMQ worker logs for stalled jobs. Recover via POST /api/system/queues/campaign/:campaignId/recover or System → Queue Management in the admin UI. |
| WhatsApp sends not delivered | Check template approval: GET /api/twilio/content-templates/:sid/approval-status. Status must be approved before campaigns can send. Verify twilioContentSid on the template and Twilio console for error 63007 (invalid/unapproved template). |
| FFmpeg overlay produces no text | Confirm font exists at /app/fonts/InstrumentSans-SemiBold.ttf. Check Dockerfile build logs for font download. Review asset worker logs for FFmpeg drawtext filter errors. |
| B2 upload fails | Verify B2_KEY_ID, B2_APPLICATION_KEY, B2_BUCKET_NAME, and B2_ENDPOINT are set. Confirm the bucket is Public in the Backblaze console so Twilio can fetch media URLs. |
| Inbound STOP not suppressing guest | Guest phoneE164 must exactly match the inbound number in E.164 format (Twilio sends whatsapp:+…, normalised on match). Check phone normalisation on import and that the guest belongs to the correct event. |
| Cross-campaign dedup blocking sends | Expected behaviour: guest received a delivered message on that channel within 24 hours. Inspect campaign_sends for the recent delivery. Wait for the window to elapse or adjust campaign timing/channels if intentional re-contact is required. |
| Test send gate blocks launch | Complete test sends for all three test contacts (testSendStatus must not be none). Configure contacts under System → Test Contacts. |
| Preflight shows WhatsApp not approved | Finish Twilio Content API workflow (create → submit → poll until approved). See WhatsApp Templates. |
| Asset unreachable but send still goes out | Expected graceful degradation: HEAD check fails → message sends without media, assetDegraded: true on send. Fix B2 public URL or regenerate assets. |
| Campaign paused after asset pre-warm | >90% guest asset generation failed. Review system events asset.campaign_paused. Fix master/overlay config in Assets, then resume campaign. |
| Report magic link expired | Use Reports → Resend Link (POST /api/reports/:id/resend-link, 72h). Or create link via Copy Link (30-day default on first create). |
| Scheduled host report not generated | Enable report schedule in Event settings (reportScheduleTime, hours after event). Event date must be past; job runs 08:00 WAT. Host report skipped if active magic link already exists. |
| 401 on admin API | Re-login. Verify JWT_SECRET unchanged. Check token blocklist if password was rotated. |
| Env health score low | Open Env Health nav. Replace placeholder values (changeme, your-*, example). All required vars in SYSTEM_PASSPORT must be set in Railway. |
| Access card image missing on WhatsApp | Generate cards in Assets. Ensure ASSETS_BASE_URL and ASSETS_API_KEY set. Relay fetches GET .../api/assets/cards/{nucleusEventId}/{nucleusGuestId}. |
| AI audience query fails | Check ANTHROPIC_API_KEY and AI feature flag in AI Settings. Review rate limits in logs. |