Session: Email Subscription & RSS Feed Implementation
What We Built
A complete email subscription system for the portfolio blog with two distribution channels: daily email digest and RSS feed.
Core Architecture
Database Layer (Supabase):
- Isolated
portfolio_blogschema (separate from open-brain) containing subscriber data subscriberstable: email, unsubscribe token (unique per subscriber), verified flagnewsletter_sendsaudit log: tracks what was sent, when, to how many people, and which provider- RPC-secured stored procedures (functions in public schema, tables in isolated schema) — defense-in-depth against unintended data exposure
Email Layer (Brevo SMTP):
- Provider abstraction (factory pattern):
ResendEmailService,BrevoEmailService,MockEmailService— switch providers viaEMAIL_PROVIDERenv var - Daily cron fires at midnight UTC, fetches all posts published since last send, renders unique HTML per subscriber with their personal unsubscribe token
- Email template with responsive HTML, post excerpts, unsubscribe options (header + footer)
- Digest model: one daily email at midnight UTC (reduces fatigue vs. per-post notifications)
API Endpoints:
POST /api/subscribe— validate email, generate token, insert via RPCGET /api/unsubscribe?token=...— one-click unsubscribe via token (primary via List-Unsubscribe header)POST /api/send-newsletter— Vercel cron handler, Brevo auth-gatedPOST /api/send-unsubscribe-token— send user their token via email/unsubscribepage — self-service form (email entry + token paste)
RSS Feed:
- Already built (Astro RSS plugin) — automatically includes all non-draft blog posts
- Updated frequency: automatically includes all new posts
Security Decisions
The Good: Defense-in-depth instead of vendor-lock
- Data stored in isolated
portfolio_blogschema (invisible to open-brain queries by default) - All table access goes through RPC functions with
security definer+ explicitgrant executepermissions - No direct SQL table access from client code
- Public can only INSERT (signup); service_role can select/delete (cron + unsubscribe)
The Frustrating: Brevo Link Tracking
- Brevo aggressively wraps all links with click-tracking redirects
data-no-track="true"HTML attribute is supposed to disable tracking but doesn’t work reliably- Query parameters get stripped during redirect, breaking token-based unsubscribe links
- Solution: Removed clickable links from unsubscribe confirmations. Token shown as plain text; users paste it on
/unsubscribepage instead - Not ideal UX but eliminates vendor-dependency friction
Key Learnings
Production Discipline (Early Catch)
We created the subscription UI before the backend was ready, pushed it to main as draft-hidden. This taught us: partial features should stay on feature branches. Preview deployments exist for validation. Once merged to main, the code is semantically “live” even if hidden — bad habit to normalize.
Database Access Patterns
Three approaches to secure isolated schema data:
- Views in public schema (rejected) — single RLS failure exposes everything
- Direct table access (rejected) — relies on vendor’s RLS enforcement only
- RPC functions (selected) — layered defense: schema isolation + zero table discovery + explicit permission grants
The RPC route costs more boilerplate but gives you defense-in-depth. Worth it for PII.
Vendor Quirks as Constraints
When Brevo’s link tracking broke tokens, we had three choices:
- Spend more engineering effort on workarounds (diminishing returns)
- Accept the broken feature
- Redesign around the limitation
We chose #3. The token-paste flow is “fiddly but functional” — good enough for now, improvable later without rewriting core logic.
What’s Live Now
/subscribepage with email form + RSS link- Subscribe navigation links (header, homepage, footer, sidebar)
- Daily cron fires midnight UTC
- Email digests with unique unsubscribe tokens per subscriber
- List-Unsubscribe header (mail client native button)
- Self-service unsubscribe page (
/unsubscribe) - RSS feed (automatic via Astro)
What’s Next (Future Sessions)
- Verification emails: Double opt-in (send confirmation, require click) for explicit intent
- Brevo API instead of SMTP: More control over link tracking and templating
- Grafana metrics: Track subscription growth, unsubscribe rate, email delivery success
- Unsubscribe UX polish: Direct link in email once we solve the token issue
- Subscriber management UI: Let users update preferences without unsubscribing
The big win here: A boring, production-ready feature that works end-to-end. No vendor lock-in, layered security, and two distribution channels (email + RSS). Users have choices.