An AI-powered, configurable podcast platform that aggregates content sources, generates summaries, and produces podcast audio. You can define different sources per topic and build your own podcast flow.
- Source-configurable, topic-agnostic
- Automation lowers the barrier to content creation
- Everyone should be able to run their own podcast pipeline
- Runtime: Next.js 15 (App Router) + Cloudflare Workers (via OpenNext)
- AI: OpenAI / Gemini / MiniMax for content generation
- TTS: Edge TTS / MiniMax / Murf / Gemini TTS
- Storage: Cloudflare KV (metadata) + R2 (audio files)
- UI: Tailwind CSS + shadcn/ui
To prevent Too many subrequests failures in long runs (many sources, many stories, many TTS lines), the project now uses a single main workflow with continuation handoff, instead of splitting logic into separate workflow types.
Core approach:
- Keep one
PodcastWorkflowstage flow:collect_candidates -> expand_gmail -> summarize_stories -> compose_text -> tts_render -> done - Track a subrequest budget during execution (soft limit
45, reserve6) - Before high-cost work, estimate the next cost; if budget is near the threshold, checkpoint first and spawn a continuation instance
- The continuation is still the same main workflow, reusing the same
jobIdand incrementingcontinuationSeq(instance ID like<jobId>-c<seq>)
State and snapshot storage:
- KV stores lightweight workflow state (stage, cursor, progress, snapshot keys), under keys like
workflow:job:<jobId>:state - R2 stores larger stage snapshots under keys like
workflow/jobs/<jobId>/*.jsoncandidates.json: candidate stories and pending Gmail expansionssummary.json: story summary/relevance resultscompose.json: composed podcast/blog text and TTS inputs
This allows the run to automatically continue in a fresh workflow instance before hitting the hard limit, without losing progress, while still producing one logical episode output.
- Node.js >= 18
- pnpm
- A Cloudflare account
- A Gemini API key (or OpenAI API key)
Click "Use this template" on GitHub to create your own repository, then clone it locally:
git clone https://github.com/<your-username>/<your-repo>.git
cd <your-repo>
pnpm installLog in to Cloudflare and create the required resources:
wrangler login
# Create a KV namespace
wrangler kv namespace create PODCAST_KV
# Note the returned namespace id
# Create an R2 bucket
wrangler r2 bucket create <your-podcast-name>Copy the template files and fill in your resource IDs:
cp wrangler.template.jsonc wrangler.jsonc
cp worker/wrangler.template.jsonc worker/wrangler.jsoncEdit wrangler.jsonc:
name— your podcast app name (e.g."my-podcast")vars.PODCAST_ID— your podcast identifier (e.g."my-podcast")kv_namespaces[0].id— the KV namespace ID from Step 2r2_buckets[*].bucket_name— the R2 bucket name from Step 2services[0].service— must match the worker name below
Edit worker/wrangler.jsonc:
name— your worker name (e.g."my-podcast-worker")vars.PODCAST_ID— same as abovekv_namespaces[0].id— same KV namespace IDr2_buckets[0].bucket_name— same R2 bucket nametriggers.crons— when to auto-generate episodes (default:"5 6 * * *", daily at 06:05 UTC)
These files are listed in
.gitignorebecause they contain account-specific resource IDs.
Copy the example files:
cp .env.local.example .env.local
cp worker/.env.local.example worker/.env.localEdit .env.local (Next.js app):
| Variable | Required | Description |
|---|---|---|
PODCAST_ID |
Yes | Same as in wrangler config |
ADMIN_TOKEN |
Yes | Password for the Admin console (choose a strong value) |
NEXT_STATIC_HOST |
Yes | Base URL for audio files. Local dev: http://localhost:3000/static. Production: set in wrangler vars after deployment (see Step 7) |
NODE_ENV |
No | Defaults to development locally. Set to production in wrangler vars |
PODCAST_WORKER_URL |
No | Worker URL used by the Admin trigger route in production when service binding is unavailable |
TRIGGER_TOKEN |
No | Token used by the Admin trigger route when calling the Worker over HTTP in production |
Edit worker/.env.local (Worker):
| Variable | Required | Description |
|---|---|---|
PODCAST_ID |
Yes | Same as above |
GEMINI_API_KEY |
Yes | Google Gemini API key |
MINIMAX_API_KEY |
No | MiniMax API key (if using MiniMax for text generation) |
ADMIN_TOKEN |
Yes | Same as above |
PODCAST_WORKER_URL |
Yes | Worker URL. Local dev: http://localhost:8787. Production: set in wrangler vars after deployment (see Step 7) |
PODCAST_R2_BUCKET_URL |
Yes | R2 bucket public URL. Local dev: http://localhost:8787/static. Production: your R2 custom domain or public URL |
OPENAI_API_KEY |
No | OpenAI API key (if using OpenAI) |
CLOUDFLARE_ANYPODCAST_ACCOUNT_ID |
No | Cloudflare account ID for markdown extraction. When configured, Cloudflare Markdown APIs are tried before Jina |
CLOUDFLARE_ANYPODCAST_API_TOKEN |
No | Cloudflare API token for markdown extraction. Requires Workers AI: Read and Browser Rendering: Edit scopes |
JINA_KEY |
No | Jina API key (for web content extraction) |
MINIMAX_TTS_GROUP_ID |
No | MiniMax TTS Group ID (required when tts.provider=minimax) |
MINIMAX_TTS_API_KEY |
No | MiniMax TTS API key (required when tts.provider=minimax) |
MURF_API_KEY |
No | Murf API key (required when tts.provider=murf) |
TRIGGER_TOKEN |
No | Token for manually triggering the workflow via curl |
GMAIL_* |
No | Gmail OAuth credentials (for newsletter sources) |
Business runtime settings such as AI provider/model/base URL, workflow test settings, sources, prompts, and TTS options are configured in Admin and stored in runtime config, not in Worker env variables.
For backward compatibility, the Worker still accepts legacy TTS_API_ID / TTS_API_KEY, but new deployments should use the provider-specific names above.
There are three ways to configure your podcast content and behavior:
The web-based Admin console lets you configure everything at runtime without touching code:
- Start the dev servers (see Step 6) or deploy first
- Visit
/admin/loginand enter yourADMIN_TOKEN - Configure all settings in the UI:
- Site: title, description, logo, theme color, contact email
- Hosts: name, gender, persona, speaker marker for each host
- AI: provider (Gemini/OpenAI/MiniMax), model, API base URL
- TTS: provider (Gemini/Edge/MiniMax/Murf), language, voice for each host, audio quality, intro music
- Sources: add RSS feeds, URLs, or Gmail labels
- Prompts: customize all AI prompts (story summary, podcast dialogue, blog post, intro, title)
- Locale: language, timezone
All changes are saved to KV and take effect immediately on the next workflow run.
For content sources, you can define them in code instead of (or in addition to) the Admin console:
cp workflow/sources/config.example.ts workflow/sources/config.local.tsEdit workflow/sources/config.local.ts to add your RSS feeds, URLs, or Gmail labels. This file is gitignored and takes priority as the default source configuration.
The file config.ts contains static defaults for site metadata (title, description, SEO, theme). These are used as fallbacks when no runtime config exists in KV. For most cases, prefer configuring via the Admin console instead.
# Start the Next.js dev server (port 3000)
pnpm dev
# Start the Worker dev server (port 8787) in another terminal
pnpm dev:worker
# Trigger the workflow manually
curl -X POST http://localhost:8787# Deploy the Worker first
pnpm deploy:worker
# The CLI will print the Worker URL, e.g. https://my-podcast-worker.<your-subdomain>.workers.dev
# Set production secrets for the Worker
wrangler secret put GEMINI_API_KEY --cwd worker
wrangler secret put ADMIN_TOKEN --cwd worker
# Add other secrets as needed (OPENAI_API_KEY, MINIMAX_TTS_API_KEY, MURF_API_KEY, etc.)
# Deploy the Next.js app
pnpm run deploy
# The CLI will print the app URL, e.g. https://my-podcast.<your-subdomain>.workers.devAfter the first deploy, you need to set the production URLs that weren't known beforehand. Add them to the vars section of your wrangler config files, then redeploy:
In wrangler.jsonc (Next.js app), add to vars:
In worker/wrangler.jsonc (Worker), add to vars:
"PODCAST_WORKER_URL": "https://my-podcast-worker.<your-subdomain>.workers.dev",
"PODCAST_R2_BUCKET_URL": "https://<your-r2-public-url>"Then redeploy both: pnpm deploy:worker && pnpm run deploy
You can also set custom domains for your app and Worker in the Cloudflare dashboard, then use those domains in the vars above.
After deployment:
- Your app URL is printed by the deploy command, or find it in the Cloudflare dashboard under Workers & Pages
- Go to
/adminto configure your podcast via the Admin console - Trigger the first episode from the Admin console: go to
/admin, switch to the Testing tab, and click Trigger Workflow — or use curl:curl -X POST <your-worker-url> - The Worker's cron trigger automatically generates new episodes on schedule. The default is daily at 06:05 UTC — configure this in
worker/wrangler.jsoncundertriggers.cronsusing standard cron syntax
You can run multiple independent podcasts from the same codebase. Each podcast is a separate Cloudflare deployment with its own configuration. No code changes required.
-
Create additional Cloudflare resources (KV namespace, R2 bucket) for the new podcast
-
Create named wrangler config files for each podcast:
# For a podcast named "my-second"
cp wrangler.template.jsonc wrangler.my-second.jsonc
cp worker/wrangler.template.jsonc worker/wrangler.my-second.jsonc-
Fill in the new resource IDs and podcast name in both files
-
Add the new config files to
.gitignore(they contain account-specific IDs)
The Next.js dev server (pnpm dev) always reads wrangler.jsonc. To switch which podcast is active locally, add convenience scripts to package.json:
{
"scripts": {
"use:first": "cp wrangler.my-first.jsonc wrangler.jsonc && cp worker/wrangler.my-first.jsonc worker/wrangler.jsonc && echo 'Switched to my-first'",
"use:second": "cp wrangler.my-second.jsonc wrangler.jsonc && cp worker/wrangler.my-second.jsonc worker/wrangler.jsonc && echo 'Switched to my-second'"
}
}Then switch and work as usual:
pnpm use:second # Switch to second podcast
pnpm dev:worker # Start Worker dev server
pnpm dev # Start Next.js dev serverSwitch to the target podcast before deploying:
pnpm use:second # Switch config
pnpm deploy:worker # Deploy this podcast's Worker
pnpm run deploy # Deploy this podcast's Next.js appEach deployment has its own independent Admin page, prompts, TTS settings, content sources, and data. Configure each podcast via its own Admin console after deployment.
Each podcast instance reads
PODCAST_IDfrom its wrangler config, and all data in KV/R2 is namespaced by podcast ID.
| Command | Description |
|---|---|
pnpm dev |
Start Next.js dev server (port 3000) |
pnpm dev:worker |
Start Worker dev server (port 8787) |
pnpm build |
Build the Next.js app |
pnpm run deploy |
Build and deploy the Next.js app |
pnpm deploy:worker |
Deploy the Worker |
pnpm logs:worker |
Tail Worker logs |
pnpm use:<name> |
Switch active podcast config (see Running Multiple Podcasts) |
pnpm lint:fix |
Auto-fix ESLint issues |
pnpm tests |
Run integration tests (requires remote) |
This project evolved from hacker-podcast. Thanks to the original author for open-sourcing it.
