Skip to content

Newsletter

Newsletter is a per-website feature: enable on a site, get a subscriber list, a subscribe form on the live site, an unsubscribe page, and a compose-and-send UI in the operator portal. Built as a schema layer over an email provider (Resend today) so the platform owns the operator-facing contract while leaving deliverability, storage, and bounces to the provider.

Why a schema layer instead of owning the pipeline

Section titled “Why a schema layer instead of owning the pipeline”

The platform’s value-add is the operator experience, not email infrastructure. Resend already handles per-tenant audience storage, suppression, deliverability operations, bounces, and webhooks at scale. Building those ourselves is duplicate work with no operator-visible benefit.

What the platform owns:

  • Per-website provisioning glue (mint audience + push env + redeploy when enabled)
  • Site-side template bits (subscribe form, unsubscribe page, API proxies)
  • Operator-facing portal UI (subscriber list, campaign compose, send)
  • A thin platform API that proxies to Resend with auth + tenant scoping
  • The unsubscribe HMAC token (signed + verified server-side)

What the platform does NOT own:

  • Subscriber storage (Resend Audiences)
  • Campaign drafts / send infrastructure (Resend Broadcasts)
  • Suppression list machinery (contact.unsubscribed: true on Resend)
  • Bounce handling, IP warming, SPF/DKIM ops (Resend’s whole product)

Every route, CLI command, MCP tool, and portal handler that touches newsletter data goes through NewsletterProvider — an interface in packages/api/src/integrations/newsletter.ts. The current implementation is ResendNewsletterProvider. Swapping providers (SendGrid, Mailchimp, or owning the pipeline directly via AWS SES) means writing one new class against the same interface — no route, CLI, SDK, MCP, or portal changes.

The interface intentionally hides provider-internal ids behind opaque strings. A future implementation could translate between platform-issued ids and provider-internal ids (via a mapping table) without rippling.

StageTriggerWhat the platform addsResend still does
1. NowFirst implementationAdapter on ResendEverything
2. ~50 active tenantsNeed faster analytics than provider’s APIMirror subscribers + events to DDB (write-through cache)Sends, deliverability, suppression
3. ~500 tenantsWant cross-tenant insights, custom segmentation, drip campaignsOwn campaign-state DB, scheduling, segmentationJust SMTP + bounces
4. Only when math justifies (~5M emails/month)Pipeline cost > Resend billMigrate delivery to AWS SES with own IPs; Resend becomes fallbackNothing — platform owns it

The breakeven for owning the pipeline is roughly 5M emails/month at current Resend pricing ($0.0004/email) vs AWS SES ($0.0001/email). Until then, the operational complexity of running an MTA isn’t worth the per-email savings.

  1. Platform calls provider.createAudience(...) — mints a Resend audience scoped to the website slug
  2. Persists a newsletter service row on the website (externalId = audienceId, metadata = {audienceId, defaultFromEmail, enabledAt})
  3. Pushes env vars to the per-client Vercel project: VANTAGE_WEBSITE_ID + NEXT_PUBLIC_NEWSLETTER_ENABLED=1
  4. Triggers a Vercel redeploy
  5. ~60 seconds later: the per-client site renders the subscribe form (the <SubscribeForm> component reads the env flag); the unsubscribe page lives at /unsubscribe; subscribe + unsubscribe API routes proxy to the platform.
Operator
(writes campaign in portal)
Melbora portal UI
Melbora platform API
┌───────────┴───────────┐
│ │
▼ ▼
NewsletterProvider persisted on
(ResendProvider) website-services row
│ │
▼ │
Resend API │
(sends emails) │
│ │
▼ │
Subscribers ◀───────────────┘
(per-tenant audience)

The site-side subscribe + unsubscribe routes proxy through the platform too — the per-tenant template never talks to Resend directly. This keeps provider coupling in one place (the adapter) and avoids putting any full-access Resend credentials in the client repo.

The token in every campaign’s unsubscribe link is a stable HMAC over {websiteId, email, iat} signed with the shared platform OAuth-state secret. Format: <base64url-payload>.<base64url-signature>.

  • 90-day expiration enforced via iat (forwarded emails from 6 weeks ago still work; ancient tokens age out)
  • Constant-time signature comparison via timingSafeEqual
  • The same token serves both UI confirmation (the page POSTs it) and RFC 8058 one-click (Gmail/Yahoo POST it directly from the recipient’s mail client)
  • Gmail / Yahoo bulk-sender rules (Feb 2024 in effect): List-Unsubscribe + List-Unsubscribe-Post: List-Unsubscribe=One-Click headers added by the provider on every send; the platform’s /api/unsubscribe POST handler accepts that flow.
  • CAN-SPAM: unsubscribe link in every send, suppression honored, sender identifiable, physical mailing address sourced from the operator’s CMS business profile.
  • GDPR (EU recipients): unsubscribe always available, no dark patterns, double-opt-in is a future option (we default to single-opt-in for V1; operators add CAPTCHAs on the subscribe form for spam protection).
TierEmails / monthCost
Free3,000$0
Pro50,000$20/mo
Pro100,000$35/mo

Per-email rate ~$0.0004. At small-tenant scale (a few hundred subscribers, monthly send) the platform’s Resend bill stays in the free tier or low double-digits. The platform absorbs Resend cost today; per-tenant billing pass-through is a future operator-decision.

  • Operator workflow: Run a newsletter
  • API surface: POST /websites/:id/newsletter family (see the OpenAPI reference)
  • SDK: vc.newsletter.*
  • CLI: vantage newsletter ...
  • MCP: vantage_newsletter_* tools