Files
manyangles/docs/email-delivery.md

4.3 KiB

Email delivery

Manyangles uses the RaceTix-style branded shell for verification, password resets, account invitations, and gallery-ready messages. HTML has plain-text alternatives, escaped dynamic content, and system-font fallbacks. People previews the actual gallery-ready HTML before queueing.

Production Resend configuration

Set EMAIL_PROVIDER=resend, RESEND_API_KEY, EMAIL_FROM (a verified sender), and NEXT_PUBLIC_APP_URL (the public HTTPS origin). Run the image/email worker with the same environment as the web process. Configuration changes do not migrate pending emails between providers. No production test sends are needed.

Completion outbox

Organizer actions enqueue one immutable payload per event/normalized email address. A unique database constraint deduplicates concurrent requests. Only opted-in guests qualify. Before sending, the worker checks consent and gallery visibility again. Messages whose guest link changed are held for review.

The worker records provider acceptance, attempts and sanitized errors. “Sent” means accepted by Resend/SMTP, not delivered to an inbox. No automatic event schedule sends email.

Configure a Resend webhook at /api/webhooks/resend and set RESEND_WEBHOOK_SECRET in the web process. Subscribe to email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, and email.failed. The endpoint verifies the bounded raw body with the Resend SDK and deduplicates by signed svix-id. It stores only message identifiers, outcomes and timestamps, never raw recipient payloads. Failed database writes return 503 for provider retry. Callbacks arriving before the worker saves its provider reference are retained and matched when email history is read.

People shows provider outcomes separately from queue status. Complaints, bounces and failures take precedence over delivered/delayed/sent regardless of callback order. Delivered means acceptance by the recipient mail server, not inbox placement. This tracks gallery-ready outbox messages; auth email outcomes do not have a management UI. Tracking does not change guest consent or implement a cross-event suppression list. Mailpit does not produce Resend callbacks.

Resend retries reuse gallery-ready/<delivery-id> and the original payload. Retries back off, stop after five attempts, and stay inside a conservative 23-hour window (Resend retains idempotency keys for 24 hours). Expired/uncertain deliveries require provider review. The People tab permits safe Resend retries inside that window. SMTP ambiguity is not automatically retried.

Account verification, password resets and account invitations remain synchronous transactional sends. They also use stable, recipient-specific idempotency keys.

Local verification

Keep EMAIL_PROVIDER=mailpit, SMTP_HOST=127.0.0.1, SMTP_PORT=1027. Open Mailpit at http://localhost:8027. Test scripts reject nonlocal Mailpit settings.

bun run email:preview sends all four templates to the local inbox, like RaceTix's preview workflow. Authentication/invitation links in these samples are deliberately nonfunctional; the gallery sample links to /e/demo.

Docker Compose already supplies Postgres (5439), Mailpit (SMTP 1027 / inbox 8027), and Garage (3900 / 3903). Use bun run docker:up, bun run docker:logs, and bun run docker:down. The web app and worker run on the host with Bun, just as RaceTix's development setup does. Stopping this project's Compose stack does not stop Colima or disrupt other projects.

bun run typecheck
bun test
WEBHOOK_INTEGRATION=1 bun --env-file=.env test packages/email/src/webhooks.test.ts
cd apps/web
NOTIFICATIONS_INTEGRATION=1 PUBLISHING_INTEGRATION=1 bun --env-file=../../.env test src/server/guest-notifications.integration.test.ts src/server/publishing.integration.test.ts

With the local worker running, add REAL_UPLOAD_INTEGRATION=1 to the publishing test command to copy one existing /e/demo original through a presigned PUT, verify a single queued job and smaller generated variants, then remove only the temporary event and its uploaded objects. The original demo assets are untouched.

Run bun --env-file=.env packages/database/src/verify-migrations.ts from the repo root to migrate a temporary local database; the script removes that database after verification. It never migrates a remote database.