Publishing is the project-level distribution layer for Sleeper Hit Studio. It does not create or refine MP3/MP4 media. Source surfaces such as Table Read, Pitch Deck, Storyboarding, or later video tools create durable StoryArtifact assets; Publishing promotes those ready artifacts into releases, schedules them, publishes them to destinations, and tracks status.
Two kinds of destination ship. Podcast/RSS publishes the finalized Table Read MP3 already stored on StoryArtifact.manifest.audio.recordingUrl; a video release publishes its own rendered MP4 to a connected YouTube channel or TikTok account (see Video Destinations below).
Ownership Model
StoryProjectowns one or morePublishingSeriesrows.PublishingSeriesstores medium, format, prompt direction, podcast type/category metadata, feed metadata, owner metadata, its timezone, its standing approval, and provider destinations.- A
PublishingSeriesis also the platform's show. Its production is theshowsub-resource (/publishing-series/{seriesId}/show: its setup, schedule, caps, on/off and episodes), which is how a series makes its own episodes on a schedule instead of only publishing ones made elsewhere. There is no separate show model: seedocs/shows.md. A show's episodes are never in Ready to publish: the show publishes its own. PublishingReleasestores one episode/release under a series and keeps links back toStoryProject, Series Bible document,StoryPlan,StoryJob, and the promotedStoryArtifact.PublishingReleaseAssetstores the bound durable asset URL, media metadata, and source fingerprint.PublishingReleaseTargetstores destination-specific status: the feed item for RSS, and the upload, its recorded external id and its status for each connected provider.PublishingProviderConfigstores the account's connection to a provider — the tokens of a connected YouTube or TikTok account, encrypted and never read back — with the scopes the account granted.PublishingAnalyticsSnapshotstores normalized provider/feed metrics.
Episode Promotion
Publishing v1 starts after Table Read refinement is complete:
- The writer refines the episode in Table Read.
- The existing artifact finalize path durably renders the MP3.
- The writer promotes the finalized
StoryArtifactinto aPublishingRelease. - Publishing snapshots the MP3 URL, duration, source fingerprint, and metadata into
PublishingReleaseAsset. - The writer schedules, cancels, or publishes the release.
- RSS publication makes the published release visible in the first-party feed.
Promotion, scheduling, or publish means the user considers the source artifact ready. There is no separate publishing-side draft, generation, review, or approval loop in v1.
RSS V1
The first-party RSS provider uses:
- series metadata from
PublishingSeries - episode metadata from
PublishingRelease - duration from the ready
PublishingReleaseAsset; the enclosure URL is the episode download route below, never the stored file - public feed route
GET /api/v1/publishing-feeds/{seriesSlug}.xml - public episode download
GET /api/v1/publishing-feeds/{seriesSlug}/episodes/{releaseId}.mp3(BL-3196): counts the download and 302s to the ready asset's file; in the OpenAPI spec and llms.txt like the feed
RSS does not require OAuth. The feed includes Apple-compatible podcast type and category metadata from the series. Apple Podcasts and Spotify can be tracked as destinations around that feed. An audio release is published by its feed alone: the providers that accept a submission are the video destinations below.
Video Destinations: YouTube and TikTok
A video release publishes its own rendered MP4 to a provider the writer has connected, in the same publish pass that completes its RSS target. Two ship:
- YouTube uploads the MP4 to the connected channel with the resumable upload protocol, as an unlisted video that carries
containsSyntheticMedia. Connecting goes throughGET /api/v1/publishing/youtube/connect(scopehttps://www.googleapis.com/auth/youtube.upload) and its callback, which stores the tokens. A publish with no connected channel is refused with "This destination has no connected YouTube account." - TikTok uploads the MP4 through the Content Posting API v2 (
push_by_file, so no domain verification is required). Two modes, chosen by the scopes the connected account granted: a draft to the account's inbox (video.upload, which works before TikTok has audited the app) or a direct post (video.publish, which TikTok only allows once that audit has passed). The publish polls the post's status until it is terminal. Connecting goes throughGET /api/v1/publishing/tiktok/connectand its callback.
What each destination needs: a connected account for that provider, and a release whose durable asset is the rendered MP4 — the publisher fetches that file, so a release with no rendered video is refused, never posted. A retried publish resumes from the recorded upload id and never posts the same video twice.
FIRST THE APP, THEN THE WRITER: YouTube and TikTok sign in through Sleeper Hit's own app for that platform, configured once for the whole platform (server/publishing-oauth.ts). Until that app is set up, the provider is not shown to writers at all — no destination row, no connect control, and a connect attempt is refused in one sentence (server/publishing-provider-availability.ts) — so a destination that names it cannot be completed until the app exists.
Four Surfaces
Publishing operations are exposed through:
- Writers Lab Publishing tab
- Story API
/api/v1routes and OpenAPI sleeperhit publishing ...CLI commands- MCP publishing tools
The operation registry, capabilities manifest, generated surface contract, CLI metadata, and MCP schemas must stay aligned. Mutating API operations require idempotency where retrying could duplicate rows or jobs.
In Slug
Slug reaches the same publishing cores as the other surfaces. In a conversation bound to a project, on a paid workspace, Slug carries 16 publishing tools — the ones MCP serves under the same names:
list_publishing_series,create_publishing_series,update_publishing_serieslist_publishing_destinations,upsert_publishing_destinationlist_publishing_ready_recordings,remove_publishing_ready_recordinglist_publishing_releases,get_publishing_release,create_publishing_release,update_publishing_releasegenerate_publishing_release_description,refresh_published_release_mediaschedule_publishing_release,publish_publishing_release,cancel_publishing_release
A create takes title, description, medium (audio / video / short_form), format, language and
coverArtUrl. An update takes those, plus the fields that decide how the show is found and filed:
visibility (public / unlisted / private), timezone (an IANA zone its schedule runs on), slug (its
address and feed, which a show with published episodes keeps), podcastCategory and
podcastSubcategory (Apple's list, the same table a write validates against), podcastType
(episodic / serial) and explicitContent, plus status (draft / active / paused / archived). The
synthetic-disclosure line, rating, copyright, owner contact, prompt direction, metadata, the
standing-approval key, the credit caps and the publish policy are never chat inputs — recurring shows
stay with the show tools, and standing approval and caps stay with the UI, Story API, MCP and CLI.
Two of the sixteen switch a show's video destinations. list_publishing_destinations reads which accounts
a show's videos go to and which are on: a video destination with status connected is on, archived is
off. upsert_publishing_destination makes that change — provider is youtube (widescreen videos) or
tiktok (vertical videos), and status is connected or archived. Turning one back on needs the account
it was connected with, so with no connected account the call refuses and says to connect it under
Destinations on the Publishing tab, whose Connect press is the provider's own sign-in. The Destinations
card draws the state and no switch of its own: a destination is settled once and left alone, so the rare
change is the sentence the writer says to Slug.
The ready-recording pair and the two media tools are Slug's own way through a feed that already exists:
list what the project has finished and ready to add (list_publishing_ready_recordings), take one off
that list for good (remove_publishing_ready_recording), write a release's description from what its
recording performed (generate_publishing_release_description), and put the latest recording behind an
already-published audio episode without moving its place, number or date in the feed
(refresh_published_release_media).
Schedule, publish and cancel are outward-facing and take TWO CALLS. Called without confirmed, each
answers a quote — the release, the series' destinations and feed URL, and a note ending "Nothing has
been published." / "Nothing has been scheduled." / "Nothing has been canceled." — and changes nothing.
Called again with confirmed: true after the writer has said so in their own words, it goes through
the same core the schedule/publish/cancel routes call. A published release records
publishedVia: user_confirmed with publishedByKeyId: null — the writer, in the app. None of these
tools spends credits.
Slug can also list a project's connected apps (list_app_connections) on those same paid,
project-bound conversations: connection metadata only —
id, origins, sign-in page and method, which secrets are SAVED (kinds and dates) and what is still
missing. A login is typed only on the project page's Connect an app form; the chat never asks for one
and never repeats one, and no secret value ever leaves the core.
Workers
Publishing workers are queue-backed and idempotent. The release publish worker ships for scheduled/immediate publish: it completes the release's RSS target and dispatches every other target to that provider's registered publisher — YouTube and TikTok each register one in server/publishing-providers/ — so a new platform is one registration, with no worker change. Future workers can add cadence suggestions, provider status refresh, and analytics fetches.
Workers must refuse stale or live-preview-only assets. If the source artifact fingerprint changes after promotion, the release asset and targets are marked stale until the current durable artifact is promoted again.