Collect emails for a newsletter on your static website for free using Cloudflare Workers and Cloudflare R2.
git clone https://github.com/yakkomajuri/neusletter
cd neusletter
npm run setupThe setup will walk you through authenticating with Cloudflare and then spin everything up for you.
When done it will automatically open an example page in your browser for you to see the newsletter in action.
Everything should take two minutes and you can then start collecting emails on your static website.
The goal here is not to be a full self-hosted newsletter management system, but rather to show how one can build on top of existing free offerings to build functionality for their website.
You're more than encouraged to fork this and modify as you wish. You could mofidy it to collect more information, add an endpoint to list subscribers (with a secret), or even use Workers KV instead of R2.
But now you can start collecting emails without paying for a service and before deciding what you'll use to actually send emails, all while paying zero dollars.
- A visitor submits their email via a form
- The worker validates the email and stores it as a JSON object in an R2 bucket
Each subscriber is stored as its own R2 object keyed by email address. No database needed, no race conditions on concurrent writes.
- A Cloudflare account
- Node.js (v18+)
The setup script walks you through everything:
npm run setupThis will:
- Check that wrangler is available and you're authenticated
- Create the R2 bucket (
neusletter-subscribers) and deploy the worker
If you prefer to do it yourself:
# 1. Log in to Cloudflare
npx wrangler login
# 2. Create the R2 bucket
npx wrangler r2 bucket create neusletter-subscribers
# 3. Deploy
npx wrangler deploySubscribe an email address.
Request:
{ "email": "reader@example.com" }Example:
curl -X POST https://neusletter.YOUR_SUBDOMAIN.workers.dev \
-H 'Content-Type: application/json' \
-d '{"email": "reader@example.com"}'Response (200):
{ "ok": true, "message": "Subscribed" }Errors:
400— invalid JSON or invalid email405— wrong HTTP method500— R2 storage failure
Open example/index.html in a browser. Update the WORKER_URL constant at the top of the <script> to point to your deployed worker (if you ran npm run setup the script automatically did this for you).
You can embed the same fetch logic in any page:
<form id="subscribe-form">
<input type="email" id="email" placeholder="you@example.com" required>
<button type="submit">Subscribe</button>
</form>
<script>
document.getElementById("subscribe-form").addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch("https://neusletter.YOUR_SUBDOMAIN.workers.dev", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: document.getElementById("email").value }),
});
const data = await res.json();
alert(data.ok ? "Subscribed!" : data.error);
});
</script>npm run devThis starts a local worker with wrangler dev. R2 is simulated locally in .wrangler/.
# List all subscriber emails
npx wrangler r2 object list neusletter-subscribers
# Download all records as JSON (pipe through jq if you have it)
npx wrangler r2 object list neusletter-subscribers --json | jq '.[].key'
# Download a specific subscriber
npx wrangler r2 object get neusletter-subscribers/reader@example.comMIT