ADR-0090: Newsletter Console — Operator Page, Subscriber Management & Tracked Broadcasts
Status: Accepted Date: 2026-06-26 Author: @johalputt Relates to: ADR-0068 (VayuOS), migration 015 (newsletter subscribers)
Context
VayuPress has had a working newsletter backend since migration 015: a
double-opt-in subscriber store and HTTP endpoints to subscribe, confirm,
unsubscribe, list, and broadcast. But the VayuOS sidebar's Newsletter item
linked to /os/newsletter, for which no page handler or route existed — the
link dead-ended (falling through to the catch-all /os/* redirect). Subscriber
management and broadcasts were only reachable via API-key JSON endpoints, with
no operator UI, no audience insight, no way to delete a record from the browser,
and no record of what broadcasts had been sent or how they performed.
Decision
Build a first-class Newsletter console at /os/newsletter, mirroring the
Members console pattern (server-rendered within the strict-CSP VayuOS shell,
behaviour in a same-origin nonce-gated JS file, all dynamic values escaped).
-
A real page.
handleOSNewsletterrenders audience-health stat cards (total / active / pending double-opt-in / unsubscribed / 30-day new / confirmation rate), a dependency-free growth sparkline (reusingosSparkline), a broadcast composer, a broadcast-history table, and a subscriber table. -
Subscriber management. The store gains
Stats,List(filter, search, limit)across all states,Delete(GDPR erasure / spam cleanup),GrowthByDay, andExportCSV. The table supports client-side segment tabs and instant search; delete and CSV export are wired to session-authed endpoints. -
Tracked broadcasts. A new
newsletter_broadcaststable (migration 044) persists each send with its audience size and final sent/failed tallies. The console exposes a composer with a send-test action and a one-click broadcast that records the run and delivers in the background, marking the record complete with the tallies. This replaces the previous fire-and-forget broadcast that left no trace beyond a log line. -
Session-authed surface. New
/os/api/newsletter/*routes (stats,subscribers,broadcasts,export.csv, delete,test,broadcast) sit underrequireSessionOrAPIKeywith CSRF on writes, so a browser operator never needs an API key — matching the Members console. -
SMTP awareness. The console surfaces a clear banner and disables the send actions when SMTP is unconfigured, while signups and confirmations keep working (the mailer remains a safe no-op).
Guards
internal/newsletter/console_test.gocoversStatssegmentation,Listfilter + case-insensitive search,Delete,ExportCSVshape,GrowthByDaybucketing, and the broadcast create/finish/list lifecycle.
Consequences
- Migration
044-newsletter-broadcastsis additive and backward-compatible; it only adds a history table. The down migration drops that table. - The newsletter feature is now fully operable from the browser, with audience insight and an auditable broadcast history.
- Delivery remains sovereign: plain-SMTP via the Go standard library, no hosted sender, no third-party SDK; every broadcast still appends an unsubscribe link.