Distribution Platform Catalog
The catalog in src/lib/constants/distribution-platforms.ts is a data-driven registry of every directory the Distribution settings page guides users through. All user-facing copy, portal URLs, step sequences, and expectation text live in the data, so adding or amending a platform is a copy change, never a UI change (and never a migration; see below).
The three tiers
DISTRIBUTION_PLATFORMS holds 20 entries across three DistributionTier values:
| Tier | Meaning | Count | Platforms (catalog key) |
|---|---|---|---|
one_click | show.fm submits programmatically. Podcast Index is the only platform with an open write API, so it is the only member | 1 | podcast_index |
guided | The user submits at the platform's portal; the app orchestrates the tricky step (email reveal, claim token, paste-back) | 12 | apple, spotify, youtube, amazon, iheart, tunein, pandora, deezer, pocket_casts, player_fm, podcast_addict, castbox |
ride_along | Nothing to do; the platform mirrors Apple or Podcast Index | 7 | overcast, goodpods, podchaser, listen_notes (mirror apple); fountain, antennapod, castro (mirror podcast_index) |
Ride-along entries carry coveredBy: string[] naming the catalog keys they mirror. The API rejects every listing action on a ride-along platform except set_listing_url (pasting a link is still allowed).
Fields per platform
export interface DistributionPlatform {
key: string; // stable key, stored in podcast_directory_listings.platform
name: string;
tier: DistributionTier;
portalUrl?: string; // where the user submits (guided platforms)
steps?: PlatformStep[];
verification: VerificationKind;
detection: DetectionSource;
slaCopy: string; // honest expectation copy shown while status = submitted
coveredBy?: string[]; // ride_along: which catalog keys this platform mirrors
notes?: string; // card-level caveats
listingUrlHint?: string; // hostname substring used to sanity-check pasted listing URLs
}listingUrlHint is enforced server-side: the set_listing_url action rejects a pasted URL whose hostname does not contain the hint (case-insensitive), and requires https:.
Verification kinds
verification describes how the platform proves feed ownership at submission time:
VerificationKind | Meaning | Catalog members |
|---|---|---|
email_otp | A code or link is emailed to the address inside the RSS feed; the user must open the 24-hour owner-email visibility window first | spotify, youtube, amazon, iheart, pandora, deezer, castbox |
feed_token | A token placed in the feed (Apple's podcast:txt claim flow) | apple |
account_only | Platform account plus form; no feed-level ownership check | tunein |
none | No verification (open API or mirror) | podcast_index, pocket_casts, player_fm, podcast_addict, all ride-along entries |
Detection sources
detection declares which open API the distribution-monitor worker can use to detect a live listing. Only three platforms are detectable:
DetectionSource | Platform | API |
|---|---|---|
itunes | apple | iTunes Search API (exact feed-URL match) |
podcastindex | podcast_index | podcasts/byfeedurl (exact) |
spotify | spotify | Web API show search (fuzzy title match, never auto-confirmed) |
Everything else is none. DETECTABLE_PLATFORMS exports the filtered { key, detection } list; the worker keeps its own lockstep copy (see below).
Steps
Guided platforms carry an ordered steps array. Each PlatformStep has title, description, an optional href, and three boolean flags that render inline controls in the settings page:
copyFeedUrlrenders the copy-feed-URL affordance,revealEmailrenders the "reveal owner email for 24 hours" control,claimTokenrenders the Apple verification-token control.
URL builders
Three helpers own the public URL shapes (both are lockstep-mirrored in workers, which cannot import $lib):
getFeedUrl(slug); // https://feed.podcasterplus.com/{slug}
getListenUrl(slug); // https://listen.podcasterplus.com/{slug}
getListenEpisodeUrl(slug, episodeSlug); // https://listen.podcasterplus.com/{slug}/e/{episodeSlug}The /e/ segment in the episode URL exists to avoid a route-pattern conflict with the public booking page's /{slug}/{slug} shape.
Lookup helpers: getPlatform(key), getPlatformsByTier(tier), and PLATFORM_KEYS (the zod enum source for the API's :platform path param).
How the settings page consumes it
src/routes/(app)/p/[slug]/settings/distribution/+page.svelte renders three sections by calling getPlatformsByTier('one_click' | 'guided' | 'ride_along') and drives every card from the platform entry: steps, slaCopy, notes, portalUrl, and the inline controls flagged on each step. Listing state comes from the API (GET /api/distribution/:podcastId) and actions go through PUT /api/distribution/:podcastId/:platform with the action payloads described in Listings & monitoring.
Adding a platform
- Append an entry to
DISTRIBUTION_PLATFORMS. Thekeyis permanent once rows exist: it is stored inpodcast_directory_listings.platform. - No migration is needed. Migration
20260706135415_distribution_directory_listings.sqldeliberately has no CHECK constraint onplatform; validity is enforced at the API layer viaPLATFORM_KEYS, precisely so a new platform is a copy change. - If the new platform is detectable by an open API, extend the worker: the detector in
workers/distribution-monitor/src/lib/detect.ts, plus the hand-maintained lockstep constants inworkers/distribution-monitor/src/lib/run.ts(DETECTABLE_PLATFORMS,PLATFORM_NAMES, and theDETECTION_SOURCESmap). The worker cannot import the catalog, so display names are mirrored by hand. - If the platform's verification is
email_otp, no extra wiring is needed; the reveal control reuses the shared 24-hour visibility window.
Portal URLs and verification methods in the current catalog were verified against official platform docs on 2026-07-06 (see docs/planning/plans/2026-07-06-distribution.md).