Rolling My Own Newsletter with Claude
Day two of the Hacker News traffic spike. Forty thousand visitors, no way to reach them again. I needed a newsletter signup.
I looked at Buttondown, Beehiiv, Substack, ConvertKit. All overkill. I just needed to collect emails. I didn’t need campaigns, analytics, or subscriber management. And I wanted to own my data.
So I asked Claude to build it.
flowchart LR
User([User]) --> Form[Newsletter Form]
Form -->|POST /api/subscribe| Worker[Cloudflare Worker]
Worker --> KV[(Cloudflare KV)]
KV -.->|Daily sync| GHA[GitHub Actions]
GHA -.-> Repo[(subscribers.jsonl)]
flowchart TB
User([User]) --> Form[Newsletter Form]
Form -->|POST /api/subscribe| Worker[Cloudflare Worker]
Worker --> KV[(Cloudflare KV)]
KV -.->|Daily sync| GHA[GitHub Actions]
GHA -.-> Repo[(subscribers.jsonl)]
The Setup
I gave Claude a Cloudflare API token via environment variable and described what I wanted: a form that collects emails and stores them somewhere I control.
Under 30 minutes later, I had a working newsletter.
What Claude Built
| Component | Technology |
|---|---|
| Form | HTML + vanilla JS |
| Backend | Cloudflare Worker |
| Storage | Cloudflare KV |
| Sync | GitHub Actions |
The form is a Hugo partial that posts to /api/subscribe:
<form id="newsletter-form">
<input type="email" name="email" placeholder="[email protected]" required>
<button type="submit">Subscribe</button>
</form>
The backend is a 42-line Cloudflare Worker that handles validation, normalization, duplicate detection, and referer sanitization:
export async function onRequestPost(context) {
const { email } = await request.json();
// Validate, normalize, check for duplicates
const emailKey = email.trim().toLowerCase();
const existing = await env.SUBSCRIBERS.get(emailKey);
if (existing) {
return Response.json({ success: true, message: "Already subscribed" });
}
// Sanitize referer (strip query params for privacy)
const referer = request.headers.get("Referer");
const source = referer ? new URL(referer).origin + new URL(referer).pathname : "direct";
// Store in KV with metadata
await env.SUBSCRIBERS.put(emailKey, JSON.stringify({
subscribedAt: new Date().toISOString(),
source
}));
return Response.json({ success: true }, { status: 201 });
}
Rate limiting comes from Cloudflare’s built-in protection—no extra code needed.
A GitHub Actions workflow runs daily to sync subscribers from Cloudflare KV to a JSONL file in the repo. My subscriber list lives in version control. If Cloudflare closes my account tomorrow, I still have my data. And it’s in a format that LLMs can easily work with.
The Workflow
Claude iterated locally until everything worked—form submission, KV storage, error handling. Then pushed to a preview app, tested the complete flow, and merged to main.
The form went live while traffic was still flowing.
This whole thing was built in one session.
Why Not SaaS?
Buttondown is great if you need it. I didn’t. My requirements:
- Collect emails
- Store them somewhere I control
- Ship fast
That’s it. I don’t need drip campaigns or A/B testing or fancy templates. I need a list of email addresses that I own.
42 lines of code instead of another monthly subscription. Sometimes the simple solution is the right one.
The takeaway: imperfect and live beats perfect and planned. I could have spent hours evaluating newsletter services, comparing features, reading reviews. Instead I shipped something in 30 minutes and moved on.