# openalice-radio

> Streaming radio service: shared + per-tenant channels with scheduler, now-playing WS fanout, and tracks CRUD. BGM source for the live page picker and Hub /account/radio BFF.

## At a glance
| field | value |
|---|---|
| kind | `service` |
| priority | `P2` |
| default branch | `mvp` |
| last activity | 2026-06-16T02:07:54+00:00 |
| features catalogued | **19** |
| runtime deps declared | **3** |

## Manifest
`.atlas-deps.yml` — parsed cleanly: **yes**

### Runtime deps
- **`openalice-auth`** · `shared-jwt` · via `SHARED_JWT_SECRET` — Verify oa_session JWTs on owner-gated routes (create channel, add/remove track, share-track).
- **`openalice-axum-common`** · `other` · via `cargo` — Shared axum plumbing: CORS policy, JWT verifier trait, AuthUser extractor, internal HTTP client.
- **`openalice-cdn`** · `http` · via `RADIO_CDN_BASE` · _optional_ — Base URL for resolving relative audio_url values in now-playing WS frames and track listings.

### Consumed env
- `DATABASE_URL`
- `SHARED_JWT_SECRET`
- `RADIO_PUBLIC_URL`
- `RADIO_CDN_BASE`
- `RADIO_PORT`
- `RADIO_DB_PASSWORD`
- `RUST_LOG`

### Owns tables
- `radio_channels`
- `radio_tracks`
- `radio_play_state`
- `radio_subscribers`

### Events published
- `radio.now_playing`

### Exposes
- `docker_image` — openalice-radio
- `http_endpoint` — https://radio.blal.pro/v1/radio/*
- `ws_endpoint` — wss://radio.blal.pro/v1/radio/channels/:slug/subscribe

## Env drift

**Code reads, manifest omits (1):**
- `DOMAIN`

**Manifest declares, code unused (1):**
- `RUST_LOG`

## Live reachability
- last probe: **down** · `https://radio.blal.pro` · HTTP 404 · 61ms
- uptime 24h: **0.0%**

## Features (19)

### radio/auth
- **`radio.auth.optional-extractor`** · _alpha_ · since 0.1.0 — OptionalAuth FromRequestParts extractor — yields Some(AuthUser) when JWT validates, None on any failure (missing header / invalid sig / expired) without 401-ing the request; required so /channels can mix public shared listings with private owner-scoped rows in one handler.  
  *src/handlers/channels.rs:28*

### radio/channels
- **`radio.channels.list`** — channels plus (when a valid JWT is presented) the caller's private channels; unauthenticated callers see shared-only. @feature radio.channels.create — POST /v1/radio/channels creates a per-tenant private channel (slug [a-z0-9-]+, 409 on collision) and immediately spawns its scheduler task so the new channel starts ticking.  
  *src/handlers/channels.rs:3*
- **`radio.channels.now-playing`** · _alpha_ · since 0.1.0 — GET /v1/radio/channels/{slug}/now-playing returns a one-shot snapshot of the current scheduler cursor for a channel; HTTP fallback for clients that can't open a WS.  
  *src/handlers/channels.rs:104*
- **`radio.channels.require-owner`** · _alpha_ · since 0.1.0 — Owner-gate helper — resolves `radio_channels` row by id and 403s when the caller's tenant_id does not match `owner_tenant_id` (or the channel is shared, not private); single chokepoint for every tracks/share mutation.  
  *src/handlers/channels.rs:188*

### radio/crate
- **`radio.lib.crate`** · _alpha_ · since 0.1.0 — openalice-radio library — exposes AppState, handlers, hub, scheduler, seed, and db modules so integration tests + the bin can share the same router shape without duplicating wiring.  
  *src/lib.rs:7*

### radio/db
- **`radio.db.connect`** · _alpha_ · since 0.1.0 — Async PgPool builder used by the binary bootstrap; centralizes pool tuning so every handler shares the same connection limits + acquire deadline.  
  *src/db.rs:24*
- **`radio.db.pool`** · _alpha_ · since 0.1.0 — Postgres pool factory — wraps PgPoolOptions with `max_connections=10` + `acquire_timeout=5s` so handlers fast-fail to 503 instead of hanging when the DB is overloaded.  
  *src/db.rs:6*

### radio/health
- **`radio.health.endpoint`** · _alpha_ · since 0.1.0 — GET /healthz returns 200 "ok" for Traefik liveness probing; no DB touch so it stays green even when Postgres is unreachable.  
  *src/handlers/health.rs:3*

### radio/hub
- **`radio.hub.fanout`** — frames from the scheduler to every WS subscriber on a channel in O(1) publish; lock-free on the hot path via dashmap entry().  
  *src/hub.rs:13*
- **`radio.hub.now-playing-frame`** · _alpha_ · since 0.1.0 — Serialized NowPlaying wire frame — channel_id/slug + optional track metadata + ISO-8601 `started_at` so clients can compute the live playhead as `now - started_at` (server clock skew tolerant).  
  *src/hub.rs:33*

### radio/metrics
- **`radio.metrics.endpoint`** · _alpha_ · since 0.1.0 — GET /metrics Prometheus exposition — emits `radio_build_info` gauge from a OnceLock-cached Registry so Prometheus confirms the scrape target without per-request alloc.  
  *src/handlers/metrics.rs:8*

### radio/scheduler
- **`radio.scheduler.bootstrap`** · _alpha_ · since 0.1.0 — spawn_loop discovers every row in `radio_channels` at boot and spawns one per-channel tick task; newly-created channels are added at create-time by the channels handler so the boot scan only needs to run once.  
  *src/scheduler.rs:65*
- **`radio.scheduler.tick`** — radio_play_state cursor when elapsed >= duration_ms; recovers from operator mutations within one tick cycle. @feature radio.scheduler.now-playing — current_now_playing() computes the snapshot NowPlaying frame from DB without advancing the cursor; called by both the HTTP now-playing endpoint and the WS subscribe handler for the initial frame.  
  *src/scheduler.rs:18*

### radio/seed
- **`radio.seed.shared-channels`** — platform-owned shared channels (lo-fi-24-7, synthwave-24-7, jazz-cafe, ambient-spacy) with CC0 CDN tracks; base URL overridable via RADIO_CDN_BASE.  
  *src/seed.rs:5*

### radio/server
- **`radio.server.bootstrap`** · _alpha_ · since 0.1.0 — Axum router boot — wires DB pool + Hub + handlers, seeds shared channels, spawns per-channel scheduler tasks, and binds the HTTP/WS listener on RADIO_PORT.  
  *src/main.rs:15*

### radio/state
- **`radio.state.app`** · _alpha_ · since 0.1.0 — AppState — cheap-clone bundle of (PgPool, Hub, jwt_secret, public_url) injected into every Axum handler and the scheduler task; implements JwtVerifier so the shared HS256 validator from openalice-axum-common can extract AuthUser from this state.  
  *src/state.rs:15*

### radio/tracks
- **`radio.tracks.crud`** — provides owner-gated track append and removal; order_index auto-assigned as MAX+1 when not supplied. @feature radio.tracks.share — POST /v1/radio/channels/:id/share-track copies a private track into a shared channel; intended path for publishing Lyria-generated music beds to the community streams.  
  *src/handlers/tracks.rs:3*
- **`radio.tracks.list`** · _alpha_ · since 0.1.0 — GET /v1/radio/channels/{id}/tracks lists tracks in `order_index` order; shared channels are public, private channels require a JWT whose tenant matches `owner_tenant_id` (403 otherwise).  
  *src/handlers/tracks.rs:216*

### radio/ws
- **`radio.ws.subscribe`** — called BEFORE the DB snapshot so no scheduler ticks are lost between snapshot fetch and subscription registration; lagged receivers are disconnected so clients reconnect cleanly.  
  *src/handlers/ws.rs:13*

---

*Generated by [openalice-atlas](https://atlas.blal.pro) — single source of truth for the openalicelabs ecosystem.*
