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: trueon Resend) - Bounce handling, IP warming, SPF/DKIM ops (Resend’s whole product)
The adapter
Section titled “The adapter”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.
Scaling ladder
Section titled “Scaling ladder”| Stage | Trigger | What the platform adds | Resend still does |
|---|---|---|---|
| 1. Now | First implementation | Adapter on Resend | Everything |
| 2. ~50 active tenants | Need faster analytics than provider’s API | Mirror subscribers + events to DDB (write-through cache) | Sends, deliverability, suppression |
| 3. ~500 tenants | Want cross-tenant insights, custom segmentation, drip campaigns | Own campaign-state DB, scheduling, segmentation | Just SMTP + bounces |
| 4. Only when math justifies (~5M emails/month) | Pipeline cost > Resend bill | Migrate delivery to AWS SES with own IPs; Resend becomes fallback | Nothing — 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.
What enables when the operator toggles on
Section titled “What enables when the operator toggles on”- Platform calls
provider.createAudience(...)— mints a Resend audience scoped to the website slug - Persists a
newsletterservice row on the website (externalId = audienceId, metadata ={audienceId, defaultFromEmail, enabledAt}) - Pushes env vars to the per-client Vercel project:
VANTAGE_WEBSITE_ID+NEXT_PUBLIC_NEWSLETTER_ENABLED=1 - Triggers a Vercel redeploy
- ~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.
What gets sent where
Section titled “What gets sent where” 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.
Unsubscribe token format
Section titled “Unsubscribe token format”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)
Compliance posture
Section titled “Compliance posture”- Gmail / Yahoo bulk-sender rules (Feb 2024 in effect):
List-Unsubscribe+List-Unsubscribe-Post: List-Unsubscribe=One-Clickheaders added by the provider on every send; the platform’s/api/unsubscribePOST 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).
Cost model (Resend, today)
Section titled “Cost model (Resend, today)”| Tier | Emails / month | Cost |
|---|---|---|
| Free | 3,000 | $0 |
| Pro | 50,000 | $20/mo |
| Pro | 100,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.
Related references
Section titled “Related references”- Operator workflow: Run a newsletter
- API surface:
POST /websites/:id/newsletterfamily (see the OpenAPI reference) - SDK:
vc.newsletter.* - CLI:
vantage newsletter ... - MCP:
vantage_newsletter_*tools