Skip to content

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:

TierMeaningCountPlatforms (catalog key)
one_clickshow.fm submits programmatically. Podcast Index is the only platform with an open write API, so it is the only member1podcast_index
guidedThe user submits at the platform's portal; the app orchestrates the tricky step (email reveal, claim token, paste-back)12apple, spotify, youtube, amazon, iheart, tunein, pandora, deezer, pocket_casts, player_fm, podcast_addict, castbox
ride_alongNothing to do; the platform mirrors Apple or Podcast Index7overcast, 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

typescript
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:

VerificationKindMeaningCatalog members
email_otpA code or link is emailed to the address inside the RSS feed; the user must open the 24-hour owner-email visibility window firstspotify, youtube, amazon, iheart, pandora, deezer, castbox
feed_tokenA token placed in the feed (Apple's podcast:txt claim flow)apple
account_onlyPlatform account plus form; no feed-level ownership checktunein
noneNo 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:

DetectionSourcePlatformAPI
itunesappleiTunes Search API (exact feed-URL match)
podcastindexpodcast_indexpodcasts/byfeedurl (exact)
spotifyspotifyWeb 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:

  • copyFeedUrl renders the copy-feed-URL affordance,
  • revealEmail renders the "reveal owner email for 24 hours" control,
  • claimToken renders the Apple verification-token control.

URL builders

Three helpers own the public URL shapes (both are lockstep-mirrored in workers, which cannot import $lib):

typescript
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

  1. Append an entry to DISTRIBUTION_PLATFORMS. The key is permanent once rows exist: it is stored in podcast_directory_listings.platform.
  2. No migration is needed. Migration 20260706135415_distribution_directory_listings.sql deliberately has no CHECK constraint on platform; validity is enforced at the API layer via PLATFORM_KEYS, precisely so a new platform is a copy change.
  3. 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 in workers/distribution-monitor/src/lib/run.ts (DETECTABLE_PLATFORMS, PLATFORM_NAMES, and the DETECTION_SOURCES map). The worker cannot import the catalog, so display names are mirrored by hand.
  4. 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).

Internal documentation - Not for public distribution