diff --git a/CLAUDE.md b/CLAUDE.md index 4965910..bf107a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,12 +36,13 @@ CI runs the full suite only on Linux (service container); the Windows leg is a c ## Architecture & conventions - **Library/binary split.** `src/main.rs` is only runtime plumbing (load `.env`, init tracing, connect pool, run migrations, bind, serve with graceful shutdown). The router is built by `waveflow_server::app(config, state)` in `src/lib.rs` so integration tests spawn the *same* app in-process (`tests/support.rs::spawn_app`). Put logic behind `app()`, not in `main`. -- **`AppState`** (`src/lib.rs`) holds the shared singletons threaded through every handler — currently just the `PgPool` (cheap to clone, `Arc`-backed). Add new singletons here. +- **`AppState`** (`src/lib.rs`) holds the shared singletons threaded through every handler. Cheap to clone — every field is `Arc`-backed. Fields today: `db: PgPool`, `jwt_verifier: Arc` (Phase 1.d.1), `stream_ctx: Option>` (Phase 1.e — `None` disables streaming), `sync: SyncHub` (Phase 1.f — broadcast `Sender` + `DashMap<(user_id, device_id), AckEntry>` + flush/compaction tasks; tests construct via `SyncHub::for_tests(pool)` which skips the background loops so `flush_acks` / `compact_once` can be driven by hand for determinism). Add new singletons here. - **API is one file per resource** under `src/api/`, each exposing a `router()` merged in `src/api/mod.rs`. `/health` (liveness, no DB) and `/ready` (DB-aware readiness) are unversioned infra probes; every real resource mounts under `/api/v1/`. - **No SQL in handlers.** SQL lives in the DB layer (`src/db.rs`) or in a `waveflow-core::repository::postgres::*` method; handlers stay pure HTTP orchestration. `db::ping` (`/ready`'s `SELECT 1`) and `db::users::create` are the in-tree pattern; everything tenant-scoped goes through `PostgresProfileRepository::*_for_user`. This mirrors the desktop's Tauri-command ↔ `waveflow-core` boundary. - **Tenancy is enforced at the storage layer.** Server handlers under `/api/v1/*` extract `UserId` from the `middleware::authenticate` middleware and call `*_for_user` methods only — never the single-tenant trait surface. `PostgresProfileRepository` deliberately does NOT implement `ProfileRepository`, so the compiler stops a careless `list_all()` from leaking another tenant's rows. Apply the same pattern when adding library / track / playlist repositories. - **Auth: JWT-only (Phase 1.d.2).** `middleware::authenticate` requires a Bearer JWT signed by the upstream Better Auth issuer. The middleware verifies the token via [`AppState::jwt_verifier`], then `db::users::find_or_provision_by_external_id(state.db, &sub, now_ms)` lazy-onboards the user on first request — a valid signature is the authoritative onboarding signal, so no separate `POST /api/v1/users` exists. Boot requires the full `WAVEFLOW_JWT_*` triple (`_JWKS_URL` / `_ISSUER` / `_AUDIENCE`); the legacy `X-User-Id` dev shim retired alongside `WAVEFLOW_DEV_AUTH`. - **Streaming (Phase 1.e).** Two endpoints: `POST /api/v1/profiles/{p}/libraries/{l}/tracks/{t}/stream-url` (JWT-authed) verifies tenant ownership and signs a short-lived (≤ 60 s) URL via [`stream_token::mint`]; `GET /api/v1/stream/{token}` is mounted OUTSIDE the JWT layer because browsers can't attach a Bearer to `