TL;DR: When paulkuo.tw needed a newsletter, my initial question was simple: build it myself, or just use Substack? After working through it with Claude, I ended up with a “semi-self-hosted” setup: the subscriber list and logic stay mine, and delivery goes to Resend. I’ve since added open-rate tracking on top of the same architecture. The experience made something clear: the advantage of going semi-self-hosted isn’t just convenience at launch. It’s that you can keep building later without asking anyone’s permission.

“If the site is going to have a newsletter, is it better to build one from scratch, or use an existing service like Substack?”

That was my exact question to Claude. By that point, paulkuo.tw already had a stable backend, so adding a subscriber list wasn’t technically difficult or expensive. But “technically easy” and “should I do it, and how” are different questions. I wanted to think through the architecture before writing a single line of code.

Substack is convenient, but it didn’t fit the criteria here

Substack’s strengths are real. It has its own recommendation network, which can help a new newsletter find its first readers. But if you start charging, Substack takes 10% of subscription revenue, plus payment processing fees. Layout and reader engagement gradually migrate into Substack’s app and Notes ecosystem.

For many people, that’s a reasonable trade. For paulkuo.tw, what mattered more was keeping ownership of the subscriber list and the freedom to change the site and sending process later.

If the site, content, and domain are all mine but the subscriber relationship lives on another platform, the whole architecture carries a visible dependency. Unless I really needed Substack’s early-stage distribution, that trade didn’t make sense for me.

But skipping Substack didn’t mean doing everything myself.

What “semi-self-hosted” actually means

The hard part of full self-hosting isn’t writing the code that sends email. It’s keeping those emails out of spam folders. Sending domains need authentication (SPF, DKIM, DMARC). Bounces and spam complaints need monitoring. A poor sending reputation can get an entire batch of email treated as junk. Specialist services have spent years solving exactly these problems. Starting from scratch doesn’t necessarily save anything.

A logistics analogy makes this clearer. Using a fully managed service is like setting up a counter inside a department store: everything is handled for you, but the customer list belongs to the store. Full self-hosting is like building your own warehouse and running your own delivery fleet: maximum control, maximum work. Semi-self-hosted means you manage the customer relationships and the operations, but you hand off the actual delivery to a carrier like FedEx. You don’t own the trucks.

Architecture diagram showing the semi-self-hosted split: on the left, subscriber list, signup form, and sending logic stay in-house; on the right, SPF/DKIM/DMARC domain authentication and bounce/complaint monitoring are delegated to Resend.

Concretely: I keep the subscriber list, the signup form, and the logic that triggers a send when a new post goes live. What I’m outsourcing is the act of reliably placing that email in someone’s inbox. Once the architecture was set, the question became smaller: who handles the delivery layer?

Four candidates, narrowed to one

Decision flowchart: Substack eliminated for revenue cut and list ownership issues; full self-hosting eliminated for deliverability maintenance burden; after settling on semi-self-hosted, four candidates compared; Cloudflare Email Service, Buttondown, and AWS SES eliminated in sequence; Resend selected.

I put four candidates side by side and worked through each one explicitly, not a gut-feel pick:

CandidateStrengthsConcerns / DrawbacksOutcome
Cloudflare Email ServiceNative to the CF ecosystem, direct Workers integration, 3,000 emails/month includedIn public beta as of 2026; currently only supports transactional email (registration confirmations, order notifications); bulk newsletter sending to subscriber lists is listed as a future capabilityInitially the top choice; eliminated after checking documentation
AWS SESCheapest in the industry; essentially free at this volumeList management, bounce handling, and complaint processing all require custom builds; highest ongoing maintenanceKept until the final comparison
ButtondownLightweight, markdown-friendly, handles list management; free tier up to 100 subscribers~$9/month beyond the free tier; includes list management, which is convenient but conflicts with wanting to own the list myselfEliminated once the semi-self-hosted architecture was set
ResendBuilt-in broadcast sending and unsubscribe API; handles bounce and complaint management; least to build on top of; free tier: 3,000 emails/month, 100/dayEarly impression was that it gets expensive at scaleSelected

One detail worth noting: Cloudflare Email Service was initially the obvious choice. Native to the CF ecosystem, Workers integration, a natural fit. But before writing any implementation plan, I checked the official documentation, and found it explicitly limited to transactional email. Newsletter-style bulk sending wasn’t supported yet.

That became one of the more interesting moments in the conversation with Claude. The option that looked most convenient on the surface was the first one eliminated, precisely because we checked the assumption rather than running with it. Claude wasn’t just generating a list of options here. The more useful thing was working through each assumption until something changed, then adjusting direction.

The final comparison: save on sending costs, or save on maintenance time?

It came down to Resend and AWS SES. The difference was direct: SES has lower per-email costs, but most of what a newsletter needs has to be built. Resend costs a bit more, but broadcast sending, unsubscribes, bounces, and complaint handling come ready to use.

At current subscriber volume, the actual dollar difference between the two is small. The real difference is how much time I’d spend maintaining things. I chose to keep that time for writing, so I went with Resend.

Two follow-on decisions came after that, and they’re where the semi-self-hosted principle actually landed.

The first was where to store subscriber data. My initial plan was to use Resend’s built-in audience management. I ended up keeping the data in my own Cloudflare D1 database instead, with Resend serving purely as the delivery channel. If I ever switch delivery providers, the subscriber data doesn’t need to move.

The second was the sending address. Best practice is to send newsletter email from a subdomain, keeping the newsletter’s sending reputation separate from the main domain. That requires a paid plan upgrade. Given current scale, I went with the already-verified primary domain and [email protected]. It’s not the textbook architecture, but it’s the right call for the current cost and volume tradeoff.

The system went live in late June 2026. I walked the full flow myself: subscribe, receive the confirmation email, click to confirm, then unsubscribe. The confirmation landed in my inbox. The database updated correctly. Automated tests passed. Shortly after launch, my deployment platform account was temporarily suspended, but the site and newsletter kept running without interruption. It was an accidental proof that the deployment wasn’t tied to a single platform. That’s a story for another post.

After sending, I wanted to know whether anyone was opening

Once the system was stable, I came back to a question: are subscribers actually reading these? Most newsletter platforms provide open rates out of the box. In this semi-self-hosted setup, I’d have to decide whether to track it, how to store the data, and where to surface it. The upside: the list and send records are mine, so I can do whatever I want with that data later.

The starting answer was: no data. Resend has open and click tracking, but it’s off by default, and enabling it doesn’t automatically push events back to you.

The implementation isn’t complex. Enable tracking in Resend, set up an endpoint to receive notifications. From that point, whenever someone opens an email, clicks a link, or a message bounces or generates a complaint, Resend sends that event to my system. This is what a webhook means: a service actively pushing events to another system rather than waiting to be asked.

To prevent spoofed requests, the system verifies a digital signature attached to each notification. I added two database tables: one recording who each email was sent to, one recording what happened to it afterward. No additional packages needed: the signature verification uses cryptographic libraries built into both the browser and the server runtime.

Open-rate tracking flow: Resend triggers an open event, sends it via webhook POST, Svix signature verification passes, data written to two new tables in Cloudflare D1, open rate queryable from there.

The tracking pipeline is live and the signature verification is tested. Requests without a valid signature are rejected. Because tracking only just came online, there’s no complete open event recorded yet. The open-rate page currently shows “no data yet,” not a fabricated 0%. That state, pipeline connected but data not yet accumulated, is itself the clearest demonstration of what semi-self-hosted means: when you want to add something, you add it. No waiting for a platform to decide you can.

From here, I can pull this data into my own admin dashboard, pipe it to an analytics tool, or query it through an API when needed. I haven’t decided yet. I’ll let the data accumulate first.

Looking back at how the newsletter system evolved: first it had to send reliably, then I needed to confirm unsubscribes worked, then I wanted to know whether anyone was opening. None of this was planned from the start. Each question emerged from actually using the system.

That, for me, is the practical value of going semi-self-hosted: you don’t have to figure everything out upfront, and when you’re ready to go further, you still have room to move.