Paid episodes: - server/services/cashu.ts: self-custodied Cashu wallet against a configured mint (NUT-04 mint quote -> bolt11 invoice -> mint/store proofs -> LNURL-pay payout on withdraw). Buyers pay a plain Lightning invoice, no Cashu wallet needed on their end. - routes/marketplace.ts: purchase/confirm flow, download-url paywall gate, reseller certification/revocation, earnings ledger + withdraw. - services/marketplace.ts: producer + certified-reseller source resolution, with a naive per-seller sales-count reputation signal. Discovery: - services/rss.ts: <podsteadr:source> RSS tag on priced episodes (producer + resellers, price, sales count, url) so pricing/sources are discoverable straight from the feed, not just a separate API call. Locked episodes point their <enclosure> at an info page instead of the raw file. - routes/feeds.ts: /catalog.opml lists every podcast this instance hosts, for peer podsteadr servers or any OPML-aware crawler to discover without a central directory. Frontend: episode wizard price/reseller controls, sources display, earnings dashboard. Also fixes CORS and a container-image reference: - app.ts: register @fastify/cors, open on the catalog/feed/sources endpoints (already deliberately public/crawlable) and on purchase/confirm (already accept stateless NIP-98 header auth for exactly this case). Credentials stay off, so the session cookie never crosses origins — cookie-authed admin routes stay same-origin-only. Needed so external clients like podsteadr-player can browse/buy/play from a different origin. - docker-compose.yml: fully qualify the mediamtx image reference (docker.io/bluenviron/mediamtx:1.19.2) — some Podman hosts have no unqualified-search registry configured and fail to resolve short names. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
podsteadr
Self-hosted, nostr-native podcast publishing and livestreaming.
- Log in with nostr — NIP-07 browser extension (Alby, nos2x, …); the server verifies NIP-98 signed requests. No passwords, no email.
- Upload an mp4 → it is stored on a Blossom server
(bundled, or point at any external one) and published in an RSS 2.0 podcast feed with
Podcasting 2.0
<podcast:value>lightning-address payment info. - Go live — stream from OBS (RTMP) or straight from the browser (WebRTC/WHIP) through MediaMTX; viewers watch via HLS and the stream is announced on nostr as a NIP-53 (kind 30311) live event.
- Publish recordings — live streams are recorded; one click remuxes and publishes a recording as a podcast episode.
Quick start
cp .env.example .env # edit PUBLIC_HOST if not localhost
docker compose up --build -d
Then open http://localhost:8095, log in with a NIP-07 extension, and follow the wizard.
Services / ports
| Service | Host port | Purpose |
|---|---|---|
| podsteadr | 8095 | Web UI + API + RSS feeds |
| MediaMTX | 1935 | RTMP ingest (OBS) |
| MediaMTX | 8889 (+8189/udp) | WebRTC/WHIP ingest (browser) |
| MediaMTX | 8890 | HLS playback |
| blossom-server | 8098 | Media blob storage (sha256-addressed) |
Streaming with OBS
Create a stream in the UI; it gives you:
- Server:
rtmp://<host>:1935/live - Stream key:
<streamId>?key=<secret>
The HLS playback URL (http://<host>:8890/live/<streamId>/index.m3u8) is public and never
contains the secret.
Development
docker compose up mediamtx blossom -d # backends
cd server && npm install && npm run dev # API on :8095
cd frontend && npm install && npm run dev # Vite on :5173, proxies /api + /feeds
Tests: cd server && npm test.
Data
Everything lives in named docker volumes: podsteadr-data (SQLite, server nostr key,
covers), mediamtx-recordings (stream recordings, 7-day retention), blossom-data
(media blobs). docker compose down keeps them; down -v wipes them.
Verification
- Unit tests:
cd server && npm test(35 tests). - Against a running stack:
node scripts/e2e.mjs(14 checks: login, blossom upload, feed, stream keys, MediaMTX auth webhook).
Status & internals
See docs/STATUS.md for the architecture, the full verification record, known gotchas (blossom v4 config nesting, no range requests, split-horizon blossom URL, MediaMTX polling rationale), and the remaining-work list (Archipelago packaging, git remote).