Skip to content
Asmar.
← Blog
GuideMay 31, 20264 min readSeries: Building this site

A newsletter without a newsletter provider

No free managed tier will email your list the moment you publish — that's the feature kept behind the paywall. So I didn't rent it. This site's newsletter is three small pieces I already run for free: a static form, a serverless function that holds the API key, and a CI job that broadcasts on publish.

ShareLinkedInX

I wanted one thing from a newsletter: when I publish, email the people who asked to hear about it — a short summary and a link. Every managed provider does this. None of the free tiers do — "send automatically when I publish" is precisely the feature they hold back for a paid plan.

So I didn't buy the plan. The thing I needed turned out to be small enough to own.

The site is a pure static export — there is no server to hold an API key. That single constraint shapes the whole design.

Three pieces, all on free tiers I already run#

The job is three concerns: capture an email, store it, and broadcast to the list when a post goes out. Not one of them needs a newsletter SaaS.

Capture: a form that cannot leak the key#

A static page can't hold the API key — anything in the bundle is public. The one place a secret can live on a static host is a Cloudflare Pages Function: a single serverless endpoint that deploys alongside the static files. The form POSTs to it; the key never reaches the browser.

typescript
// functions/api/subscribe.ts — the only server-side surface on the site
export async function onRequestPost({ request, env }) {
  if (!env.RESEND_API_KEY) {
    return json({ error: "Newsletter is not configured." }, 500);
  }
  const { email, website } = await readForm(request);
  if (website) return json({ ok: true });            // honeypot: a bot filled it
  if (!EMAIL_RE.test(email)) {
    return json({ error: "Enter a valid email." }, 400);
  }

  await fetch("https://api.resend.com/contacts", {
    method: "POST",
    headers: { authorization: `Bearer ${env.RESEND_API_KEY}` },
    body: JSON.stringify({ email, unsubscribed: false }),
  });
  return json({ ok: true });
}

The form degrades gracefully — it's a real <form action="/api/subscribe" method="post">, so it works with JavaScript off; the fetch handler only exists to skip a full-page navigation. A hidden honeypot field catches bots without asking a human to solve a puzzle.

Store: the list lives in Resend#

Resend is the relay — it holds the contacts and does the actual sending. The free tier is 1,000 contacts and 3,000 emails a month, which is a long runway for a personal blog. The function above creates a plain global contact; a Resend segment groups them, and that segment is what the broadcast targets.

Broadcast: the publish hook#

This is the feature I refused to pay for, and it is a dozen lines of CI. When a new file lands under content/posts/, a GitHub Action renders a branded email — the post's summary and a link — then asks Resend to create a broadcast and send it to the segment.

javascript
// create the broadcast, then send it
const { id } = await resend("/broadcasts", {
  segment_id: SEGMENT_ID,
  subject: post.title,
  html: renderEmail(post),
});
await resend(`/broadcasts/${id}/send`, {});          // fire

A small ledger file records which slugs have already been mailed, so re-running the job — or pushing an unrelated change — never double-sends.

What it costs to own#

Nothing in dollars; something in surface area. I now own three small pieces instead of one dashboard — if the broadcast logic breaks, that's mine to fix, not a support ticket. The Resend free tier caps me at 1,000 contacts and 3,000 sends a month, a limit I will happily renegotiate the day it bites. And it is single opt-in today; if deliverability ever wobbles, the honest fix is a confirmation step — which I would add before scaling the list, not after.

That is the trade: a few moving parts I understand, instead of a monthly line item I don't.

The takeaway#

A "newsletter provider" is, mostly, a hosted form, a contacts table, and a send button. When the only thing you actually need from behind the paywall is email my list when I publish, you can usually assemble it from infrastructure you already run for free — and own the result instead of renting it. The provider sells convenience; sometimes the convenience isn't worth the dependency.

ShareLinkedInX

Keep reading

Stay in the loop

New posts, in your inbox.

When I publish on architecture, AI engineering, or the systems I'm building, you'll be the first to know.

New posts only. No spam; unsubscribe anytime.