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
sending - Fetch 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.
Top comments (0)