Skip to main content
Glama
robconery

big-mailer

by robconery

big-mailer 📬

Broadcasts, Drip-Sequenzen und Transaktions-E-Mails in einem einzigen Cloudflare Worker. Ein selbst gehosteter Ersatz für einen kostenpflichtigen ESP, bei dem die Liste, der Versand und die Interaktionsdaten bei dir bleiben.

CI License: MIT TypeScript Cloudflare Workers

Status: funktional vollständig und lokal lauffähig, noch nicht deployed. Es ist für einen einzelnen Betreiber gebaut und nicht mandantenfähig – bewusst so entworfen, nicht aus Versehen. Siehe Vor dem Deployment für die ehrliche Liste dessen, was noch fehlt.

Das big-mailer-Dashboard mit Consent, aufgeschlüsselt nach Scope

Das Dashboard nach dem Einspielen der Demo-Daten. Consent, nach Scope ist das Panel, das zählt: Zwei Personen haben eine einzelne Serie verlassen und sind weiterhin auf der Liste. Bei einem normalen ESP wären diese beiden Zahlen identisch.


Die Idee 💡

Jeder ESP behandelt Abmeldung als einen einzigen Schalter. Jemand beendet deine Onboarding-Serie, klickt auf „Abmelden", um die zu stoppen, und verlässt still deinen Newsletter für immer. Du erfährst es nie. Die Zahl geht einfach runter.

Hier ist Consent scoping-basiert. Eine Serie zu verlassen nimmt dich von genau dieser Serie. Den Newsletter zu verlassen, bricht keine Serie ab, in die du dich bewusst eingewählt hast. Nur eine explizite „von allem abmelden"-Aktion, ein harter Bounce oder eine Spam-Beschwerde entfernt jemanden vollständig.

Diese Asymmetrie ist der Grund, warum es das hier gibt, und alles andere im Codebase ist so angeordnet, dass sie nicht versehentlich kaputtgehen kann.


Related MCP server: Resend MCP Server

Ausführen 🚀

bun install
bun run db:migrate     # applies migrations to the local D1 database
bun run dev            # http://localhost:8787

Öffne http://localhost:8787 und klicke auf Demo-Daten einspielen: zwölf Personen, zwei aktive Serien, ein gesendeter Broadcast. Öffne dann den Postausgang, um die E-Mails zu lesen, die „rausgegangen" sind.

Nichts verlässt deinen Rechner. EMAIL_PROVIDER=console ist die lokale Standardeinstellung und schreibt vollständig gerenderte E-Mails in den In-App-Postausgang, statt sie zu versenden. Kein API-Key nötig und keine Möglichkeit, versehentlich eine echte Person zu mailen, während du herumprobierst.

Das ausprobieren, wofür es gedacht ist 🎯

  1. Abonnenten → jemanden auswählen → Deren Präferenzzentrum öffnen

  2. Füge ?scope=sequence:2 an diese URL an. So sieht ein Link innerhalb einer Sequenz-E-Mail aus.

  3. Klicke auf Nur diese Serie stoppen

  4. Gehe zurück zur Abonnenten-Seite: immer noch active, immer noch im Newsletter, aus genau einer Serie raus

  5. Consent zeigt alle, die eine einzelne Serie verlassen haben, im Vergleich zur (leeren) Liste der Personen, die komplett weg sind

Sende danach einen Broadcast, und sie werden ihn trotzdem erhalten. Das ist das gesamte Argument.

Sequenz-Verzögerungen sind in Tagen, und der erste Schritt ist standardmäßig 0 (kommt beim Beitritt an), während spätere Schritte standardmäßig 1 sind. Das bedeutet, eine eingespielte Serie wird nicht fertig, während du zusiehst – deshalb hat das Dashboard Uhr vorspulen (nur lokal): Es zieht jeden ausstehenden Schritt auf jetzt und führt einen Tick aus. Nutze es, und du siehst, wie Schritt 2 die Personen überspringt, die diese Serie verlassen haben.


Was hier drin ist 🗂

src/
  worker.tsx      fetch + scheduled + queue handlers — the whole entry point, 151 lines
  core/           domain logic: consent, sending, sequences, segments, rendering
  db/             Drizzle schema (24 tables) and the D1 client
  web/            server-rendered admin console (Hono + JSX, no frontend framework)
  api/            transactional send API, signup forms, media upload, bearer-key auth
  mcp/            MCP server — 93 tools, 4 resources, 4 prompts
  providers/      EmailProvider port + console and Resend adapters
  client/         the only browser JS in the project: the TipTap editor bundle
migrations/       drizzle-kit generated, applied by wrangler
docs/             problem brief, architecture, spec, and a decision log

Rund 16.000 Zeilen TypeScript. bun run typecheck deckt den Worker und das Browser-Bundle getrennt ab und ist sauber.


Architektur auf einen Blick 🧱

Cloudflare Workers · D1 (SQLite) über Drizzle · Queues für den Send-Fan-out · Cron-Trigger für die Terminierung · R2 für Medien · Hono + JSX serverseitig gerendertes Admin · Cloudflare Access für Auth · ein pluggbarer EmailProvider-Port mit console- und Resend-Adaptern.

Die Entscheidungen, die es wert sind, bekannt zu sein, und warum:

Jede messages-Zeile wird materialisiert, bevor auch nur ein Versand rausgeht. Ein Broadcast löst seine vollständige Empfängerliste im Voraus auf, schreibt eine Zeile pro beabsichtigtem Versand und verteilt erst dann an die Queue. Das macht einen Broadcast nach einem Absturz fortsetzbar, idempotent über Queue-Wiederholungen hinweg und im Nachhinein prüfbar. Empfänger erst zum Sendezeitpunkt lazy aufzulösen ist billiger und verwandelt jeden Fehler mitten im Broadcast in ein nicht wiederherstellbares Chaos.

Consent wird unmittelbar vor dem Provider-Aufruf erneut geprüft, nicht beim Einreihen in die Queue. Eine Queue kann Minuten nach der Erstellung der Nachricht zustellen, und jemand kann zwischenzeitlich opt-outen. Beim Einreihen zu prüfen würde sie trotzdem mailen.

Die Queue-Konkurrenz ist auf 6 festgepinnt. Ein Batch ist eine Provider-Anfrage, also ist Batch-Konkurrenz die Anfragenrate. Wenn sie nicht gesetzt ist, skaliert Cloudflare Queues auf 250 gleichzeitige Consumer hoch, begräbt Resends 10 req/s-Limit unter 429ern, verbrennt alle drei Wiederholungen und dead-lettert einwandfreie E-Mails. Sechs Batches à 100 lassen ~600 E-Mails/Sek. Spielraum, während sie unter dem Limit bleiben.

Die Unterdrückung ist nach E-Mail-Adresse geschlüsselt, nicht nach Abonnent. Transaktions- Empfänger und gebouncte Adressen haben oft gar keine Abonnenten-Zeile, also würde ein subscribers.status-Flag sie still übersehen.

Alles Beobachtbare ist eine Zeile in D1, niemals eine Log-Zeile. Worker-Logs laufen nach 3–7 Tagen ab. Ein Audit-Trail mit einer Woche Aufbewahrung ist keiner. mcp_calls zeichnet jede Agenten-Aktion auf, einschließlich der Ablehnungen; sync_runs zeichnet jeden Stripe-Abzug auf.

Die Admin-Konsole hat kein Passwort. Cloudflare Access beendet die Identität an der Edge, und src/web/auth.ts verifiziert das weitergeleitete JWT ordentlich: Signatur gegen das Live-JWKS des Teams (pro Isolate gecacht, mit erzwungenem Neufetch bei unbekannter Key-ID), alg auf RS256 festgepinnt, plus Audience, Issuer, exp und nbf. Das Vorhandensein des Headers beweist nichts und wird nie als Beweis behandelt. Falsch konfiguriert, failt die Middleware geschlossen und sperrt alle aus, auch dich. Das ist die richtige Fail-Richtung.

Der E-Mail-HTML-Renderer ist handgeschrieben (core/render-doc.ts) statt @tiptap/html zu verwenden, dessen Server-Einstiegspunkt happy-dom braucht und nicht in workerd läuft. Es stellte sich ohnehin als die bessere Antwort heraus: Der Walker inline-t jeden Stil (Gmail entfernt <style>) und erzeugt verschachtelte Tabellen für Buttons (Outlook ignoriert Padding auf <a>), was generische HTML-Serialisierung nicht tun würde.

Das vollständige Entscheidungsprotokoll, einschließlich der abgelehnten Alternativen und warum, steht in docs/MEMORY.md.


Drei unabhängige Scopes. Eine enge Wahl eskaliert nie zu einer weiten.

Scope

Gespeichert als

Wirkung

Sequenz

sequence_optouts-Zeile

Weg von genau dieser Serie. Alles andere läuft weiter.

Broadcast

subscribers.status

Weg vom Newsletter. Serien laufen weiter.

Global

suppressions-Zeile

Weg von allem. Die rechtliche Notluke.

Nur eine explizite „von allem abmelden"-Aktion, ein harter Bounce oder eine Beschwerde schreibt eine globale Unterdrückung.

Sequenz-Sends ignorieren bewusst status = 'unsubscribed', weil dieses Flag Broadcast-scoping hat: Jemand, der den Newsletter verlassen hat, bekommt trotzdem die Onboarding-Serie, die er angefordert hat. Transaktions-E-Mails (Belege, Downloads) ignorieren Marketing-Consent vollständig und werden nur durch eine tote Adresse oder eine Spam-Beschwerde blockiert. Ein Beleg ist kein Marketing, und ein abgemeldeter Kunde braucht trotzdem seinen Download.


Der Editor ✍️

Blockbasierter Rich-Text, TipTap v3, vanilla (kein React). Öffne den eingespielten Entwurf „Entwurf: alles, was der Editor kann", um das Ganze zu sehen.

  • / in einer Zeile → Blockmenü: Überschriften, Listen, Checkliste, Zitat, Code, Tabelle, Toggle, Trennlinie, Bild, YouTube, CTA-Button

  • @ → Personalisierungsfelder als echte Nodes, damit first_name nicht falsch geschrieben werden kann

  • Ziehe den Griff am linken Rand zum Umsortieren; Umschalt-Auswahl markiert mehrere Blöcke

  • Ziehe ein Bild per Drag-and-Drop hinein oder füge es ein → Upload zu R2, Einfügen, sobald die URL zurückkommt

  • Codeblöcke sind syntax-hervorgehoben (15 Sprachen, inkl. Ruby, Elixir, TS, SQL)

  • Text für das Bubble-Menü auswählen; einen Button auswählen und das Bubble-Menü wird zu dessen URL- und Farbwähler

Buttons und Merge-Tags sind benutzerdefinierte Nodes, die speziell für E-Mail gebaut wurden. Ein CTA rendert als verschachtelte Tabelle, und jeder Stil ist inline. Ein Merge-Tag ist eine Node statt rohem {{first_name}}-Text, weil ein Tippfehler im Rohtext „Hi {{frist_name}}" an die gesamte Liste mailt.

Inhalte werden als TipTap-JSON in body_json gespeichert. Legacy-Markdown in body_md rendert weiterhin und wird konvertiert, sobald du es im Editor öffnest. Es wird nichts in großen Mengen migriert, weil eine Massenmigration, die schiefgeht, das Archiv mitnimmt.

Das Client-Bundle ist ~226 KB gzipped und lädt nur auf den beiden Bildschirmen, die E-Mail verfassen. Alles andere in der Admin-Konsole ist serverseitig gerendert mit null JavaScript.

Es gibt einen Browser-Smoke-Test (bun run smoke, 33 Prüfungen), der echtes Chromium fährt, weil eine umbenannte Erweiterungsoption im Browser still scheitert und das Body-Feld einfach nie speichert. Nichts Serverseitiges kann das abfangen.


Von Claude Code aus steuern 🤖

Der Worker bedient einen MCP-Server unter POST /mcp/<secret>: 93 Tools, die den gesamten Mailer abdecken, sodass ein Agent Segmente schneiden, Broadcasts entwerfen und senden, Sequenzen bauen, Kampagnen-Performance lesen und Stripe abgleichen kann.

# 1. a path secret (this is what makes the endpoint exist at all)
openssl rand -hex 24                      # → put in .dev.vars as MCP_PATH_SECRET

# 2. an admin-scoped key — POST /seed prints one, or use apikey_create

# 3. point Claude Code at it
claude mcp add --transport http --scope local \
  --header "Authorization: Bearer $BIG_MAILER_KEY" \
  big-mailer "http://localhost:8787/mcp/$MCP_PATH_SECRET"

Drei Tore, das billigste zuerst. Ein unerratbares Pfad-Geheimnis, das in konstanter Zeit verglichen wird (ein Fehlschlag gibt 404 zurück, nicht 403, weil eine URL, die niemand erraten hat, aussehen sollte, als wäre da nichts), dann ein admin-scoped Bearer-Key (transaktionale send-Keys kommen nicht daran), dann Per-Tool-Guards. Jeder Aufruf landet in mcp_calls, einschließlich der Ablehnungen.

Irreversible Sends brauchen einen Preflight. broadcast_send weigert sich ohne ein Token von broadcast_preflight: einmal verwendbar, 10 Minuten Ablauf, ungültig gemacht durch jede Änderung am Inhalt oder an der Zielgruppe. Dasselbe für sequence_activate. Obendrein ist MCP_ALLOW_SEND in Produktion "false", sodass MCP alles lesen und entwerfen kann, aber keine E-Mail auf die Leitung bringen kann, bis du es bewusst umlegst. Es zurückzulegen ist der sofortige Ausschalter.

Agenten sollten bigmailer://conventions lesen, bevor sie Consent anfassen. Scoped-Unsubscribe ist nicht die Form, die irgendetwas erwartet, das auf normalen ESPs trainiert wurde, und es falsch zu machen ist genau das Versagen, das dieses Projekt verhindern sollte.


Stripe → Kampagnen-Attribution 💳

Setze STRIPE_SECRET_KEY (eingeschränkt, nur lesend auf charges/refunds/customers). Ein täglicher Cron um 09:17 UTC zieht neue Charges und schreibt jede dem letzten Attribution-Touch des Käufers gut, oder metadata.campaign, wenn die Charge eine trägt. Idempotent auf der Stripe-Charge-ID, sodass Wiederholungsläufe und überlappende Backfills harmlos sind.

stripe_sync_preview führt einen Trockenlauf aus, sales_unattributed ist die Arbeitsliste dessen, was die Heuristik nicht zuordnen konnte, und sync_runs_list beweist, dass der nächtliche Job tatsächlich läuft.


Befehle ▶️

bun run dev

Client-Bundle bauen, dann auf :8787 ausliefern

bun run watch:client

Editor-Bundle bei Änderung neu bauen (parallel zu dev)

bun run smoke

Browser-Smoke-Test des Editors. Braucht laufendes dev

bun run db:migrate

Migrationen auf lokales D1 anwenden

bun run db:generate

Migration nach Bearbeitung von src/db/schema.ts generieren

bun run db:studio

Drizzle Studio gegen die lokale Datenbank

bun run typecheck

Typprüfung des Workers und des Browser-Bundles getrennt

bun run deploy

Bauen, dann wrangler deploy --env production. Erst den Abschnitt unten lesen


Wirklich senden 📮

Kopiere .dev.vars.example nach .dev.vars, füge einen Resend-Schlüssel hinzu und setze EMAIL_PROVIDER=resend. Richte den Webhook von Resend auf /webhooks/resend ein, damit Bounces und Beschwerden korrekt unterdrückt werden. Ohne diesen Webhook werden ungültige Adressen nie unterdrückt und dein Senderuf verschlechtert sich stillschweigend – das ist der langsame Weg, die Zustellbarkeit für die gesamte Domain zu verlieren.

Geheimnisse gehören in .dev.vars (gitignored) oder per wrangler secret put. Niemals in wrangler.jsonc und niemals in .dev.vars.example.

⚠️ Vor dem Deployment

Noch nicht deployed, und zwischen hier und live gibt es echte Einrichtungsschritte:

  • DEV_AUTH_BYPASS=true steht in den Vars der obersten Ebene von wrangler.jsonc, damit die App lokal ausführbar ist. --env production setzt es auf false. Ein nacktes wrangler deploy veröffentlicht eine unauthentifizierte Admin-Konsole, weshalb bun run deploy fest --env production verwendet. Arbeite nicht dagegen.

  • database_id ist ein Platzhalter. Erstelle eine echte D1-Datenbank mit wrangler d1 create.

  • Erstelle den R2-Bucket (big-mailer-media) und die beiden Queues (big-mailer-send, big-mailer-dlq). Queues benötigen den kostenpflichtigen Workers-Plan für 5 $/Monat.

  • Setze PUBLIC_URL auf den echten Host. Er wird in Tracking-Links und Bild-URLs zum Zeitpunkt des Sendens eingebacken, sodass ein falscher Wert dauerhaft kaputte E-Mails an bereits zugestellte Nachrichten liefert. Das kann im Nachhinein nicht korrigiert werden.

  • MCP_PATH_SECRET muss ein echtes Geheimnis sein (wrangler secret put), keine Var. Wenn nicht gesetzt, antwortet der MCP-Endpunkt mit 404, was der sichere Standard ist. Aktiviere es bewusst.

  • Cloudflare Access benötigt eine Allow-App und mehrere Bypass-Apps. Wenn du den gesamten Hostnamen mit einer einzigen Allow-Richtlinie schützt, schützt das auch den Tracking-Pixel, die Anmeldeformulare, das Präferenzzentrum, die Webhooks und MCP, was bedeutet, dass jeder Tracking-Pixel in jeder E-Mail, die du sendest, dauerhaft zu einem Anmeldebildschirm umleitet – für bereits zugestellte Nachrichten. Access matcht zuerst den spezifischsten Pfad, daher benötigen /t, /f, /p, /api, /webhooks und /mcp jeweils ihre eigene Bypass-App. Jede davon trägt ihre eigene Authentifizierung oder ist von Natur aus öffentlich.

  • Keine serverseitige Testsuite. bun run smoke deckt den Editor ab. docs/SPEC.md ist als nummerierte, testbare Anforderungen geschrieben, die so gestaltet sind, dass sie ausführbar gemacht werden können.


Dokumentation 📚

docs/PROJECT.md

Das Problem, für wen es ist und was explizit ausgeschlossen ist

docs/ARCHITECTURE.md

Systemdesign, Datenmodell und die Sendepipeline

docs/SPEC.md

Nummerierte Verhaltensanforderungen. Die Referenz für das beabsichtigte Verhalten

docs/MEMORY.md

Entscheidungslog: Was gewählt wurde, was verworfen wurde und warum


Mitwirken 🤝

Fehlerberichte, Korrektheitskorrekturen und Korrekturen für E-Mail-Client-Rendering sind sehr willkommen. Multi-Tenancy, ein Drag-and-Drop-Builder und selbst betriebenes SMTP sind bewusst ausgeschlossen. Siehe CONTRIBUTING.md, bevor du einen PR eröffnest, und SECURITY.md, wenn du eine Schwachstelle gefunden hast (bitte melde sie privat, nicht als Issue).

Forking ist ausdrücklich erwünscht. Das Projekt ist klein genug, um es vollständig zu lesen und dein eigenes zu machen. Die Teilnahme unterliegt dem Code of Conduct.

Lizenz 📄

MIT © Rob Conery

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    C
    quality
    B
    maintenance
    MCP server that exposes the complete Libredesk REST API (54 endpoints) as tools, enabling natural language management of conversations, contacts, agents, teams, and more for the open-source customer support desk.
    54
    11
    3
    MIT
  • A
    license
    A
    quality
    F
    maintenance
    An MCP server for the Resend email API, enabling AI assistants to send emails, manage contacts, audiences, and domains through natural language.
    18
    44
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Remote MCP server for the Transmit email platform, enabling email sending, contact management, template and campaign operations via natural language.
  • F
    license
    C
    quality
    D
    maintenance
    Comprehensive MCP server for Mailchimp Marketing API v3.0 with over 104 tools and 15+ React UI apps, enabling management of campaigns, audiences, ecommerce, automations, reports, and more via natural language.
    100
    1

View all related MCP servers

Related MCP Connectors

  • GibsonAI MCP server: manage your databases with natural language

  • Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.

  • Markdown-first MCP server for Notion API with 8 composite tools and 39 actions.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/robconery/big-mailer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server