The Origin Story: My friend M asked to be removed from our newsletter. But instead of just hitting unsubscribe like a normal person, he said: "Just give me an RSS feed". So here we are. This is what friendship looks like in 2025. 😅
A Next.js API that converts Resend newsletter broadcasts into an RSS feed with full HTML content. Built for desplega.ai to give power users RSS access to our newsletter without maintaining a separate distribution system.
After posting about the limitations we encountered on Hacker News, Zeno Rocha (CEO of Resend) reached out and updated the Broadcast API to include HTML content directly. This allowed us to simplify the implementation by ~44% (262 → 146 lines) and make it much faster. Thanks Resend team! 🙏
Read about the initial implementation to see how user feedback can drive API improvements.
At desplega.ai, we send newsletters via Resend. Some folks (looking at you, M) prefer RSS feeds over email. Instead of maintaining two separate systems, this API:
- ✅ Syncs broadcasts from Resend daily via cron
- ✅ Caches everything in Vercel Blob for fast access
- ✅ Serves a standard RSS feed with full HTML content
- ✅ Provides individual broadcast URLs for web viewing
Perfect for teams using Resend who want to offer RSS without the hassle.
- Daily Auto-Sync: Cron job fetches new broadcasts from Resend every day
- Simple & Fast: Direct HTML from Broadcast API (~14s sync time)
- Full HTML Content: RSS feed includes complete email HTML (not just text)
- Individual Broadcast URLs: Each email is viewable at
/api/broadcast/{id} - Smart Caching: Uses Vercel Blob for fast RSS generation (no API calls)
- Rate-Limited: Respects Resend's 2 calls/second limit
- Type-Safe: Built with TypeScript
┌─────────────┐
│ Resend API │
│ Broadcasts │ ← Now includes HTML! 🎉
└──────┬──────┘
│
│ Daily Cron (midnight UTC)
│
▼
┌──────────────────────────────┐
│ 1. Fetch audiences │
│ 2. Fetch broadcasts list │
│ 3. Fetch each broadcast │
│ (includes HTML + text) │
│ 4. Convert HTML → Markdown │
└──────┬───────────────────────┘
│
│ Store in Vercel Blob
│
▼
┌──────────────────────────┐
│ rss_audiences │
│ rss_broadcasts │
│ rss_broadcast_{id}_info │ ← Full broadcast w/ HTML
│ rss_broadcast_{id}_info_md │ ← Markdown version
└──────┬───────────────────┘
│
│ Read from cache
│
▼
┌──────────────────────┐
│ /api/rss │ ← RSS Feed
│ /api/broadcast/{id} │ ← Individual broadcasts
└──────────────────────┘
- Node.js 18+ and pnpm
- Resend API key
- Vercel account (for deployment)
git clone https://github.com/desplega-ai/rss.git
cd rss
pnpm installCreate a .env file:
# Required
RESEND_API_KEY=re_xxxxx
RESEND_AUDIENCE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_xxxxx # Auto-generated by Vercel
CRON_SECRET=your-random-secret-here
# Optional: RSS Feed Customization
RSS_FEED_TITLE="Newsletter Feed"
RSS_FEED_DESCRIPTION="RSS feed of newsletter broadcasts"Environment Variable Details:
| Variable | Required | Description | Default |
|---|---|---|---|
RESEND_API_KEY |
✅ | Your Resend API key | - |
RESEND_AUDIENCE_ID |
✅ | Target audience ID to fetch broadcasts for | - |
BLOB_READ_WRITE_TOKEN |
✅ | Vercel Blob storage token (auto-generated) | - |
CRON_SECRET |
✅ | Secret for authenticating cron requests | - |
RSS_FEED_TITLE |
⚪ | Custom title for your RSS feed | "Newsletter Feed" |
RSS_FEED_DESCRIPTION |
⚪ | Custom description for your RSS feed | "RSS feed of newsletter broadcasts" |
# Start dev server
pnpm dev
# Trigger cron job manually (in another terminal)
curl -X GET http://localhost:3000/api/cron \
-H "Authorization: Bearer your-cron-secret"
# View RSS feed
curl http://localhost:3000/api/rss
# or open in browser
open http://localhost:3000/api/rsspnpm build-
Push to GitHub
git push origin main
-
Import to Vercel
- Go to vercel.com
- Click "New Project"
- Import your repository
-
Add Environment Variables
In Vercel project settings → Environment Variables:
Required:
RESEND_API_KEY- Your Resend API keyRESEND_AUDIENCE_ID- Target audience IDCRON_SECRET- Random secret for cron authenticationBLOB_READ_WRITE_TOKEN- Auto-generated when you enable Vercel Blob
Optional (RSS Customization):
RSS_FEED_TITLE- Custom feed title (default: "Newsletter Feed")RSS_FEED_DESCRIPTION- Custom feed description (default: "RSS feed of newsletter broadcasts")
-
Enable Vercel Blob
- Go to Storage tab → Create Blob Store
BLOB_READ_WRITE_TOKENwill be automatically set
-
Deploy
- Vercel will auto-deploy on push
- Cron runs daily at midnight UTC (configured in
vercel.json)
-
First Run (Manual Trigger)
curl -X GET https://your-app.vercel.app/api/cron \ -H "Authorization: Bearer your-cron-secret" -
Access Your RSS Feed
https://your-app.vercel.app/api/rss
Returns RSS XML feed with all broadcasts.
Response:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Newsletter Feed</title>
<description>RSS feed of newsletter broadcasts</description>
<item>
<title>Newsletter Subject</title>
<link>https://your-app.vercel.app/api/broadcast/{id}</link>
<guid>https://your-app.vercel.app/api/broadcast/{id}</guid>
<pubDate>Mon, 29 Sep 2025 18:54:10 GMT</pubDate>
<description><![CDATA[Full HTML content here]]></description>
</item>
</channel>
</rss>Syncs data from Resend. Protected by Authorization header.
Headers:
Authorization: Bearer {CRON_SECRET}
Response:
{
"success": true,
"audiences": 1,
"broadcasts": 5
}Performance:
- Typical sync: ~14 seconds (fetches broadcast details with HTML)
Renders individual broadcast as HTML page.
Example:
https://your-app.vercel.app/api/broadcast/fa8f0216-3d02-4543-ad86-5e92e3c27124
Initially, Resend's broadcast API didn't return email content (HTML/text). We had to build a complex workaround involving email fetching and matching. This worked but was slow (~4 minutes) and complex (~262 lines of code).
Read about the initial implementation →
After we shared our experience on Hacker News, the Resend team (shoutout to Zeno!) updated the Broadcast API to include HTML content directly. This is a perfect example of how developer feedback can drive API improvements.
Impact:
- 🚀 44% code reduction (262 → 146 lines)
- ⚡ 94% faster (~14s vs 4 min)
- 🎯 No complex matching logic needed
- 🧹 Simpler architecture
- Rate Limiting: Resend allows 2 API calls/second, so large broadcast lists may take time
- Vercel Blob: Free tier has storage limits; consider S3 for larger deployments (see Migration to S3)
Currently using Vercel Blob for storage. If you outgrow it or want more control:
Benefits of S3:
- Lower cost at scale
- More control over data
- Works outside Vercel ecosystem
- Better for multi-region deployments
Migration Path:
-
Install AWS SDK:
pnpm add @aws-sdk/client-s3
-
Replace
@vercel/blobimports with S3 client -
Update storage functions:
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'us-east-1' }); // Instead of: put(key, data, options) await s3.send(new PutObjectCommand({ Bucket: 'your-bucket', Key: key, Body: data, }));
-
Update environment variables to use S3 credentials
The code structure is designed to make this swap easy - all storage operations are isolated in the cron route.
Resend allows 2 API calls per second. The cron job adds 500ms delays between calls to respect this limit.
rss_audiences # All audiences
rss_broadcasts # All broadcasts for target audience
rss_broadcast_{id}_info # Full broadcast with HTML
rss_broadcast_{id}_info_md # Markdown version (for future use)
rss_audience_{id} # Individual audience (for reference)
The RSS endpoint uses export const dynamic = 'force-dynamic' to ensure it always fetches fresh data from Vercel Blob instead of serving a cached static version.
- Next.js 14 - API routes
- TypeScript - Type safety
- Vercel Blob - Storage
- Vercel Cron - Scheduled jobs
- Turndown - HTML to Markdown (stored but not used in RSS)
- Resend API - Source data
This project was built by desplega.ai, a platform for building AI-powered browser automation and testing tools. We use this to provide RSS access to our newsletter while keeping everything automated through Resend.
If you're building AI agents that need to interact with web applications, check us out!
MIT License
Copyright (c) 2025 desplega.ai
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Made with ☕ by desplega.ai | Thanks M for the inspiration 🎉