The idea
A command-line tool for writers who want full ownership of their newsletter without paying a platform for the privilege. You write each issue as a markdown file in a local folder, run send with the issue filename, and the tool renders it to HTML using your saved template, resolves the subscriber list from a local SQLite database, and sends it in batches via Amazon SES or Postmark. Subscriber management (subscribe, unsubscribe, export) is done through subcommands, not a web UI.
Why build this
Newsletter platforms — Mailchimp, ConvertKit, Beehiiv — charge based on subscriber count and own your data. A creator with 2,000 subscribers pays $30–50/month for what is essentially: render some HTML, call an SMTP endpoint, store a list of emails. Amazon SES charges $0.10 per thousand emails. Postmark is similarly cheap. The actual cost for a 2,000-subscriber list is under $1/month. The only thing missing is the send infrastructure, and that is not complicated to build.
Writers increasingly want to work in tools they already use — Obsidian, Neovim, VS Code — rather than browser-based WYSIWYG editors. A CLI that speaks markdown fits directly into that workflow.
Stack sketch
- Language: Python, distributed as a single
pipx-installable package - Markdown rendering:
markdown-it-pywith ajinja2template wrapping the rendered HTML (header, footer, unsubscribe link with HMAC token) - Email delivery:
boto3for SES or the Postmark Python SDK, configurable via aconfig.tomlin the project folder - Subscriber store: SQLite via
sqlite-utils— one table with email, confirmed, created_at, unsubscribe_token - CLI framework:
typer(type-annotated Click wrapper that generates help text for free) - Template format: A single
template.htmlfile with{{ content }}and{{ unsubscribe_url }}placeholders; users edit it once and never think about it again - Unsubscribe handling: One-click unsubscribe URL points to a minimal Flask endpoint (single file, ~50 lines) the user deploys separately, or a Cloudflare Worker equivalent
Scope for v1
init— scaffold a project folder withconfig.toml, an example template, and an emptysubscribers.dbsubscriber add <email>andsubscriber remove <email>subcommands with a confirmation promptsubscriber import <csv>for migrating from another platformsubscriber list [--count]to inspect the current listsend <issue.md> [--dry-run]— renders to HTML, previews the subject line extracted from the markdown frontmatter (subject:field), then sends to the full list in configurable batch sizes (default 50/s to respect SES burst limits)preview <issue.md>— renders to a local HTML file and opens it in the default browser- Deliberately out: double opt-in confirmation flow, open/click tracking, bounce handling, web UI of any kind, scheduling
Where it could go
The unsubscribe handler is the weakest link in v1 — it requires the user to deploy a small web service just to process clicks. The cleanest follow-on is a serve subcommand that runs a local FastAPI instance, designed to sit behind Caddy or Nginx with a public URL, handling both unsubscribes and an optional web archive of past issues. This turns the CLI into a complete self-hosted publishing stack.
Bounce and complaint handling is the second priority. SES and Postmark both push delivery events to webhooks; wiring those events into the subscriber database (marking hard bounces as unsubscribed, flagging complaint addresses) is essential before a list grows past a few hundred subscribers. It is straightforward to implement once the web server exists.
Watch out for
Email deliverability is unforgiving: send from an unverified domain or skip SPF/DKIM setup and everything lands in spam regardless of how good the tool is. The README needs to walk through SES domain identity verification and DKIM signing step-by-step before it touches CLI installation; burying that in "prerequisites" will cause most users to fail silently and blame the tool.