Cloudflare Services
show.fm uses multiple Cloudflare services for hosting, storage, and edge computing.
Account Information
| Property | Value |
|---|---|
| Account ID | b8eff1b484adf398bda38644efc85bce |
| Zone | podcasterplus.com |
| Plan | Workers Paid (required for Queues) |
Services Overview
┌─────────────────────────────────────────────────────────────────────────────────┐
│ show.fm Infrastructure │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────────────────────────────────────┐ │
│ │ Cloudflare │ │ Cloudflare Workers │ │
│ │ Pages │ │ │ │
│ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ Main App │ │ │ RSS Feed │ │ Scheduled │ │ │
│ │ Guest Portal │ │ │ Worker │ │ Publisher │ │ │
│ │ (app. / book.) │ │ │ (feed.) │ │ (cron) │ │ │
│ │ │ │ └─────────────┘ └─────────────┘ │ │
│ └────────┬─────────┘ │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ │ Automation │ │ Automation │ │ │
│ │ │ │ Scheduler │ │ Executor │ │ │
│ │ │ │ (cron) │ │ (queue) │ │ │
│ │ │ └─────────────┘ └─────────────┘ │ │
│ │ └─────────────────────┬────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Supporting Services │ │
│ │ │ │
│ │ ┌────────┐ ┌─────────────┐ ┌───────────┐ ┌───────┐ ┌───────────┐ │ │
│ │ │ R2 │ │ KV │ │ Queues │ │ Hyper │ │ DNS │ │ │
│ │ │ Media │ │ RSS Cache │ │ (4 types) │ │ drive │ │ │ │ │
│ │ └────────┘ └─────────────┘ └───────────┘ └───────┘ └───────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘Cloudflare Pages
Main Application
| Property | Value |
|---|---|
| Project Name | podcaster-plus-app |
| Domain | app.podcasterplus.com |
| Framework | SvelteKit |
| Build Output | .svelte-kit/cloudflare |
| Configuration | /wrangler.toml |
Deployment:
cd /path/to/podcaster-plus-app
CLOUDFLARE_ACCOUNT_ID=b8eff1b484adf398bda38644efc85bce \
npx wrangler pages deploy .svelte-kit/cloudflare --project-name podcaster-plus-appBindings:
# wrangler.toml
[[r2_buckets]]
binding = "MEDIA_BUCKET"
bucket_name = "podcasterplus-media"
[[queues.producers]]
queue = "rss-invalidation"
binding = "RSS_INVALIDATION_QUEUE"
[[queues.producers]]
queue = "automation-executions"
binding = "AUTOMATION_EXECUTION_QUEUE"Guest Booking Portal
| Property | Value |
|---|---|
| Project Name | podcaster-plus-app |
| Domain | book.podcasterplus.com |
| Status | Active |
| Routes | /book/[host]/[slug] |
The guest booking portal is served from the main SvelteKit application via subdomain routing. See Google Calendar Integration for the booking system architecture.
Cloudflare Workers
show.fm uses four Cloudflare Workers for edge computing:
RSS Feed Worker
| Property | Value |
|---|---|
| Worker Name | podcasterplus-rss-feed |
| Domain | feed.podcasterplus.com |
| Source | /workers/rss-feed/ |
| Handler Types | HTTP (fetch) + Queue (consumer) |
| Documentation | RSS Feed Worker |
Deployment:
cd workers/rss-feed && npx wrangler deployBindings:
- KV Namespace:
RSS_CACHE - Hyperdrive:
HYPERDRIVE - Queue Consumer:
rss-invalidation
Scheduled Publisher Worker
| Property | Value |
|---|---|
| Worker Name | podcasterplus-scheduled-publisher |
| Source | /workers/scheduled-publisher/ |
| Handler Types | Cron (every minute) + HTTP (health) |
| Documentation | Scheduled Publisher |
Purpose: Automatically publishes episodes at their scheduled time and emits automation events.
Deployment:
cd workers/scheduled-publisher && npx wrangler deployBindings:
- Hyperdrive:
HYPERDRIVE - Queue Producer:
RSS_INVALIDATION_QUEUE
Automation Scheduler Worker
| Property | Value |
|---|---|
| Worker Name | podcasterplus-automation-scheduler |
| Source | /workers/automation-scheduler/ |
| Handler Types | Cron (every minute) + Queue (consumer) + HTTP |
| Documentation | Automation Scheduler |
Purpose: Processes time-based automation triggers and event-based triggers from the queue.
Deployment:
cd workers/automation-scheduler && npx wrangler deployBindings:
- Hyperdrive:
HYPERDRIVE - Queue Producer:
AUTOMATION_QUEUE(toautomation-executions) - Queue Consumer:
automation-events
Automation Executor Worker
| Property | Value |
|---|---|
| Worker Name | podcasterplus-automation-executor |
| Source | /workers/automation-executor/ |
| Handler Types | Queue (consumer) + HTTP (health) |
| Documentation | Automation Executor |
Purpose: Executes automation actions (email, webhook, field update) with delay handling.
Deployment:
cd workers/automation-executor && npx wrangler deployBindings:
- Hyperdrive:
HYPERDRIVE - Queue Consumer:
automation-executions
R2 Object Storage
Media Bucket
| Property | Value |
|---|---|
| Bucket Name | podcasterplus-media |
| Binding | MEDIA_BUCKET |
| Public URL | https://media.podcasterplus.com |
| Purpose | Audio files, podcast artwork, transcripts |
Directory Structure:
podcasterplus-media/
├── podcasts/
│ └── {podcast-id}/
│ ├── images/
│ │ ├── cover/
│ │ │ └── {uuid}.jpg # 1400x1400 cover art
│ │ ├── header/
│ │ │ └── {uuid}.jpg # Header/banner image
│ │ ├── profile/
│ │ │ └── {uuid}.jpg # Profile image
│ │ └── banner/
│ │ └── {uuid}.jpg # Banner image
│ └── episodes/
│ └── {episode-id}/
│ └── audio/
│ └── {uuid}.mp3 # Episode audio (mp3/m4a/wav/ogg)
├── imports/
│ └── {user-id}/
│ └── {uuid}.jpg # Temporary artwork imports from RSS
└── users/
└── {user-id}/
└── avatar.jpgSupported Audio Formats: MP3, M4A, WAV (max 500MB)
Image Types:
| Type | Purpose | Typical Size |
|---|---|---|
cover | Podcast cover art | 1400x1400 - 3000x3000 |
header | Page header/banner | Variable |
profile | Circular profile image | 400x400 |
banner | Wide banner image | Variable |
CORS Configuration:
{
"AllowedOrigins": ["https://app.podcasterplus.com", "https://book.podcasterplus.com"],
"AllowedMethods": ["GET", "PUT", "HEAD"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}Cloudflare Image Transformations
show.fm uses Cloudflare Image Transformations for on-demand image resizing and optimization.
Transform URL Pattern:
https://app.podcasterplus.com/cdn-cgi/image/{options}/{source-url}Preset Variants:
| Variant | Width | Height | Quality | Use Case |
|---|---|---|---|---|
thumbnail | 150 | 150 | 80 | List views, small cards |
avatar | 100 | 100 | 85 | Profile images |
card | 300 | 300 | 85 | Card displays |
preview | 600 | 600 | 85 | Preview images |
hero | 1200 | 1200 | 90 | Hero sections |
social | 1200 | 630 | 90 | Social media shares |
Usage (from src/lib/utils/image-url.ts):
import { getVariantUrl, generateSrcSet, getOriginalUrl } from '$lib/utils/image-url';
// Get optimized URL for a preset variant
const thumbnailUrl = getVariantUrl(originalUrl, 'thumbnail');
// Generate responsive srcset
const srcSet = generateSrcSet(originalUrl, [150, 300, 600]);
// Get original R2 URL (for RSS feeds - must be JPEG/PNG)
const rssImageUrl = getOriginalUrl(originalUrl);Note: RSS feeds require original URLs (not transformed) because podcast apps expect JPEG/PNG formats.
KV Namespaces
RSS Cache
| Property | Value |
|---|---|
| Namespace ID | c22b685ea2bb4099a58a4788b58e1007 |
| Binding | RSS_CACHE |
| Used By | RSS Feed Worker |
Key Patterns:
feed:{slug} # XML content (TTL: 3600s)
feed:{slug}:etag # ETag hash (TTL: 3600s)
feed:{slug}:meta # Metadata JSON (TTL: 3600s)Management:
# List keys
npx wrangler kv:key list --namespace-id=c22b685ea2bb4099a58a4788b58e1007
# Get a value
npx wrangler kv:key get --namespace-id=c22b685ea2bb4099a58a4788b58e1007 "feed:wp-tavern"
# Delete a key
npx wrangler kv:key delete --namespace-id=c22b685ea2bb4099a58a4788b58e1007 "feed:wp-tavern"Hyperdrive
Supabase Connection Pool
| Property | Value |
|---|---|
| Config ID | a81d477ff9264805989f5a72f0354ee8 |
| Binding | HYPERDRIVE |
| Target | Supabase PostgreSQL |
| Used By | All 4 Workers |
Benefits:
- 10-100x faster cold starts vs direct connections
- Automatic connection pooling
- Prepared statement caching (disabled for compatibility)
Configuration:
# All worker wrangler.toml files
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "a81d477ff9264805989f5a72f0354ee8"Usage:
import postgres from 'postgres';
const sql = postgres(env.HYPERDRIVE.connectionString, {
max: 1, // Hyperdrive handles pooling
prepare: false // Required for Hyperdrive
});Management:
# List configs
npx wrangler hyperdrive list
# Get config details
npx wrangler hyperdrive get a81d477ff9264805989f5a72f0354ee8
# Update connection string
npx wrangler hyperdrive update a81d477ff9264805989f5a72f0354ee8 \
--origin-host=xxx.supabase.co \
--origin-port=5432 \
--database=postgres \
--origin-user=postgres \
--origin-password=xxxQueues
show.fm uses four Cloudflare Queues for async messaging:
RSS Invalidation Queue
| Property | Value |
|---|---|
| Queue Name | rss-invalidation |
| Producer | Main App, Scheduled Publisher |
| Consumer | RSS Feed Worker |
| Max Batch Size | 10 |
| Max Retries | 3 |
Message Schema:
type RssInvalidationMessage = {
type: string; // Event type (e.g., "episode.published")
podcast_id: string; // UUID
podcast_slug: string; // Cache key identifier
episode_id?: string; // Optional
timestamp: string; // ISO 8601
};Automation Events Queue
| Property | Value |
|---|---|
| Queue Name | automation-events |
| Producer | Reserved for future use |
| Consumer | Automation Scheduler Worker |
| Max Batch Size | 50 |
| Max Retries | 3 |
| Dead Letter Queue | automation-dlq |
Database-Backed Pattern
Current Implementation: The main app does NOT send messages to this queue directly. Instead, event producers write to the automation_executions database table. The Scheduler Worker polls this table and sends messages to the automation-executions queue.
This queue exists for potential future use cases requiring real-time queue-based event dispatch.
Message Schema (if queue is used):
type AutomationEventMessage = {
trigger_type: string; // e.g., "booking.confirmed"
podcast_id: string;
episode_id?: string;
booking_id?: string;
guest_id?: string;
timestamp: string; // ISO 8601
};Automation Executions Queue
| Property | Value |
|---|---|
| Queue Name | automation-executions |
| Producer | Automation Scheduler Worker |
| Consumer | Automation Executor Worker |
| Max Batch Size | 25 |
| Max Retries | 5 |
| Dead Letter Queue | automation-dlq |
Message Schema:
type AutomationExecutionMessage = {
type: 'execute_rule';
rule_id: string;
execution_id: string;
payload: {
trigger_type: string;
podcast_id: string;
episode_id?: string;
booking_id?: string;
guest_id?: string;
context: Record<string, unknown>;
timestamp: string;
};
attempt: number;
scheduled_at: string;
};Automation Dead Letter Queue
| Property | Value |
|---|---|
| Queue Name | automation-dlq |
| Purpose | Failed automation messages after max retries |
| Producers | automation-events, automation-executions |
Queue Flow Diagram:
Main App (Producers)
│
├──[Queue]──────────────────────────────┐
│ │
│ ┌──[Database]───────────────────┐ │
│ │ │ │
▼ ▼ ▼ ▼
[rss-invalidation] automation_executions (DB)
│ automation_scheduled_jobs (DB)
▼ │
RSS Feed Worker │
│ ▼
▼ Automation Scheduler (Cron)
KV Cache │
▼
[automation-executions]
│
▼
Automation Executor
│
┌───────────┼───────────┐
▼ ▼ ▼
Email Webhook Field Update
(Resend) (HTTP) (Database)
┌─────────────────────────────────────┐
│ automation-dlq │
│ (Failed messages after retries) │
└─────────────────────────────────────┘
Note: Automation uses database-backed queue pattern.
Main app writes to DB tables, Scheduler polls and dispatches.Create Queues (if not exists):
npx wrangler queues create rss-invalidation
npx wrangler queues create automation-events
npx wrangler queues create automation-executions
npx wrangler queues create automation-dlqDNS Configuration
Subdomains
| Subdomain | Type | Target | Purpose |
|---|---|---|---|
app | CNAME | podcaster-plus-app.pages.dev | Main application |
feed | Route | Workers route binding | RSS feeds |
media | CNAME | R2 public bucket | Media CDN |
book | CNAME | podcaster-plus-app.pages.dev | Guest booking portal |
Workers Routes
# RSS Feed Worker route
routes = [
{ pattern = "feed.podcasterplus.com/*", zone_name = "podcasterplus.com" }
]Secrets Management
Main App Secrets
Set via Cloudflare Dashboard or wrangler secret:
| Secret | Description |
|---|---|
STRIPE_SECRET_KEY | Stripe API key |
STRIPE_WEBHOOK_SECRET | Stripe webhook signing |
SUPABASE_SECRET_KEY | Database access |
RSS_INVALIDATION_SECRET | RSS cache invalidation auth |
RESEND_API_KEY | Email service API key |
GOOGLE_CLIENT_SECRET | Google OAuth client secret |
Worker Secrets
Each worker requires specific secrets:
RSS Feed Worker:
cd workers/rss-feed
npx wrangler secret put SUPABASE_SECRET_KEY
npx wrangler secret put RSS_INVALIDATION_SECRETScheduled Publisher:
cd workers/scheduled-publisher
npx wrangler secret put SUPABASE_SECRET_KEY
npx wrangler secret put RSS_INVALIDATION_SECRETAutomation Scheduler:
cd workers/automation-scheduler
npx wrangler secret put SUPABASE_SECRET_KEYAutomation Executor:
cd workers/automation-executor
npx wrangler secret put SUPABASE_SECRET_KEY
npx wrangler secret put RESEND_API_KEY
npx wrangler secret put RESEND_FROM_EMAILEnvironment Variables
Main App (wrangler.toml)
[vars]
PUBLIC_APP_URL = "https://app.podcasterplus.com"
PUBLIC_SITE_URL = "https://app.podcasterplus.com"
PUBLIC_BOOK_URL = "https://book.podcasterplus.com"
PUBLIC_SUPABASE_URL = "https://cmhnfgvbfrkgayrrgrmf.supabase.co"
PUBLIC_SUPABASE_PUBLISHABLE_KEY = "sb_publishable_xxx"
PUBLIC_STRIPE_PUBLISHABLE_KEY = "pk_test_xxx"
R2_PUBLIC_URL = "https://media.podcasterplus.com"Workers (workers/*/wrangler.toml)
[vars]
PUBLIC_MEDIA_URL = "https://media.podcasterplus.com"
PUBLIC_FEED_URL = "https://feed.podcasterplus.com"
PUBLIC_SUPABASE_URL = "https://cmhnfgvbfrkgayrrgrmf.supabase.co"
PUBLIC_APP_URL = "https://app.podcasterplus.com"
PUBLIC_BOOK_URL = "https://book.podcasterplus.com"Deployment Commands
Full Deployment
# 1. Build main app
pnpm build
# 2. Deploy main app
CLOUDFLARE_ACCOUNT_ID=b8eff1b484adf398bda38644efc85bce \
npx wrangler pages deploy .svelte-kit/cloudflare --project-name podcaster-plus-app
# 3. Deploy all Workers
cd workers/rss-feed && npx wrangler deploy
cd ../scheduled-publisher && npx wrangler deploy
cd ../automation-scheduler && npx wrangler deploy
cd ../automation-executor && npx wrangler deployIndividual Services
# Pages only
pnpm run deploy
# Individual workers
cd workers/rss-feed && npx wrangler deploy
cd workers/scheduled-publisher && npx wrangler deploy
cd workers/automation-scheduler && npx wrangler deploy
cd workers/automation-executor && npx wrangler deploy
# View logs
npx wrangler pages deployment tail --project-name podcaster-plus-app
npx wrangler tail podcasterplus-rss-feed
npx wrangler tail podcasterplus-scheduled-publisher
npx wrangler tail podcasterplus-automation-scheduler
npx wrangler tail podcasterplus-automation-executorMonitoring & Debugging
Dashboard Locations
| Service | Dashboard Path |
|---|---|
| Pages | Workers & Pages > podcaster-plus-app |
| RSS Worker | Workers & Pages > podcasterplus-rss-feed |
| Publisher | Workers & Pages > podcasterplus-scheduled-publisher |
| Scheduler | Workers & Pages > podcasterplus-automation-scheduler |
| Executor | Workers & Pages > podcasterplus-automation-executor |
| R2 | R2 > podcasterplus-media |
| KV | Workers & Pages > KV > RSS_CACHE |
| Queues | Workers & Pages > Queues |
| Hyperdrive | Workers & Pages > Hyperdrive |
CLI Commands
# Real-time logs
npx wrangler tail [worker-name]
npx wrangler pages deployment tail --project-name podcaster-plus-app
# KV inspection
npx wrangler kv:key list --namespace-id=c22b685ea2bb4099a58a4788b58e1007
npx wrangler kv:key get --namespace-id=c22b685ea2bb4099a58a4788b58e1007 "feed:podcast-slug"
# Queue inspection (via dashboard)
# Cloudflare Dashboard > Queues > [queue-name]
# Hyperdrive status
npx wrangler hyperdrive list
npx wrangler hyperdrive get a81d477ff9264805989f5a72f0354ee8Cost Considerations
| Service | Free Tier | Paid Tier |
|---|---|---|
| Pages | Unlimited sites | N/A |
| Workers | 100K req/day | $5/mo + usage |
| R2 | 10GB storage, 1M Class A, 10M Class B | Usage-based |
| KV | 100K reads/day, 1K writes/day | Usage-based |
| Queues | Requires Workers Paid | Included |
| Hyperdrive | Requires Workers Paid | Included |
| Image Transforms | 5000 transforms/mo | Usage-based |