-
Notifications
You must be signed in to change notification settings - Fork 20
Open
Labels
enhancementNew feature or requestNew feature or request
Description
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
- Subscriber CRUD (email, name, status:
subscribed/unsubscribed/bounced) - Opt-in confirmation flow (double opt-in support via lifecycle hooks)
- Unsubscribe link auto-appended to all outgoing emails
- Subscriber tags/segments for targeted sends
- CSV import/export
Campaigns
- Campaign CRUD (subject, from name, from email, reply-to, body)
- Rich text + Markdown email body editor (reuse CMS markdown editor)
- "Send test email" to a single address
- Schedule campaign for future send
- Campaign status:
draft→scheduled→sending→sent
Blog Integration
- "Send as newsletter" button on blog post edit page
- Auto-populate campaign subject + body from post title + content
Analytics
- Delivery count, open rate (via tracking pixel), click rate (via redirect links)
- Per-campaign stats dashboard
- Subscriber growth chart
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): EmailAdapterRoutes
| 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 —
newsletterBackendPluginwith adapter,newsletterClientPlugin - Email adapters — Resend, Mailgun examples; custom adapter interface
- Embeddable subscribe form —
<SubscribeForm>usage - Blog integration — "Send as newsletter" workflow
- Schema reference —
AutoTypeTablefor config + hooks - Double opt-in — how to enable + confirmation email flow
Related Issues
- Job Board Plugin #58 Job Board Plugin
- Analytics Plugin #74 Analytics Plugin
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request