# Zuzufy

**A creator can become their own 24/7 radio station and podcast network.**

Zuzufy combines podcast hosting, always-on internet radio, live audio/video broadcasting, creator
memberships, live chat and analytics. Listeners open any channel at any moment and immediately hear
what is *on air right now*: the exact point of the programme that everyone else is hearing.

| | |
|---|---|
| Backend | Node.js 22 LTS, Express 5, `ws` WebSockets, MariaDB 10.6+ (tested on 12.1) |
| Media | FFmpeg (probe, loudness-normalised CBR MP3 renditions, waveforms, thumbnails, WebM→MP4) |
| Live | WebRTC through a mediasoup SFU (worker per core, origin → edge router fan-out) |
| P2P delivery | Listeners exchange SHA-256-verified chunks over WebRTC data channels; signed HTTP Range fallback |
| Frontend | Vanilla ES modules + Web Components, no build step (only `mediasoup-client` is vendored) |
| Payments | Provider abstraction, Stripe Checkout + Billing Portal + webhooks |
| AI | Provider abstraction: Claude for metadata, local whisper.cpp for transcription |

---

## Contents

1. [Requirements](#1-requirements)
2. [Install Node.js](#2-install-nodejs)
3. [MariaDB setup](#3-mariadb-setup)
4. [Create the database](#4-create-the-database)
5. [Configure the environment](#5-configure-the-environment)
6. [Run migrations](#6-run-migrations)
7. [Seed development data](#7-seed-development-data)
8. [Install FFmpeg](#8-install-ffmpeg)
9. [Development server](#9-development-server)
10. [Production server](#10-production-server)
11. [WebSocket configuration](#11-websocket-configuration)
12. [WebRTC configuration](#12-webrtc-configuration)
13. [TURN configuration](#13-turn-configuration)
14. [Storage configuration](#14-storage-configuration)
15. [Tests](#15-tests)
16. [Project layout](#16-project-layout)
17. [Roadmap / deferred features](#17-roadmap--deferred-features)

**Import from YouTube.** In *Creator studio → Media*, paste a YouTube video link to fetch its audio
as an MP3 (the approach from the yt-mp3 project, run as a background job through the normal media
pipeline). Before importing, the creator must tick a statement confirming they own the content or
hold all rights to use it and take full responsibility for it. The server enforces this and records
each confirmation (exact text and version, time, hashed IP) in `media_rights_attestations`; admins
see the source and confirmation in *Admin → Content → Media*. Disable with `YOUTUBE_IMPORT_ENABLED=0`.

**Merch.** Creators sell their own merchandise and products through their store on
[Torvali](https://torvali.com) (the F9Networks commerce platform). In *Creator studio → Merch* they
connect a channel to their Torvali store by signing in once with the store-owner login (TOTP
supported; Zuzufy never stores the password or token, it only records which store belongs to the
channel). Products appear in a *Shop* tab on the channel page; the creator can feature up to 12 of them
and *pin one on air*, which pops up live for everyone listening (also from the live studio). Product
clicks go through `/go/shop/…`, are counted (30-day click stats per product) and land on the Torvali
product page with UTM tags. Cart, checkout, payments, inventory, shipping and orders stay in Torvali.
Set `TORVALI_URL` (and optionally `TORVALI_API_URL`) to enable it.

Further reading: [docs/architecture.md](docs/architecture.md) · [docs/deployment.md](docs/deployment.md) ·
[docs/security.md](docs/security.md) · [docs/api/openapi.yaml](docs/api/openapi.yaml) · [docs/api/websocket.md](docs/api/websocket.md)

---

## 1. Requirements

- Linux (Ubuntu 22.04/24.04, Debian 12) or macOS for development
- Node.js **22 LTS** or newer
- MariaDB **10.6+** (uses `FOR UPDATE SKIP LOCKED`, JSON, FULLTEXT)
- FFmpeg 5+ with `libmp3lame` (and `libx264` for video recordings)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for *Import from YouTube* (`brew install yt-dlp` / `pipx install yt-dlp`).
  YouTube changes often — keep it current (`yt-dlp -U`, or a weekly cron). Admin → System shows the installed version.
- For live broadcasting: UDP ports `40000–49999` open to the SFU host (configurable), plus a TURN server for listeners behind strict NATs
- A reverse proxy with TLS in production (Nginx or Apache; examples in `docs/deployment.md`)

## 2. Install Node.js

```bash
# Ubuntu / Debian (NodeSource)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs build-essential python3
# macOS
brew install node@22
```

`mediasoup` ships prebuilt workers for common platforms; `build-essential` and `python3` are only
needed if npm has to compile it.

## 3. MariaDB setup

```bash
sudo apt-get install -y mariadb-server     # or: brew install mariadb && brew services start mariadb
sudo mariadb-secure-installation
```

## 4. Create the database

```sql
CREATE DATABASE zuzufy CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'zuzufy'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON zuzufy.* TO 'zuzufy'@'localhost';
-- optional, for the integration test suite:
CREATE DATABASE zuzufy_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON zuzufy_test.* TO 'zuzufy'@'localhost';
```

## 5. Configure the environment

```bash
cp .env.example .env
# Generate secrets:
openssl rand -hex 32   # SESSION_SECRET
openssl rand -hex 32   # ENCRYPTION_KEY  (encrypts TOTP seeds at rest)
openssl rand -hex 24   # MEDIA_SIGNING_SECRET (shared with Nginx secure_link)
```

Every variable is documented inline in `.env.example`. **Never commit `.env`.** In production the
server refuses to start without valid secrets and an `https://` `APP_URL`.

## 6. Run migrations

```bash
npm install
npm run migrate            # apply all pending migrations
npm run migrate:status     # list applied/pending
npm run migrate:down 1     # roll back the most recent migration (each has a .down.sql)
```

Migrations live in `migrations/NNN_name.up.sql` / `.down.sql` and are tracked in `schema_migrations`.
Installation never requires editing tables by hand.

## 7. Seed development data

```bash
npm run seed
```

Creates three demo accounts (password `Zuzufy-demo-2026`, override with `SEED_PASSWORD`):
`admin@zuzufy.local`, `creator@zuzufy.local` (two 24/7 stations with generated test-signal audio,
playlists, published schedules, episodes and membership tiers) and `listener@zuzufy.local`.
The seed refuses to run when `NODE_ENV=production`.

For a fresh production install, create the first administrator from the server shell:

```bash
npm run create-admin -- --email you@example.com --name "Your Name"
```

or visit `/setup` (only works while no admin exists; in production it also requires `ADMIN_BOOTSTRAP_TOKEN`).

## 8. Install FFmpeg

```bash
sudo apt-get install -y ffmpeg      # Ubuntu/Debian
brew install ffmpeg                 # macOS
ffmpeg -encoders | grep -E "libmp3lame|libx264"
```

Set `FFMPEG_PATH` / `FFPROBE_PATH` if the binaries are not on `PATH`.

## 9. Development server

```bash
npm run dev        # node --watch; http://localhost:8900
```

With `EMAIL_PROVIDER=log`, verification and password-reset links are written to the log (and
returned to the browser console in development) so every flow works without SMTP.

## 10. Production server

Run under systemd behind Nginx with TLS. Node controls authentication, scheduling and
synchronisation; **Nginx (or a CDN) serves media bytes** using the same signed-URL scheme, so Node
never proxies audio. Full walkthrough, Nginx config, systemd unit and hardening checklist:
[docs/deployment.md](docs/deployment.md).

```bash
NODE_ENV=production SERVE_MEDIA=0 TRUST_PROXY=1 node server.js
```

## 11. WebSocket configuration

The WebSocket endpoint is `/ws` on the same origin. Behind a proxy, forward `Upgrade`/`Connection`
headers and raise read timeouts (see deployment guide). Set `WEBSOCKET_URL` only if WebSockets are
served from a different host. Cross-site WebSocket connections are rejected by Origin check.

## 12. WebRTC configuration

Live broadcasting uses mediasoup. Configure:

| Variable | Meaning |
|---|---|
| `LIVE_LISTEN_IP` | Interface to bind (default `0.0.0.0`) |
| `LIVE_ANNOUNCED_IP` | Public IP announced in ICE candidates (**required in production**) |
| `LIVE_RTC_MIN_PORT` / `LIVE_RTC_MAX_PORT` | UDP/TCP port range (default 40000–49999) |
| `LIVE_WORKERS` | mediasoup workers (default: CPU cores) |
| `LIVE_VIEWERS_PER_ROUTER` | Viewers per edge router before fanning out to another worker (default 400) |
| `LIVE_HOST_GRACE_SECONDS` | How long a disconnected host may rejoin before failover (default 60) |
| `STUN_SERVER` | STUN URL for browsers (default Google STUN) |

## 13. TURN configuration

Listeners behind symmetric NAT or restrictive firewalls need TURN for live WebRTC. Install coturn
(config in `docs/deployment.md`) and set `TURN_SERVER` (comma-separated URLs, e.g.
`turn:turn.example.com:3478?transport=udp,turns:turn.example.com:5349`), `TURN_USERNAME`,
`TURN_PASSWORD`. Peer-to-peer chunk sharing deliberately uses STUN only (it is an optimisation;
HTTP is always the fallback).

## 14. Storage configuration

`StorageService` abstracts where bytes live (`STORAGE_PROVIDER=local`, `STORAGE_PATH`). Keys are
`public/…` (artwork, unsigned), `private/…` (audio/video, served only via expiring signed URLs) and
`tmp/…` (never served). `PUBLIC_MEDIA_URL` may point at a CDN; signatures use the Nginx
`secure_link` MD5 format so the CDN/edge can validate them. See `docs/architecture.md` for adding an
S3-compatible provider.

## 15. Tests

```bash
npm test          # unit + integration tests (engine, auth/2FA, IDOR, uploads, broadcast API,
                  # WebSocket, chat moderation, RSS validity, signed media, P2P manifests/tracker, AI, web push, YouTube import, merch)
npm run test:e2e  # Playwright acceptance test (spec §88) against a running server: two browsers in
                  # sync, P2P chunk exchange, track transitions, refresh/reconnect resync, schedule
                  # changes, live override over WebRTC, recording → episode → RSS, subscription, analytics
```

The integration suite uses the `*_test` database (`TEST_DATABASE_NAME`) and wipes it on each run.
The E2E test needs `npx playwright install chromium` once.

## 16. Project layout

```
server.js                 entry point (HTTP + WebSocket + SFU + workers + periodic jobs)
src/config                environment parsing and validation
src/app.js                Express app: security headers, CSRF, sessions, routes, SPA fallback
src/routes, controllers   HTTP layer (thin)
src/services              business logic (auth, channels, schedule, episodes, subscriptions, live, chat…)
src/repositories          SQL for core entities
src/broadcast             BroadcastEngine (pure clock), BroadcastManager (state machine, timers), validator
src/websocket             WebSocket server + room hub
src/live                  mediasoup SFU + WebRTC signalling
src/p2p                   chunk manifests + peer swarm tracker
src/media                 FFmpeg wrappers, upload handling, durable processing worker
src/storage               StorageService + providers
src/payments              PaymentService + Stripe provider
src/ai                    AIProcessingService + providers
src/analytics             privacy-preserving listening analytics
src/notifications         in-app notifications + email
migrations/               versioned SQL (up/down)
public/                   web client (ES modules, CSS design system, PWA)
public/js/player          ServerClock, LinearBroadcastPlayer, LiveClient, P2P swarm/stream, PlayerController
tests/                    node:test unit + integration suites; tests/e2e Playwright acceptance
docs/                     architecture, deployment, security, OpenAPI, WebSocket protocol
```

## 17. Roadmap / deferred features

Implemented and tested: everything in the MVP milestone plus live WebRTC broadcasting, guests,
recording, FFmpeg processing, AI processing, paid memberships (Stripe), advertising, peer-to-peer
delivery, browser push notifications (Web Push/VAPID — `npm run vapid` to create keys), admin portal and PWA.

Intentionally deferred (clearly marked in the UI where relevant):

- **HLS live packaging** for audiences beyond what SFU edge routers serve; the SFU already fans out
  across all CPU cores on one machine.
- **Mid-roll insertion inside on-demand episodes** (pre/post-roll and in-schedule ads work).
- **Stripe Connect automated payouts** – payouts are recorded and marked paid by admins.
- **Multi-instance realtime fan-out** – the Hub has an adapter interface for Redis pub/sub; a single
  instance is used until it is needed.
