🚀 Building a Newsletter Subscription Feature That Actually Works (No BS Edition)
No Mailchimp. No overengineering. Just a clean, event-driven newsletter system that works.
The Problem With Most “Newsletter Features”
Let’s be honest—most newsletter setups are either:
Overkill (hello 20-step Mailchimp workflows)
Manual (someone forgets to hit “send”)
Or duct-taped together like a weekend hackathon project
What we actually want is simple:
User subscribes → You publish → Email goes out automatically
That’s it. No ceremony.
🧠 The Approach: Keep It Lean
This feature was built into a Blog CMS with a few strict rules:
No new vendors (use existing stack)
No double opt-in friction
Fully automated sending
One-click unsubscribe (because lawsuits are expensive 😅)
👉 Full design spec:
⚙️ Core Architecture
Instead of relying on external newsletter platforms, this setup runs fully in-house:
Database: Supabase
Email: Resend
Scheduler: Vercel Cron
Frontend: Next.js
Flow (a.k.a. “what actually happens”)
User enters email → stored in DB
Admin publishes a post
System schedules a send (with delay)
Cron job runs every minute
Emails are sent automatically
User can unsubscribe with one click
No dashboards. No manual triggers. No “oops we forgot to send.”
🗄️ Data Model (Simple but Powerful)
Two tables. That’s it.
1. newsletter_subscriptions
Tracks subscribers:
email (unique)
subscribed_at
unsubscribed_at (null = active)
unsubscribe_token (for one-click unsubscribe)
2. newsletter_sends
Acts like a queue:
post_id (1 send per post)
scheduled_at
status (pending → sending → sent/failed)
This separation is key. It lets you:
Schedule emails
Retry failures
Track delivery
⏱️ The Secret Sauce: Delayed Sending
Instead of blasting emails immediately:
NEWSLETTER_DELAY_MINUTES=60
Why this matters:
Gives you time to fix mistakes after publishing
Avoids “oops typo in production” emails
Feels more intentional
🔁 Automation via Cron
A Vercel cron job runs every minute:
* * * * * → /api/newsletter/cron
It checks:
Any
pendingsends?Is
scheduled_at <= now()?
If yes:
Mark as
sendingFetch active subscribers
Send emails
Update status
If something crashes midway?
👉 Anything stuck in sending for 10+ minutes is marked as failed
No silent failures. No ghost jobs.
✉️ Subscription Experience (UX Matters)
The Subscribe Form
Placed at the bottom of every blog post:
Email input
Subscribe button
Instant feedback (success or error)
No confirmation email. No friction.
Because let’s be real—every extra step kills conversions.
❌ Unsubscribe (Don’t Mess This Up)
Every email includes:
/api/newsletter/unsubscribe?token=xxx
Click → done.
No login. No “are you sure?” guilt trips.
Just clean, respectful UX.
🛡️ Edge Cases You’ll Actually Hit
Handled upfront:
Scenario | Result |
|---|---|
Duplicate email | “Already subscribed” |
Re-subscribe | Reactivates user |
Invalid token | 404 |
No subscribers | Send marked as complete |
Cron crash | Auto-mark failed |
This is where most systems break. Don’t skip it.
🧪 Testing Strategy
Because “it works on my machine” isn’t a strategy:
Unit tests: subscribe/unsubscribe logic
Integration tests: cron + email dispatch
E2E tests: full user flow
Yes, even for a “simple” feature.
💡 Why This Approach Wins
Let’s compare:
Approach | Reality |
|---|---|
Mailchimp | Expensive + overkill |
Manual send | Someone forgets |
Zapier hacks | Fragile |
This system | Clean, automatic, predictable |
You control everything. No black boxes.
🧠 Final Thoughts
This isn’t just a newsletter feature—it’s an event-driven system disguised as one.
Publish event → triggers queue
Queue → processed by cron
State machine → tracks delivery
It’s simple on the surface, but solid under the hood.
And that’s the sweet spot.
🔥 If You’re Building Something Similar…
Start here:
Keep your data model clean
Automate everything
Design for failure (because it will happen)
Avoid adding tools just because they exist
🏁 TL;DR
If your newsletter requires manual effort, it’s broken.
Automate it. Keep it lean. Ship it.