The ConvertKit replacement you already built
Private deliverable. Enter the hub password to continue.
That's not right. Try again.
The ConvertKit replacement you already built
It does double opt-in, one-click unsubscribe, a six-email drip and a provable consent log. It is about 700 lines and it is already live.
You asked how to build what you would be missing if you sent newsletters from your own Amazon SES instead of paying for Kit, the platform that used to be called ConvertKit.
The honest answer is that you mostly do not have to build it. You already did. It is running on testosteroneinwomen.com, it was deployed on July 25 and moved from Resend to SES the next day, and it does the compliance-critical parts properly. This page is what it does, why the tricky decisions in it are the right ones, and what it would take to put it on your other sites.
What it is
Four moving parts, no third-party email platform anywhere:
| Part | What it does |
|---|---|
| Cloudflare Worker | Handles signup, confirm, unsubscribe and health, and runs the drip on a cron trigger |
| Cloudflare D1 | Two tables: one row per subscriber, plus an append-only event log |
| Amazon SES v2 | Actually delivers the mail, from a verified domain in us-east-1 |
| A build step | Turns six markdown files into the six sequence emails, text and HTML |
Roughly 700 lines of JavaScript across four files. No SDK, no framework, no dependency on anything that can raise its prices.
The honest cost picture
I want to give you this straight rather than make your own build look better than it is.
At your current list size, Kit would cost you nothing. Their free Newsletter tier now covers up to 10,000 subscribers with unlimited sends. So the savings argument, which is the usual reason people self-host, does not apply to you yet.
Where it does apply:
| Subscribers | Kit Creator | This backend |
|---|---|---|
| Up to 10,000 | Free | Pennies |
| 10,000 | $139 a month | Well under a dollar a month |
| 25,000 | $199 a month | A few dollars a month |
SES charges about ten cents per thousand emails. Sending a six-part sequence to ten thousand people is roughly six dollars, once. Workers and D1 sit inside their free tiers at that volume.
So the real reasons to run your own are not the money today. They are that you own the list outright, nobody can change the terms under you, nothing gets deleted if a bill fails, and the cost stays flat as you grow instead of stepping up every few thousand subscribers. Prices on that platform already jumped about 35% in one go in September 2025. That is the risk you are buying out of.
The five decisions that make it good
Anyone can send an email from SES. The difference between that and a real newsletter system is in the failure modes, and this build gets five of them right.
One-click unsubscribe, done the only way that works
Gmail and Yahoo require bulk senders to support one-click unsubscribe. Miss it and nothing errors, you simply start landing in spam, which is the worst kind of failure because it is invisible.
Doing it requires the List-Unsubscribe and List-Unsubscribe-Post headers, and SES only lets you set custom headers if you send Raw MIME rather than its simpler content API. So the code assembles the MIME message by hand. That is a real piece of work and it is the single most important thing in the whole system.
Better still, the confirmation email deliberately leaves those headers off, with a comment explaining that a transactional confirmation is not bulk mail and the person has not yet agreed to anything to unsubscribe from. That is a genuinely careful distinction.
Double opt-in, so consent is provable
An address that has not clicked the confirmation link sits at pending and never receives anything else. Someone typing your address into the form cannot sign you up. It grows the list more slowly and it is worth it: deliverability is better, and if consent is ever questioned there is an answer.
Signing up tells an attacker nothing
Submitting an address that is already on the list returns a byte-identical response to submitting a new one. Without that, the signup form quietly becomes a tool for checking whether a particular woman is subscribed to a site about testosterone and low desire. On this site of all your sites, that matters.
It claims the slot before it sends, not after
This is the sharpest thing in the codebase. The drip updates a subscriber's position first, with a conditional update that only succeeds if nobody else has moved it, and only then sends. If the send fails it rolls the position back so the next run retries.
Two cron runs overlapping therefore cannot both send email number three to the same person. The asymmetry is chosen on purpose: delaying an email is better than sending a duplicate, both for the reader and for your sending reputation.
A muzzle you have to deliberately remove
Nothing sends unless SENDING_ENABLED is exactly "true". While it is off, every signup still records that sending was disabled, so it is provable that nothing went out. That let the whole thing be built, deployed and tested long before it was armed.
Also already handled
- A scoped IAM user that can send SES mail and read SES account info, and is denied IAM, S3 and EC2. Credentials go in as Worker secrets, never committed.
- A CAN-SPAM physical mailing address in the footer of every sequence email, which is legally required for commercial mail and is the thing most self-hosted setups forget.
- An append-only event log recording every signup, confirmation, unsubscribe and send, never deleted.
- The domain verified in SES us-east-1 with DKIM passing, and production access, so there is no sandbox limit. Both of those are per-region, and getting the region wrong fails in a way that is hard to diagnose.
What is still open
Bounce and complaint handling is the real gap. SES will suppress hard bounces account-wide by itself, but your own database never finds out, so a dead address stays confirmed forever and keeps counting against you. The fix is an SES configuration set publishing to SNS, with a small endpoint writing the result back to a bounced status. The schema already has the status value waiting for it.
This matters more than it sounds. Bounce and complaint rates are what get a sending domain throttled or shut off, and it is the one thing a paid platform absorbs for you completely.
The backend README is out of date. It still lists the six-email drip, the email bodies and the CAN-SPAM footer as unbuilt. All three are done. Worth correcting before it misleads a future session into rebuilding them.
Putting it on your other sites
The pieces that are specific to one site are the six email bodies, the domain, and the from address. Everything else moves across unchanged.
For each new site: verify the domain in SES us-east-1, create a D1 database and apply the same schema, copy the four source files, set the two AWS secrets on that Worker, point the signup form at it, and leave the muzzle on until you have tested with an address you control.
buildamensgroup.com is the obvious first candidate, and it is currently the worst offender. Its kit emails promise readers they can unsubscribe anytime, and there is no unsubscribe link and no header in them at all. The only way off that list today is replying and asking. That site also still sends through Resend rather than SES, and its domain is not yet verified in SES, which is the one step in this whole plan that has to be done by you in the AWS console rather than by me.
Published to Annette's hub. Rebuilt from the source markdown, so edit the source and rerun rather than editing this page.