Overview
Add a newsletter and marketing email plugin that manages subscriber lists, campaigns, and transactional sends — without requiring a separate SaaS subscription for basic use cases. Built on top of a pluggable email adapter (Resend, Mailgun, SMTP), so consumers can bring their own sending infrastructure.
Deep integration with the Blog plugin makes this especially powerful: one-click "send this post as a newsletter" is a first-class feature.
Core Features
Subscriber Management
Campaigns
Blog Integration
Analytics
Schema
import { createDbPlugin } from "@btst/stack/plugins/api"
export const newsletterSchema = createDbPlugin("newsletter", {
subscriber: {
modelName: "subscriber",
fields: {
email: { type: "string", required: true },
name: { type: "string", required: false },
status: { type: "string", defaultValue: "subscribed" }, // "subscribed" | "unsubscribed" | "bounced"
tags: { type: "string", required: false }, // JSON array
confirmedAt: { type: "date", required: false },
createdAt: { type: "date", defaultValue: () => new Date() },
},
},
campaign: {
modelName: "campaign",
fields: {
subject: { type: "string", required: true },
fromName: { type: "string", required: true },
fromEmail: { type: "string", required: true },
replyTo: { type: "string", required: false },
body: { type: "string", required: true }, // HTML or Markdown
status: { type: "string", defaultValue: "draft" },
scheduledAt: { type: "date", required: false },
sentAt: { type: "date", required: false },
createdAt: { type: "date", defaultValue: () => new Date() },
updatedAt: { type: "date", defaultValue: () => new Date() },
},
},
send: {
modelName: "send",
fields: {
campaignId: { type: "string", required: true },
subscriberId: { type: "string", required: true },
status: { type: "string", defaultValue: "pending" }, // "pending" | "delivered" | "opened" | "clicked" | "bounced"
sentAt: { type: "date", required: false },
},
},
})
Plugin Structure
src/plugins/newsletter/
├── db.ts
├── types.ts
├── schemas.ts
├── query-keys.ts
├── client.css
├── style.css
├── api/
│ ├── plugin.ts # defineBackendPlugin — subscriber, campaign, send endpoints
│ ├── getters.ts # listSubscribers, getCampaign, getCampaignStats
│ ├── mutations.ts # createSubscriber, sendCampaign, unsubscribe
│ ├── query-key-defs.ts
│ ├── serializers.ts
│ ├── email-adapter.ts # EmailAdapter interface (send(to, subject, html))
│ └── index.ts
└── client/
├── plugin.tsx # defineClientPlugin — admin routes
├── overrides.ts # NewsletterPluginOverrides
├── index.ts
├── hooks/
│ ├── use-newsletter.tsx # useSubscribers, useCampaigns, useCampaignStats
│ └── index.tsx
└── components/
├── subscribe-form.tsx # Embeddable opt-in form component
└── pages/
├── subscribers-page.tsx
├── subscribers-page.internal.tsx
├── campaigns-page.tsx
├── campaigns-page.internal.tsx
├── edit-campaign-page.tsx
├── edit-campaign-page.internal.tsx
└── campaign-stats-page.tsx
Email Adapter Interface
export interface EmailAdapter {
send(options: {
to: string | string[]
subject: string
html: string
from: string
replyTo?: string
}): Promise<{ messageId?: string }>
}
// Built-in adapters (thin wrappers):
export function resendAdapter(apiKey: string): EmailAdapter
export function mailgunAdapter(apiKey: string, domain: string): EmailAdapter
Routes
| Route |
Path |
Description |
subscribers |
/newsletter/subscribers |
Subscriber list + import/export |
campaigns |
/newsletter/campaigns |
Campaign list |
editCampaign |
/newsletter/campaigns/:id |
Campaign editor + send controls |
newCampaign |
/newsletter/campaigns/new |
New campaign |
campaignStats |
/newsletter/campaigns/:id/stats |
Per-campaign analytics |
Subscribe Form Component
An embeddable component for public-facing subscription forms — not a full plugin route:
import { SubscribeForm } from "@btst/stack/plugins/newsletter/client"
// Drop into any page — hits the newsletter API directly
<SubscribeForm
apiBaseURL="https://example.com"
apiBasePath="/api/data"
tags={["blog-sidebar"]}
onSuccess={() => toast("You're subscribed!")}
/>
Blog Integration
On edit-post-page.internal.tsx, a "Send as newsletter" action:
- Pre-fills a new campaign with
subject = post.title, body = post.content
- Navigates to
/newsletter/campaigns/new?fromPostId=...
Hooks
newsletterBackendPlugin({
emailAdapter: resendAdapter(process.env.RESEND_API_KEY!),
onBeforeSubscribe?: (email, ctx) => Promise<void> // throw to reject
onAfterSubscribe?: (subscriber, ctx) => Promise<void>
onBeforeUnsubscribe?: (subscriber, ctx) => Promise<void>
onAfterSend?: (campaign, ctx) => Promise<void>
})
Consumer Setup
// lib/stack.ts
import { newsletterBackendPlugin } from "@btst/stack/plugins/newsletter/api"
import { resendAdapter } from "@btst/stack/plugins/newsletter/api"
newsletter: newsletterBackendPlugin({
emailAdapter: resendAdapter(process.env.RESEND_API_KEY!),
})
// lib/stack-client.tsx
import { newsletterClientPlugin } from "@btst/stack/plugins/newsletter/client"
newsletter: newsletterClientPlugin({
apiBaseURL: "",
apiBasePath: "/api/data",
siteBasePath: "/pages",
queryClient,
})
Non-Goals (v1)
- Visual drag-and-drop email builder (use Markdown/HTML body)
- Automated drip sequences / workflows
- Subscriber scoring / engagement scoring
- Bounce handling webhooks (handled by adapter)
- SMS / push channels
Plugin Configuration Options
| Option |
Type |
Description |
emailAdapter |
EmailAdapter |
Sending backend (Resend, Mailgun, SMTP) |
doubleOptIn |
boolean |
Require confirmation email (default: false) |
unsubscribeBaseURL |
string |
Base URL for unsubscribe links |
hooks |
NewsletterPluginHooks |
Lifecycle hooks |
Documentation
Add docs/content/docs/plugins/newsletter.mdx covering:
- Overview — subscriber management + campaign sending, pluggable email adapter
- Setup —
newsletterBackendPlugin with adapter, newsletterClientPlugin
- Email adapters — Resend, Mailgun examples; custom adapter interface
- Embeddable subscribe form —
<SubscribeForm> usage
- Blog integration — "Send as newsletter" workflow
- Schema reference —
AutoTypeTable for config + hooks
- Double opt-in — how to enable + confirmation email flow
Related Issues
Overview
Add a newsletter and marketing email plugin that manages subscriber lists, campaigns, and transactional sends — without requiring a separate SaaS subscription for basic use cases. Built on top of a pluggable email adapter (Resend, Mailgun, SMTP), so consumers can bring their own sending infrastructure.
Deep integration with the Blog plugin makes this especially powerful: one-click "send this post as a newsletter" is a first-class feature.
Core Features
Subscriber Management
subscribed/unsubscribed/bounced)Campaigns
draft→scheduled→sending→sentBlog Integration
Analytics
Schema
Plugin Structure
Email Adapter Interface
Routes
subscribers/newsletter/subscriberscampaigns/newsletter/campaignseditCampaign/newsletter/campaigns/:idnewCampaign/newsletter/campaigns/newcampaignStats/newsletter/campaigns/:id/statsSubscribe Form Component
An embeddable component for public-facing subscription forms — not a full plugin route:
Blog Integration
On
edit-post-page.internal.tsx, a "Send as newsletter" action:subject = post.title,body = post.content/newsletter/campaigns/new?fromPostId=...Hooks
Consumer Setup
Non-Goals (v1)
Plugin Configuration Options
emailAdapterEmailAdapterdoubleOptInbooleanfalse)unsubscribeBaseURLstringhooksNewsletterPluginHooksDocumentation
Add
docs/content/docs/plugins/newsletter.mdxcovering:newsletterBackendPluginwith adapter,newsletterClientPlugin<SubscribeForm>usageAutoTypeTablefor config + hooksRelated Issues