A self-hosted newsletter application you buy once and run on your own server, sending through Amazon SES. Lists, campaigns, autoresponders and reports.
Build me a newsletter application that replaces Sendy: lists, campaigns, autoresponders and reports, sending through a bulk provider at cost. Read this first: Sendy is a one-off purchase for about the price of a dinner, with no monthly bill, and it works. That makes it one of the weakest cases for building your own on this list. Build it if you want the schema to be yours, if you want to send through something other than the one provider it targets, or if you enjoy the work — but the honest saving is close to zero. STACK - Node 20+ with Fastify, server-rendered HTML - SQLite through better-sqlite3, WAL mode - A bulk sending provider through SMTP or its API, with the credentials in .env - A worker for sending, separate from the web process - Caddy in front THE DATA MODEL - lists: id, name, from_name, from_email, reply_to, confirm_required, welcome_campaign_id, created_at - subscribers: id, list_id, email, name, fields_json, status, confirmed_at, unsubscribed_at, bounced_at, complained_at, source, ip_hash, consent_text, consent_at, created_at - segments: id, list_id, name, rules_json — evaluated at send time, never stored as a member list - campaigns: id, list_id, segment_id, subject, preheader, body_html, body_text, status, scheduled_for, sent_at, recipient_count, from_name, from_email - deliveries: id, campaign_id, subscriber_id, status, sent_at, opened_at, first_clicked_at, bounced_at, bounce_kind, complained_at — one row per person per campaign, which is what makes a resume exact - clicks: id, delivery_id, link_id, at; links: id, campaign_id, url, position - autoresponders: id, list_id, trigger_kind, offset_minutes, subject, body_html, body_text, is_active - autoresponder_sends: autoresponder_id, subscriber_id, sent_at — so nobody gets the same one twice - Everything append-only where it records something that happened SENDING, WHICH IS THE PART THAT MUST NOT GO WRONG - The worker claims a batch of deliveries with a lease, sends, and writes the result. A crash mid-campaign resumes exactly where it stopped - One delivery row per subscriber, written before the send. That is what makes the resume exact rather than approximate - Idempotent per subscriber per campaign: sending twice is impossible because the row already exists - Rate limited to what the provider accepts, with backoff on their throttling responses - A test send to yourself required before any campaign can go out, enforced in the interface - A confirmation step showing the recipient count and the segment, because the mistake everybody makes once is sending to the wrong list DELIVERABILITY, WHICH IS THE ACTUAL DIFFICULTY - SPF, DKIM and DMARC on your sending domain, with the records written out in the README - A dedicated subdomain for sending, warmed slowly. A new domain sending twenty thousand messages on day one goes to spam and stays there - Bounces and complaints consumed from the provider's webhook or notification topic: a hard bounce unsubscribes immediately, a soft bounce is counted and unsubscribes after several, a complaint unsubscribes permanently and is never mailed again under any circumstance - Watch the complaint rate. Above a fraction of a percent the provider suspends the account, and that is the failure that ends the project - List hygiene: suppress anyone who has not opened anything in a very long time, and say so in the interface. A list full of dead addresses is what destroys a sender reputation - A global suppression list across every list, so an unsubscribe means unsubscribed THE MESSAGE - HTML built with tables and inline styles, because that is what mail clients render, and a plain-text alternative generated from the same source rather than written twice - Personalisation fields with a default for a missing value, so nobody is greeted as 'Hi ,' - A preview at phone and desktop width, and a preheader with its length shown - Link tracking through your own domain, switchable off per campaign - One-click unsubscribe with no login, plus the List-Unsubscribe and List-Unsubscribe-Post headers, which the large providers now effectively require for bulk senders SIGNUP AND CONSENT - A form and an embeddable one, with a honeypot and a rate limit per address hash - Double opt-in available and recommended, with the consent sentence and its timestamp stored exactly as the person saw it - Import from CSV with a mapping step, a dry run, and a required declaration of where the addresses came from. Importing a purchased list is how a sending account is closed in a week AUTORESPONDERS - Triggered by subscribing, by a date field, or by an offset from another autoresponder - The worker is idempotent and records each send, so a restart never repeats one - Pausing a sequence does not lose anyone's position REPORTS - Sent, delivered, opened, clicked, bounced, complained, unsubscribed, per campaign and over time - Open rate labelled as the unreliable number it is, because privacy proxies pre-fetch images - Clicks per link, which is the number that means something - Everything computed from rows at query time OPERATIONS - .env: DATABASE_PATH, BASE_URL, PROVIDER credentials, FROM_ADDRESS, SESSION_SECRET, HASH_SALT - Migrations on boot, each once - Nightly backup off the machine, restore script — this holds your list, which is the asset - Health endpoint checking the provider connection and the bounce feed WHAT MATTERS MOST Per-subscriber delivery rows and the bounce handling. Build the resumable send and the automatic suppression first, then send to a few thousand real addresses and watch the complaint rate. Everything else here is an editor; those two are what keep you able to send at all. Give me the repository, migrations, .env.example, the signup embed, and a README with deploy steps behind Caddy, the DNS records, and a warm-up schedule.
What you lose
- A finished application for the price of a dinner, with no monthly bill at all
- Autoresponders, segments and reports already built and debugged
- Bounce and complaint handling wired to SES for you
If you would rather not build
- Sendy itself, which is genuinely cheap
What it costs
read from their page 17 Aug 2026
| Plan | Billed monthly | Billed yearly | Last read |
|---|---|---|---|
| — | $5.75/mo | — | 17 Aug 2026 |
Their pricing page is where these came from. Seeing a different price? Tell us.
The escape hatch
open source · no votes, no paid placement
Why this verdict
our own opinion · changed only by a person
76/100
Verdict yes at 76, though the honest advice is that a $69 licence is hard to beat on cost. Build for ownership, not savings.
History
tracked since 10 Aug 2026 · nothing is ever overwritten
Questions about Sendy
answered from the record above
Is Sendy free?
No — the plan we track is $5.75 a month. A one-off licence of $69 rather than a subscription; the figure shown spreads that over a year for comparison, and you still pay Amazon SES for sending.
Can you replace Sendy by building your own?
YES. Replaceable in one session with an AI coding agent. Replacement score 76 out of 100, build time one session. Read what you lose before you decide.
How much does Sendy cost?
$5.75 a month on One-off licence — $69 a year. Recorded 9 Aug 2026.
What do you lose by replacing Sendy?
A finished application for the price of a dinner, with no monthly bill at all; Autoresponders, segments and reports already built and debugged; Bounce and complaint handling wired to SES for you. If any of those carry weight for you, keep paying.
Is there an open-source alternative to Sendy?
Yes: Listmonk, Keila. The prompt on this page is for when you want it your way instead.
Related entries
same category first, most replaced first
Every week, something stops being worth paying for.
New verdicts, prices that moved, entries added. One email a week. Unsubscribe in one click. Nothing is being sent yet — your address is kept here, and the first issue is the first thing it is used for.
free forever · no tracking pixel · stored here, never passed to anyone

