chore: rename project from podpuddle to podsteadr

Renames the repo directory and every podpuddle/PODPUDDLE reference
across code, config, and docs to podsteadr/PODSTEADR (package names,
Docker Compose project/service/volume names, env var names, UI/RSS
strings). Existing Docker volume data (uploaded blobs, mediamtx
recordings, the server's sqlite DB and its nostr identity key) was
migrated to new podsteadr_-prefixed volumes with matching filenames
so it isn't orphaned by the rename.
This commit is contained in:
2026-07-25 01:11:42 +00:00
parent 26d166a7e8
commit 2d61abc40c
20 changed files with 49 additions and 49 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Hostname or IP your users will reach this box on (no scheme, no port)
PUBLIC_HOST=localhost
# Where the podpuddle UI/API is reachable from browsers
# Where the podsteadr UI/API is reachable from browsers
PUBLIC_URL=http://${PUBLIC_HOST}:8095
# Public endpoints handed out for streaming / playback / uploads
+3 -3
View File
@@ -1,4 +1,4 @@
# podpuddle
# podsteadr
Self-hosted, nostr-native podcast publishing and livestreaming.
@@ -26,7 +26,7 @@ Then open http://localhost:8095, log in with a NIP-07 extension, and follow the
| Service | Host port | Purpose |
|---|---|---|
| podpuddle | 8095 | Web UI + API + RSS feeds |
| podsteadr | 8095 | Web UI + API + RSS feeds |
| MediaMTX | 1935 | RTMP ingest (OBS) |
| MediaMTX | 8889 (+8189/udp) | WebRTC/WHIP ingest (browser) |
| MediaMTX | 8890 | HLS playback |
@@ -54,7 +54,7 @@ Tests: `cd server && npm test`.
## Data
Everything lives in named docker volumes: `podpuddle-data` (SQLite, server nostr key,
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.
+1 -1
View File
@@ -1,4 +1,4 @@
# blossom-server (v4.x) configuration for podpuddle.
# blossom-server (v4.x) configuration for podsteadr.
# Uploads require a signed nostr auth event (BUD-02, kind 24242);
# reads are public so podcast apps can fetch enclosures.
+7 -7
View File
@@ -1,9 +1,9 @@
name: podpuddle
name: podsteadr
services:
podpuddle:
podsteadr:
build: .
container_name: podpuddle
container_name: podsteadr
restart: unless-stopped
ports:
- "8095:8095"
@@ -21,7 +21,7 @@ services:
BLOSSOM_URL_INTERNAL: http://blossom:3000
NOSTR_RELAYS: ${NOSTR_RELAYS:-wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band}
volumes:
- podpuddle-data:/data
- podsteadr-data:/data
- mediamtx-recordings:/recordings:ro
depends_on:
- mediamtx
@@ -29,7 +29,7 @@ services:
mediamtx:
image: bluenviron/mediamtx:1.19.2
container_name: podpuddle-mediamtx
container_name: podsteadr-mediamtx
restart: unless-stopped
ports:
- "1935:1935" # RTMP ingest
@@ -45,7 +45,7 @@ services:
blossom:
image: ghcr.io/hzrd149/blossom-server:4
container_name: podpuddle-blossom
container_name: podsteadr-blossom
restart: unless-stopped
ports:
- "8098:3000"
@@ -54,6 +54,6 @@ services:
- blossom-data:/app/data
volumes:
podpuddle-data:
podsteadr-data:
mediamtx-recordings:
blossom-data:
+13 -13
View File
@@ -1,4 +1,4 @@
# podpuddle — project status & handoff
# podsteadr — project status & handoff
_Last updated: 2026-07-10. State: **working, verified end-to-end, parked.**_
@@ -18,7 +18,7 @@ fully standalone compose stack today.
## How to resume
```sh
cd ~/podpuddle
cd ~/podsteadr
docker compose up -d # data volumes were kept; everything resumes as-is
node scripts/e2e.mjs # 14 automated checks against the running stack
```
@@ -26,14 +26,14 @@ node scripts/e2e.mjs # 14 automated checks against the running stack
Open http://localhost:8095 and log in with a NIP-07 extension (Alby, nos2x).
For a non-localhost box, edit `.env` (`PUBLIC_HOST=…`) and `docker compose up -d --build`.
State that persists in the named volumes: `podpuddle-data` (SQLite DB, the
State that persists in the named volumes: `podsteadr-data` (SQLite DB, the
server's nostr key in `config/server-nostr-key`, admin = first pubkey that ever
logged in), `blossom-data` (media blobs), `mediamtx-recordings` (stream
recordings, 7-day retention). `docker compose down -v` wipes all of it.
Note: the automated e2e run left test data behind (an "E2E Test Show" podcast
with 2 episodes, test streams, and the e2e identity as **admin**). The e2e
identity's secret key is in `/tmp/podpuddle-e2e-key` (hex). For real use you
identity's secret key is in `/tmp/podsteadr-e2e-key` (hex). For real use you
probably want a clean slate: `docker compose down -v && docker compose up -d`,
then log in with your own extension first so *your* pubkey becomes admin.
@@ -43,9 +43,9 @@ Three containers on one compose network:
| Container | Image | Host ports | Role |
|---|---|---|---|
| `podpuddle` | built from `Dockerfile` (node:22 + ffmpeg) | 8095 | Fastify API + built Vue UI + RSS feeds |
| `podpuddle-mediamtx` | `bluenviron/mediamtx:1.19.2` | 1935 (RTMP), 8889 (WHIP), 8189/udp (ICE), 8890→8888 (HLS) | ingest + HLS output + recording |
| `podpuddle-blossom` | `ghcr.io/hzrd149/blossom-server:4` (4.4.1) | 8098→3000 | sha256-addressed media blobs |
| `podsteadr` | built from `Dockerfile` (node:22 + ffmpeg) | 8095 | Fastify API + built Vue UI + RSS feeds |
| `podsteadr-mediamtx` | `bluenviron/mediamtx:1.19.2` | 1935 (RTMP), 8889 (WHIP), 8189/udp (ICE), 8890→8888 (HLS) | ingest + HLS output + recording |
| `podsteadr-blossom` | `ghcr.io/hzrd149/blossom-server:4` (4.4.1) | 8098→3000 | sha256-addressed media blobs |
Key flows:
@@ -55,7 +55,7 @@ Key flows:
directly on any API route (scripting). First pubkey to log in becomes admin.
- **Episode upload** — browser hashes the file (`crypto.subtle`, ≤2 GB), signs a
kind-24242 BUD-02 auth via NIP-07, PUTs **directly to blossom** (file never
transits podpuddle), then registers `{sha256, size, …}` with
transits podsteadr), then registers `{sha256, size, …}` with
`POST /api/podcasts/:id/episodes`; server HEAD-verifies the blob before
accepting. Enclosure URL = `<blossom>/<sha256>.mp4`.
- **RSS** — `GET /feeds/:podcastId/feed.xml`, RSS 2.0 + itunes + podcast
@@ -68,7 +68,7 @@ Key flows:
travels as `?key=` (OBS stream key) or WHIP `Bearer`, is stored only as a
sha256 hash, and never appears in viewer URLs. MediaMTX delegates auth to
`POST /api/mediamtx/auth` (2xx = allow); reads are public.
- **Status / NIP-53** — podpuddle polls the MediaMTX API (`:9997/v3/paths/list`,
- **Status / NIP-53** — podsteadr polls the MediaMTX API (`:9997/v3/paths/list`,
every 3 s) and flips `planned → live → ended` (the stock MediaMTX image has no
shell, so `runOn*` curl hooks are impossible — polling is deliberate). On each
transition it (re)publishes the kind-30311 live event **signed by the server's
@@ -76,7 +76,7 @@ Key flows:
with the user `p`-tagged as `host`, to the relay list in Settings. 30311 is
replaceable, which is why the server key signs rather than the user's.
- **Recordings** — MediaMTX records fmp4 segments to the shared volume;
podpuddle mounts it read-only, lists per-stream files, and
podsteadr mounts it read-only, lists per-stream files, and
`POST /api/streams/:id/recordings/publish` remuxes (`ffmpeg -c copy
+faststart`), uploads to blossom **with the server's key** (the only
server-signed upload path), and creates the episode.
@@ -112,7 +112,7 @@ frontend/ # Vue 3 + Vite + Tailwind + Pinia
admin claim, episode blob verification, feed ETag/304, stream key
create/verify/rotate, webhook allow/deny paths, settings admin gating).
- `node scripts/e2e.mjs` against the compose stack — **14/14**.
- Live leg (manual, via ffmpeg in the podpuddle container):
- Live leg (manual, via ffmpeg in the podsteadr container):
- RTMP publish with the right key → HLS `index.m3u8` 200 (after MediaMTX's
`?cookieCheck=1` redirect — use `curl -L`).
- Wrong key → connection reset by MediaMTX (webhook denial confirmed live).
@@ -139,7 +139,7 @@ frontend/ # Vue 3 + Vite + Tailwind + Pinia
streaming/seeking is degraded. Options later: upgrade to the Deno rewrite,
or front blossom with nginx.
3. **Split-horizon blossom URL**: browsers use `http://<host>:8098`, but inside
the podpuddle container that address doesn't exist. `BLOSSOM_URL_INTERNAL`
the podsteadr container that address doesn't exist. `BLOSSOM_URL_INTERNAL`
(compose sets `http://blossom:3000`) is used server-side whenever the active
blossom URL equals the bundled default; feeds always carry the public URL.
4. **Stock MediaMTX image has no shell** — `runOnReady`-style hooks can't run
@@ -154,7 +154,7 @@ frontend/ # Vue 3 + Vite + Tailwind + Pinia
## Remaining work
- **Archipelago packaging** (the original deployment target, archy repo):
`apps/podpuddle/manifest.yml` + `apps/podpuddle-mediamtx` + `apps/podpuddle-blossom`
`apps/podsteadr/manifest.yml` + `apps/podsteadr-mediamtx` + `apps/podsteadr-blossom`
following the `apps/btcpay-server` (dependencies) + `apps/monero-ui`
(`container.build` on `/opt/archipelago/docker/...`) patterns; bind volumes
under `/var/lib/archipelago/<app>`; add ports **8095, 1935, 8889, 8189/udp,
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>podpuddle</title>
<title>podsteadr</title>
</head>
<body>
<div id="app"></div>
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "podpuddle-frontend",
"name": "podsteadr-frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "podpuddle-frontend",
"name": "podsteadr-frontend",
"version": "0.1.0",
"dependencies": {
"hls.js": "^1.6.0",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "podpuddle-frontend",
"name": "podsteadr-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
+1 -1
View File
@@ -16,7 +16,7 @@ async function logout() {
<header class="border-b border-slate-200 bg-white">
<div class="mx-auto flex max-w-4xl items-center justify-between px-4 py-3">
<RouterLink to="/" class="flex items-center gap-2 text-lg font-bold text-puddle-700">
<span aria-hidden="true">💧</span> podpuddle
<span aria-hidden="true">💧</span> podsteadr
</RouterLink>
<nav v-if="auth.pubkey" class="flex items-center gap-4 text-sm">
<RouterLink to="/" class="hover:text-puddle-600">Home</RouterLink>
+1 -1
View File
@@ -1,4 +1,4 @@
// Thin fetch wrapper for the podpuddle API (session cookie auth).
// Thin fetch wrapper for the podsteadr API (session cookie auth).
export class ApiError extends Error {
constructor(public status: number, message: string) {
+1 -1
View File
@@ -1,6 +1,6 @@
// Browser-direct Blossom (BUD-02) upload: hash locally, sign the kind-24242
// authorization with the user's NIP-07 extension, PUT straight to the blossom
// server. The file never transits podpuddle.
// server. The file never transits podsteadr.
import { nip07 } from './nip07';
+1 -1
View File
@@ -27,7 +27,7 @@ async function login() {
<div class="mx-auto max-w-md">
<div class="card text-center">
<div class="mb-2 text-4xl" aria-hidden="true">💧</div>
<h1 class="mb-1 text-2xl font-bold">podpuddle</h1>
<h1 class="mb-1 text-2xl font-bold">podsteadr</h1>
<p class="mb-6 text-slate-500">Publish podcasts and go live on nostr, from your own server.</p>
<template v-if="hasNip07()">
+4 -4
View File
@@ -1,6 +1,6 @@
# MediaMTX configuration for podpuddle.
# MediaMTX configuration for podsteadr.
# Ingest: RTMP (OBS) + WebRTC/WHIP (browser). Output: HLS. Publish auth is
# delegated to podpuddle via HTTP; stream status is polled from the API.
# delegated to podsteadr via HTTP; stream status is polled from the API.
logLevel: info
@@ -9,7 +9,7 @@ apiAddress: :9997
# ---- authentication ------------------------------------------------------
authMethod: http
authHTTPAddress: http://podpuddle:8095/api/mediamtx/auth
authHTTPAddress: http://podsteadr:8095/api/mediamtx/auth
authHTTPExclude:
- action: api
- action: metrics
@@ -45,5 +45,5 @@ pathDefaults:
paths:
# Streams live at live/<streamId>; publish requires the stream secret,
# which podpuddle checks in the auth webhook.
# which podsteadr checks in the auth webhook.
"~^live/[A-Za-z0-9]+$": {}
+3 -3
View File
@@ -10,12 +10,12 @@ import { createHash } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { finalizeEvent, generateSecretKey } from '../server/node_modules/nostr-tools/lib/esm/pure.js';
const BASE = process.env.PODPUDDLE_URL ?? 'http://localhost:8095';
const BASE = process.env.PODSTEADR_URL ?? 'http://localhost:8095';
const BLOSSOM = process.env.BLOSSOM_URL ?? 'http://localhost:8098';
// Persist the test identity across runs — the first pubkey to log in becomes
// admin, so reruns must reuse it for the settings test to pass.
const KEY_FILE = '/tmp/podpuddle-e2e-key';
const KEY_FILE = '/tmp/podsteadr-e2e-key';
let sk;
if (existsSync(KEY_FILE)) {
sk = Uint8Array.from(Buffer.from(readFileSync(KEY_FILE, 'utf8').trim(), 'hex'));
@@ -89,7 +89,7 @@ ok('podcast created', podcast.status === 201 && podcast.json.id);
const podcastId = podcast.json.id;
// ---- blossom upload (BUD-02, signed like the browser would) ----
const media = readFileSync(process.env.TEST_MP4 ?? '/tmp/podpuddle-test.mp4');
const media = readFileSync(process.env.TEST_MP4 ?? '/tmp/podsteadr-test.mp4');
const sha256 = createHash('sha256').update(media).digest('hex');
const now = Math.floor(Date.now() / 1000);
const auth24242 = finalizeEvent(
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "podpuddle-server",
"name": "podsteadr-server",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "podpuddle-server",
"name": "podsteadr-server",
"version": "0.1.0",
"dependencies": {
"@fastify/cookie": "^11.0.2",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "podpuddle-server",
"name": "podsteadr-server",
"version": "0.1.0",
"private": true,
"type": "module",
+3 -3
View File
@@ -29,7 +29,7 @@ function nip98Header(url: string, method: string): string {
}
beforeAll(async () => {
dataDir = mkdtempSync(join(tmpdir(), 'podpuddle-test-'));
dataDir = mkdtempSync(join(tmpdir(), 'podsteadr-test-'));
const config = loadConfig({
DATA_DIR: dataDir,
PUBLIC_URL: 'http://localhost:8095',
@@ -59,7 +59,7 @@ describe('auth', () => {
expect(res.json().pubkey).toBe(pk);
expect(res.json().isAdmin).toBe(true); // first login claims admin
const setCookie = res.headers['set-cookie'] as string;
expect(setCookie).toContain('podpuddle_session=');
expect(setCookie).toContain('podsteadr_session=');
cookie = setCookie.split(';')[0];
});
@@ -168,7 +168,7 @@ describe('streams + mediamtx auth webhook', () => {
method: 'POST',
url: '/api/streams',
headers: { cookie },
payload: { title: 'Live Test', summary: 'hi', hashtags: ['podpuddle'] },
payload: { title: 'Live Test', summary: 'hi', hashtags: ['podsteadr'] },
});
expect(res.statusCode).toBe(201);
const body = res.json();
+1 -1
View File
@@ -39,7 +39,7 @@ export interface BuildAppOptions {
export async function buildApp(opts: BuildAppOptions): Promise<FastifyInstance> {
const { config } = opts;
const db = openDatabase(opts.dbPath ?? join(config.DATA_DIR, 'podpuddle.sqlite3'));
const db = openDatabase(opts.dbPath ?? join(config.DATA_DIR, 'podsteadr.sqlite3'));
const settings = new SettingsService(db, config);
const serverKey = loadServerKey(config.DATA_DIR);
+1 -1
View File
@@ -5,7 +5,7 @@ import type { DB } from '../db/database.js';
import type { Config } from '../config.js';
import { Nip98Error, verifyNip98 } from '../services/nip98.js';
export const SESSION_COOKIE = 'podpuddle_session';
export const SESSION_COOKIE = 'podsteadr_session';
declare module 'fastify' {
interface FastifyInstance {
+1 -1
View File
@@ -92,7 +92,7 @@ export function buildFeedXml(podcast: Podcast, episodes: Episode[], ctx: FeedCon
` <link>${xmlEscape(link)}</link>`,
` <description>${xmlEscape(podcast.description)}</description>`,
` <language>${xmlEscape(podcast.language)}</language>`,
` <generator>podpuddle</generator>`,
` <generator>podsteadr</generator>`,
` <lastBuildDate>${rfc2822(lastPub)}</lastBuildDate>`,
` <atom:link href="${xmlEscape(feedUrl)}" rel="self" type="application/rss+xml"/>`,
` <podcast:guid>${podcast.podcast_guid}</podcast:guid>`,