- Editable podcast settings: new /podcasts/:id/settings page, reusing PodcastForm.vue in an edit mode (PUT instead of POST) since it was previously create-only with no way to fix a field (e.g. lightning address) after the fact. - Recorded episodes can now be priced same as uploads: "Publish recording" gained an optional price_sats field, wired through the existing episode paywall machinery. Live streams themselves stay unpaywalled by design — only the resulting recording can be priced. - Accept Cashu tokens as an alternative to a Lightning invoice: POST .../purchase/token redeems a pasted token directly (via the mint's swap/receive flow) and finalizes the purchase in one step, no quote/confirm round trip. A token worth more than the price is treated as a tip (seller gets the full amount); worth less is rejected. Added a "pay with a Cashu token instead" option next to the existing invoice flow. - cashu.ts: fixed payout() always requesting an invoice for the full held balance with no room for the mint's routing-fee reserve, which made a balance that exactly matched one sale's price permanently unwithdrawable (needed slightly more than held to cover the fee). Now shrinks the request and requotes once if the first quote doesn't fit. - docker-compose.yml / mediamtx.yml: renamed the podsteadr container's DNS alias away from the literal string "podsteadr" — on a host whose own hostname is "podsteadr", cloud-init's self-hostname /etc/hosts entry shadowed the container-network alias, so mediamtx's auth webhook callback resolved to the wrong address and rejected every RTMP publish attempt. 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).