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.