Podcast Lifecycle (Pause & Deletion)
This guide covers the complete lifecycle management system for podcasts: pausing, unpausing, and deleting podcasts, including their effects on RSS feeds, billing, and data retention.
Prerequisite
For billing sync details (Stripe integration), see Subscription Lifecycle Management. This guide focuses on the podcast-level state machine and its downstream effects.
Architecture Overview
Podcast Status State Machine
Status Definitions
| Status | is_active | RSS Behavior | App Features | Billing |
|---|---|---|---|---|
active | true | Serve feed normally | Full access | Active |
paused | true | Serve feed normally | Frozen (read-only) | Paused (if last podcast) |
pending_deletion | false | Redirect or grace period | No access | Canceled (if last podcast) |
The is_active column is kept in sync via the sync_podcast_is_active() database trigger for backwards compatibility.
Database Schema
Migration: supabase/migrations/20260413111213_add_podcast_lifecycle_states.sql
New Enum Type
CREATE TYPE podcast_status AS ENUM ('active', 'paused', 'pending_deletion');Columns Added to podcasts
| Column | Type | Default | Purpose |
|---|---|---|---|
status | podcast_status | 'active' | Lifecycle state (source of truth) |
paused_at | TIMESTAMPTZ | NULL | When the podcast was paused |
pause_expires_at | TIMESTAMPTZ | NULL | When pause auto-expires (max 90 days) |
last_pause_ended_at | TIMESTAMPTZ | NULL | When last pause ended (12-month cooldown) |
deletion_requested_at | TIMESTAMPTZ | NULL | When owner requested deletion |
deletion_redirect_url | TEXT | NULL | RSS redirect URL for subscriber migration |
deletion_scheduled_at | TIMESTAMPTZ | NULL | When hard delete + R2 cleanup occurs |
Constraints
| Constraint | Rule |
|---|---|
chk_pause_requires_timestamps | paused status requires paused_at and pause_expires_at |
chk_deletion_requires_timestamps | pending_deletion requires deletion_requested_at and deletion_scheduled_at |
chk_pause_max_90_days | pause_expires_at <= paused_at + 91 days |
chk_deletion_redirect_max_90_days | deletion_scheduled_at <= deletion_requested_at + 91 days |
Indexes
-- Lifecycle worker: find paused podcasts to auto-reactivate
CREATE INDEX idx_podcasts_pause_expires ON podcasts (pause_expires_at) WHERE status = 'paused';
-- Lifecycle worker: find pending_deletion podcasts to hard-delete
CREATE INDEX idx_podcasts_deletion_scheduled ON podcasts (deletion_scheduled_at) WHERE status = 'pending_deletion';
-- General status filtering
CREATE INDEX idx_podcasts_status ON podcasts (status);Backwards Compatibility Trigger
-- Keeps is_active in sync: TRUE for active/paused, FALSE for pending_deletion
CREATE OR REPLACE FUNCTION sync_podcast_is_active()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.status IN ('active', 'paused') THEN
NEW.is_active := TRUE;
ELSE
NEW.is_active := FALSE;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;Pause Flow
Eligibility Rules
A podcast can be paused when all of these are true:
- User is on a paid plan (starter or professional)
- Podcast
statusisactive - 12-month cooldown has elapsed since
last_pause_ended_at
Pause Effects
status→pausedpaused_at→ nowpause_expires_at→ now + 90 days- RSS feed continues serving (subscribers don't notice)
- App features are frozen (read-only)
- If this is the user's last active podcast: Stripe billing paused via
pause_collection: { behavior: 'void' }
Unpause Effects
status→activepaused_at→ NULL,pause_expires_at→ NULLlast_pause_ended_at→ now (starts the 12-month cooldown)- Stripe billing resumed (clears
pause_collection)
Auto-Reactivation
The Lifecycle Manager Worker runs daily at 04:00 UTC and auto-reactivates any podcast where status = 'paused' and pause_expires_at <= NOW(). This also triggers billing resumption.
Deletion Flow
Deletion works differently based on hosting_type:
External Podcasts (hosting_type = 'external')
External podcasts have no RSS feed or R2 audio managed by show.fm, so they are hard-deleted immediately:
DELETE FROM podcasts WHERE id = $id(CASCADE handles all related tables)- Response:
{ status: 'deleted', immediate: true }
Self-Hosted Podcasts (hosting_type = 'podcasterplus')
Self-hosted podcasts have an RSS feed and R2 audio, so they go through a grace period:
Delete Confirmation
The UI requires the user to type the exact podcast name (case-insensitive) to confirm deletion. The API validates this:
const deleteSchema = z.object({
confirm_name: z.string().min(1),
redirect_url: z.string().url().optional().nullable(),
redirect_days: z.number().int().min(0).max(90).optional().default(90)
});What Gets Deleted (CASCADE)
When the podcast row is deleted, CASCADE removes:
| Table | What's deleted |
|---|---|
episodes | All episodes |
episode_credits | All episode credits |
episode_guests | All guest portal access |
episode_people | All episode rosters |
bookings | All bookings |
booking_links | All booking links |
automation_rules | All automation rules |
automation_executions | All execution history |
podcast_members | All team memberships |
team_invitations | All pending invitations |
R2 cleanup (best-effort): All objects under podcasts/{podcastId}/ are deleted.
What is NOT deleted: Guest user profiles (user_profiles) survive — guests can still be invited to other podcasts.
RSS Feed Behavior by Status
The RSS Feed Worker query now includes podcast lifecycle status:
WHERE slug = $slug
AND hosting_type = 'podcasterplus'
AND (
(status IN ('active', 'paused') AND is_active = true)
OR status = 'pending_deletion'
)| Status | Redirect URL | Days Since Request | RSS Response |
|---|---|---|---|
active | — | — | 200 with feed |
paused | — | — | 200 with feed (unchanged) |
pending_deletion | Set | 0-7 | 200 with feed + <itunes:new-feed-url> |
pending_deletion | Set | 8+ | 301 redirect to URL |
pending_deletion | Not set | Within window | 200 with feed (grace period) |
pending_deletion | — | Past deletion_scheduled_at | 404 |
| Hard deleted | — | — | 404 |
Billing Sync
All billing sync functions are safe side effects: they try-catch internally, log failures, and never block the main operation.
| Podcast Event | Condition | Stripe Action |
|---|---|---|
| Pause | Last active podcast for owner | pause_collection: { behavior: 'void' } |
| Unpause | Any podcast unpaused | Clear pause_collection |
| Delete (external) | Last active/paused podcast | cancel_at_period_end: true |
| Delete (self-hosted) | Last active/paused podcast | cancel_at_period_end: true |
| Auto-reactivation (cron) | Pause expired | Clear pause_collection |
Exempt billing models: enterprise_manual, ltd, and enterprise tier users skip all Stripe sync.
Location: src/api/utils/stripe-sync.ts (app), workers/lifecycle-manager/src/index.ts (worker)
UI Flow (Settings Page)
Location: src/routes/(app)/p/[slug]/settings/+page.svelte
The settings page implements a multi-step state machine for the delete/pause journey:
idle
├→ pause-offer (if paid + active + eligible)
│ └→ pause-confirm → [Execute Pause] → redirect home
│
└→ warning (delete path)
└→ Step 1: What gets deleted
└→ billing-warning (if last podcast on paid plan)
└→ redirect (self-hosted only: URL + duration slider 0-90 days)
└→ confirm (type podcast name)
└→ [Execute Delete] → redirect homeBillingWarning Component
Location: src/lib/components/subscription/BillingWarning.svelte
Shown when deleting/pausing the user's last active podcast on a paid plan. Warns about subscription cancellation or billing pause.
Key Files
| File | Purpose |
|---|---|
src/api/routes/podcast-lifecycle.ts | API: GET state, POST pause/unpause/delete |
src/api/utils/stripe-sync.ts | Billing sync functions |
src/routes/(app)/p/[slug]/settings/+page.svelte | UI: multi-step pause/delete journey |
src/lib/components/subscription/BillingWarning.svelte | Billing impact warning |
workers/lifecycle-manager/src/index.ts | Cron: auto-reactivate pauses, hard-delete |
workers/rss-feed/src/index.ts | RSS: pending_deletion redirect logic |
workers/rss-feed/src/db/client.ts | RSS: status-aware podcast query |
supabase/migrations/20260413111213_add_podcast_lifecycle_states.sql | Schema: status enum, columns, constraints |
Related Documentation
- Podcast Lifecycle API - Endpoint specifications
- Lifecycle Manager Worker - Cron worker documentation
- RSS Feed Worker - Feed generation and redirect behavior
- Subscription Lifecycle - Billing sync details
- Stripe Integration - Full Stripe service documentation