Email subscriber management powered by Cloudflare's send_email binding. SQL-backed storage, queue-powered campaigns, built-in admin dashboard — zero external services.
Built on Cloudflare Workers, Durable Objects (SQL), Queues, and Email Routing. Everything runs on Cloudflare. No Mailgun. No SendGrid. No API keys to rotate.
import { optkit } from 'optkit';
const kit = optkit({
do: env.SUBSCRIBERS_DO,
queue: env.EMAIL_QUEUE,
email: env.SEND_EMAIL, // Cloudflare send_email binding
senderEmail: '[email protected]',
senderName: 'My Newsletter',
});
await kit.optIn('[email protected]');
await kit.sendCampaign({ subject: 'News', html: '<h1>Hello!</h1>' });| Feature | OptKit | Typical SaaS |
|---|---|---|
| Email sending | Cloudflare send_email — no API keys |
Third-party API + secrets |
| Storage | Durable Object SQL — colocated, fast | External DB or KV hacks |
| Campaigns | Queue-powered — 10K+ emails, no timeouts | Hope your function doesn't timeout |
| Admin UI | One-line Hono middleware | Build your own or pay for dashboards |
| Cost | Workers free tier gets you far | $20+/mo before you send anything |
| Vendor lock | It's just SQL + email. Portable. | Good luck migrating |
- A Cloudflare account with Email Routing enabled on your domain
- A verified sender address (must be on a domain with Email Routing active)
- Workers, Durable Objects, and Queues enabled
npm install optkit hono mimetextimport { Hono } from 'hono';
import { optkit, OptKitDO, processCampaignBatch } from 'optkit';
import type { QueueMessage } from 'optkit';
import { adminUI } from 'optkit/admin';
import { basicAuth } from 'hono/basic-auth';
// Re-export the Durable Object class
export { OptKitDO };
interface Env {
SUBSCRIBERS_DO: DurableObjectNamespace;
EMAIL_QUEUE: Queue;
SEND_EMAIL: { send(message: EmailMessage): Promise<void> };
ADMIN_USER: string;
ADMIN_PASS: string;
}
const app = new Hono<{ Bindings: Env }>();
app.post('/subscribe', async (c) => {
const kit = optkit({
do: c.env.SUBSCRIBERS_DO,
queue: c.env.EMAIL_QUEUE,
email: c.env.SEND_EMAIL,
senderEmail: '[email protected]',
senderName: 'My Newsletter',
});
const { email } = await c.req.json();
const result = await Effect.runPromise(kit.optIn(email));
return c.json(result);
});
// Admin dashboard — one line
const auth = basicAuth({ username: 'admin', password: 'secret' });
app.get('/admin', auth, adminUI);
export default {
fetch: app.fetch,
// Queue consumer — handles campaign email batches
async queue(batch: MessageBatch<QueueMessage>, env: Env) {
await processCampaignBatch(batch, {
do: env.SUBSCRIBERS_DO,
queue: env.EMAIL_QUEUE,
email: env.SEND_EMAIL,
senderEmail: '[email protected]',
senderName: 'My Newsletter',
});
},
};OptKit uses Cloudflare's native send_email binding — the same infrastructure that powers Email Routing. Your Worker sends email directly through Cloudflare's edge. No SMTP servers, no third-party APIs.
┌──────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────┐
│ optkit │───▶│ Queue batch │───▶│ send_email │───▶│ Inbox │
│ .sendCampaign │ (50/batch) │ │ (CF edge) │ │ │
└──────────┘ └──────────────┘ └───────────────┘ └──────────┘
Sender requirements:
- The sender address must be on a domain where you have Email Routing active
- For unrestricted sending, use a binding with no
destination_address/allowed_destination_addresses - For targeted use (e.g. admin notifications only), use
destination_addressto lock the binding
const kit = optkit({
do: env.SUBSCRIBERS_DO,
queue: env.EMAIL_QUEUE,
email: env.SEND_EMAIL, // optional: Cloudflare send_email binding
senderEmail: '[email protected]', // required if email binding is set
senderName: 'My Newsletter', // optional: display name in From header
adminEmail: '[email protected]', // optional: get notified on new subs
});
// Subscribers
await Effect.runPromise(kit.optIn(email));
await Effect.runPromise(kit.optOut(email));
const sub = await Effect.runPromise(kit.get(email));
const list = await Effect.runPromise(kit.list({
status: 'active',
search: '@gmail',
limit: 100,
}));
// Campaigns (queued, fault-tolerant)
const campaign = await Effect.runPromise(kit.sendCampaign({
subject: 'Weekly Update',
html: '<h1>Hi</h1>',
}));
const status = await Effect.runPromise(kit.getCampaign(campaign.id));import { adminUI } from 'optkit/admin';
import { basicAuth } from 'hono/basic-auth';
app.get('/admin', basicAuth({ username: env.ADMIN_USER, password: env.ADMIN_PASS }), adminUI);Full dashboard with search, filters, pagination. Tailwind-styled, server-rendered.
Use the API directly from your own frontend:
const res = await fetch('/api/subscribers');
const { subscribers, total, active } = await res.json();const kit = optkit({
do: env.SUBSCRIBERS_DO,
queue: env.EMAIL_QUEUE,
email: env.SEND_EMAIL,
senderEmail: '[email protected]',
templates: {
optIn: (email) => ({
subject: 'Welcome!',
html: `<p>Thanks ${email}!</p>`,
text: `Thanks ${email}!`,
}),
optOut: (email) => ({
subject: 'Goodbye',
html: '<p>Unsubscribed.</p>',
}),
newSubscriber: (email) => ({
subject: 'New sub!',
html: `<p>${email} joined</p>`,
}),
},
});Don't need email sending? Skip the binding — OptKit works as a pure subscriber store:
const kit = optkit({
do: env.SUBSCRIBERS_DO,
queue: env.EMAIL_QUEUE,
// no email binding — subscriber management only
});
await Effect.runPromise(kit.optIn('[email protected]'));
await Effect.runPromise(kit.list({ status: 'active' }));- Durable Object (SQL) — Single global DO stores all subscribers and campaign state. SQLite under the hood. No external database.
- Queues — Campaign sends are batched (50 emails per message) and processed via Queue consumer. Automatic retries on failure.
- Email Routing —
send_emailbinding sends through Cloudflare's edge. MIME messages built with mimetext. - Effect — All operations return
Effecttypes with typed errors (InvalidEmail,AlreadySubscribed,EmailSendError,DatabaseError).
MIT