Skip to content

Cloudflare Services

show.fm uses multiple Cloudflare services for hosting, storage, and edge computing.

Account Information

PropertyValue
Account IDb8eff1b484adf398bda38644efc85bce
Zonepodcasterplus.com
PlanWorkers 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

PropertyValue
Project Namepodcaster-plus-app
Domainapp.podcasterplus.com
FrameworkSvelteKit
Build Output.svelte-kit/cloudflare
Configuration/wrangler.toml

Deployment:

bash
cd /path/to/podcaster-plus-app
CLOUDFLARE_ACCOUNT_ID=b8eff1b484adf398bda38644efc85bce \
  npx wrangler pages deploy .svelte-kit/cloudflare --project-name podcaster-plus-app

Bindings:

toml
# 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

PropertyValue
Project Namepodcaster-plus-app
Domainbook.podcasterplus.com
StatusActive
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

PropertyValue
Worker Namepodcasterplus-rss-feed
Domainfeed.podcasterplus.com
Source/workers/rss-feed/
Handler TypesHTTP (fetch) + Queue (consumer)
DocumentationRSS Feed Worker

Deployment:

bash
cd workers/rss-feed && npx wrangler deploy

Bindings:

  • KV Namespace: RSS_CACHE
  • Hyperdrive: HYPERDRIVE
  • Queue Consumer: rss-invalidation

Scheduled Publisher Worker

PropertyValue
Worker Namepodcasterplus-scheduled-publisher
Source/workers/scheduled-publisher/
Handler TypesCron (every minute) + HTTP (health)
DocumentationScheduled Publisher

Purpose: Automatically publishes episodes at their scheduled time and emits automation events.

Deployment:

bash
cd workers/scheduled-publisher && npx wrangler deploy

Bindings:

  • Hyperdrive: HYPERDRIVE
  • Queue Producer: RSS_INVALIDATION_QUEUE

Automation Scheduler Worker

PropertyValue
Worker Namepodcasterplus-automation-scheduler
Source/workers/automation-scheduler/
Handler TypesCron (every minute) + Queue (consumer) + HTTP
DocumentationAutomation Scheduler

Purpose: Processes time-based automation triggers and event-based triggers from the queue.

Deployment:

bash
cd workers/automation-scheduler && npx wrangler deploy

Bindings:

  • Hyperdrive: HYPERDRIVE
  • Queue Producer: AUTOMATION_QUEUE (to automation-executions)
  • Queue Consumer: automation-events

Automation Executor Worker

PropertyValue
Worker Namepodcasterplus-automation-executor
Source/workers/automation-executor/
Handler TypesQueue (consumer) + HTTP (health)
DocumentationAutomation Executor

Purpose: Executes automation actions (email, webhook, field update) with delay handling.

Deployment:

bash
cd workers/automation-executor && npx wrangler deploy

Bindings:

  • Hyperdrive: HYPERDRIVE
  • Queue Consumer: automation-executions

R2 Object Storage

Media Bucket

PropertyValue
Bucket Namepodcasterplus-media
BindingMEDIA_BUCKET
Public URLhttps://media.podcasterplus.com
PurposeAudio 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.jpg

Supported Audio Formats: MP3, M4A, WAV (max 500MB)

Image Types:

TypePurposeTypical Size
coverPodcast cover art1400x1400 - 3000x3000
headerPage header/bannerVariable
profileCircular profile image400x400
bannerWide banner imageVariable

CORS Configuration:

json
{
	"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:

VariantWidthHeightQualityUse Case
thumbnail15015080List views, small cards
avatar10010085Profile images
card30030085Card displays
preview60060085Preview images
hero1200120090Hero sections
social120063090Social media shares

Usage (from src/lib/utils/image-url.ts):

typescript
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

PropertyValue
Namespace IDc22b685ea2bb4099a58a4788b58e1007
BindingRSS_CACHE
Used ByRSS 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:

bash
# 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

PropertyValue
Config IDa81d477ff9264805989f5a72f0354ee8
BindingHYPERDRIVE
TargetSupabase PostgreSQL
Used ByAll 4 Workers

Benefits:

  • 10-100x faster cold starts vs direct connections
  • Automatic connection pooling
  • Prepared statement caching (disabled for compatibility)

Configuration:

toml
# All worker wrangler.toml files
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "a81d477ff9264805989f5a72f0354ee8"

Usage:

typescript
import postgres from 'postgres';

const sql = postgres(env.HYPERDRIVE.connectionString, {
	max: 1, // Hyperdrive handles pooling
	prepare: false // Required for Hyperdrive
});

Management:

bash
# 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=xxx

Queues

show.fm uses four Cloudflare Queues for async messaging:

RSS Invalidation Queue

PropertyValue
Queue Namerss-invalidation
ProducerMain App, Scheduled Publisher
ConsumerRSS Feed Worker
Max Batch Size10
Max Retries3

Message Schema:

typescript
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

PropertyValue
Queue Nameautomation-events
ProducerReserved for future use
ConsumerAutomation Scheduler Worker
Max Batch Size50
Max Retries3
Dead Letter Queueautomation-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):

typescript
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

PropertyValue
Queue Nameautomation-executions
ProducerAutomation Scheduler Worker
ConsumerAutomation Executor Worker
Max Batch Size25
Max Retries5
Dead Letter Queueautomation-dlq

Message Schema:

typescript
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

PropertyValue
Queue Nameautomation-dlq
PurposeFailed automation messages after max retries
Producersautomation-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):

bash
npx wrangler queues create rss-invalidation
npx wrangler queues create automation-events
npx wrangler queues create automation-executions
npx wrangler queues create automation-dlq

DNS Configuration

Subdomains

SubdomainTypeTargetPurpose
appCNAMEpodcaster-plus-app.pages.devMain application
feedRouteWorkers route bindingRSS feeds
mediaCNAMER2 public bucketMedia CDN
bookCNAMEpodcaster-plus-app.pages.devGuest booking portal

Workers Routes

toml
# 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:

SecretDescription
STRIPE_SECRET_KEYStripe API key
STRIPE_WEBHOOK_SECRETStripe webhook signing
SUPABASE_SECRET_KEYDatabase access
RSS_INVALIDATION_SECRETRSS cache invalidation auth
RESEND_API_KEYEmail service API key
GOOGLE_CLIENT_SECRETGoogle OAuth client secret

Worker Secrets

Each worker requires specific secrets:

RSS Feed Worker:

bash
cd workers/rss-feed
npx wrangler secret put SUPABASE_SECRET_KEY
npx wrangler secret put RSS_INVALIDATION_SECRET

Scheduled Publisher:

bash
cd workers/scheduled-publisher
npx wrangler secret put SUPABASE_SECRET_KEY
npx wrangler secret put RSS_INVALIDATION_SECRET

Automation Scheduler:

bash
cd workers/automation-scheduler
npx wrangler secret put SUPABASE_SECRET_KEY

Automation Executor:

bash
cd workers/automation-executor
npx wrangler secret put SUPABASE_SECRET_KEY
npx wrangler secret put RESEND_API_KEY
npx wrangler secret put RESEND_FROM_EMAIL

Environment Variables

Main App (wrangler.toml)

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)

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

bash
# 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 deploy

Individual Services

bash
# 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-executor

Monitoring & Debugging

Dashboard Locations

ServiceDashboard Path
PagesWorkers & Pages > podcaster-plus-app
RSS WorkerWorkers & Pages > podcasterplus-rss-feed
PublisherWorkers & Pages > podcasterplus-scheduled-publisher
SchedulerWorkers & Pages > podcasterplus-automation-scheduler
ExecutorWorkers & Pages > podcasterplus-automation-executor
R2R2 > podcasterplus-media
KVWorkers & Pages > KV > RSS_CACHE
QueuesWorkers & Pages > Queues
HyperdriveWorkers & Pages > Hyperdrive

CLI Commands

bash
# 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 a81d477ff9264805989f5a72f0354ee8

Cost Considerations

ServiceFree TierPaid Tier
PagesUnlimited sitesN/A
Workers100K req/day$5/mo + usage
R210GB storage, 1M Class A, 10M Class BUsage-based
KV100K reads/day, 1K writes/dayUsage-based
QueuesRequires Workers PaidIncluded
HyperdriveRequires Workers PaidIncluded
Image Transforms5000 transforms/moUsage-based

Internal documentation - Not for public distribution