trails/openspec/changes/archive/2026-03-26-transactional-emails/design.md
Ullrich Schäfer ba044610b7
Add route interactions: click-to-split, drag-to-reshape, colored route rendering
- Enrich BRouter response with per-point 3D coordinates and segment boundary
  tracking (EnrichedRoute interface)
- ColoredRoute component: plain, elevation gradient (green→yellow→red), and
  surface color modes with invisible wide polyline for click targeting
- Click-to-split: click on route polyline inserts waypoint at nearest point,
  mapped to correct segment via boundary indices
- MidpointHandles: draggable CircleMarkers at route segment midpoints for
  reshaping, hidden below zoom 12, opaque on hover
- Color mode toggle (select) synced via Yjs routeData
- i18n keys for color mode labels (en + de)
- Unit tests for segment boundary tracking (13 tests)
- E2E tests for enriched route response and color mode toggle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:36:42 +01:00

2.4 KiB

Context

The Journal has passkey auth (primary) and magic link login (fallback). Magic links work in dev (auto-redirect) but in production the link is only logged to console — users never receive it. No email infrastructure exists.

Goals / Non-Goals

Goals:

  • Actually deliver magic link emails in production
  • Send a welcome email after registration
  • Provider-agnostic email interface (easy to swap providers)
  • HTML templates with plain-text fallback
  • Easy to add new email types in the future (just add a template + call)
  • Dev mode: still log to console / return devLink (no real emails)

Non-Goals:

  • Marketing emails or newsletters
  • Email preferences / unsubscribe (only transactional)
  • Inline CSS / complex email layouts (keep it simple text-focused)
  • Queuing or retry logic (provider handles delivery)

Decisions

D1: SMTP via nodemailer

Use nodemailer with SMTP — standard, provider-agnostic, works with any SMTP server. Single dependency: nodemailer npm package. Configured via SMTP_URL env var (e.g., smtp://user:pass@mail.example.com:587).

Alternative considered: Resend API. Simpler initial setup but creates vendor dependency. SMTP is a standard protocol that works with any provider and aligns with the self-hostable philosophy.

D2: Email module in apps/journal/app/lib/email.server.ts

Not a shared package — only the Journal sends emails. Keep it simple:

  • sendEmail(to, subject, html, text) — low-level sender
  • sendMagicLink(email, link) — template wrapper
  • sendWelcome(email, username) — template wrapper

In dev mode, sendEmail logs to console instead of calling Resend.

D3: Inline HTML templates

Simple functions returning HTML strings. No template engine. Each email type is a function: magicLinkTemplate(link), welcomeTemplate(username). Plain text generated by stripping tags.

D4: Sender domain: noreply@trails.cool

Requires DNS records (DKIM/SPF/DMARC) for deliverability. Add TXT records via Terraform or manually in Hetzner DNS.

Risks / Trade-offs

  • SMTP server required → Must provide an SMTP server. Can use any provider (Mailgun, Amazon SES, Postfix, etc.) or a simple relay.
  • DNS records required → Must add SPF/DKIM/DMARC records for deliverability.
  • No built-in retry → nodemailer doesn't queue. If SMTP is down, the send fails. Acceptable for current scale — welcome emails are fire-and-forget, magic links can be re-requested.