WaveFlow's web client — React + TanStack Start frontend for waveflow-server.
Companion to the desktop app at InstaZDLL/WaveFlow. Architecture intent and the long-term roadmap live in RFC-001.
- React 19 + TypeScript — UI layer
- TanStack Start (Vite + Nitro) — file-based routing, SSR, server functions, API routes
- Tailwind CSS 4 — styling
- Vitest + Testing Library — test runner
bun install # one-shot; runs `prepare: husky` to install the commit-msg hook
cp .env.example .env # then fill in DATABASE_URL, BETTER_AUTH_SECRET (openssl rand -base64 32), and BETTER_AUTH_URL
bun run dev # Vite dev server on http://localhost:3000
bun run typecheck # tsc --noEmit (run after a build so routeTree.gen.ts exists)
bun run lint # eslint
bun run format # prettier --write
bun run build # production build (Vite + Nitro bundling)
bun run test # vitest runBetter Auth's tables live in Postgres. See db/README.md for the schema + bootstrap steps. A local dev DB via Docker takes ~10 seconds to stand up.
bun run db:migrate # apply pending migrations
bun run db:migrate --dry-run # show what would be appliedOnce waveflow-server's streaming config is on (WAVEFLOW_MUSIC_ROOT + WAVEFLOW_STREAM_SECRET), the web app exposes a basic player:
/profiles→ click a profile →/profiles/$profileId(libraries) → click a library →/profiles/$profileId/libraries/$libraryId(tracks)- Click any track's ▶ button — a sticky player bar pinned to the bottom plays the audio. Range-aware seek bar included.
- Auth flow: the browser hits the Nitro
getStreamUrlserver fn, which mints a signed URL on the server (Bearer JWT) and returns an absolute URL. The browser then hitswaveflow-serverdirectly via<audio src>— no CORS preflight (media elements skip it withoutcrossorigin), so this works across hosts in dev.
The /profiles route (src/routes/profiles.tsx) calls waveflow-server's /api/v1/profiles through a TanStack server function (src/server-fns/profiles.ts) that mints a fresh JWT off the active Better Auth session and forwards the request server-side. The browser never sees WAVEFLOW_SERVER_URL and never crosses an origin — sidesteps CORS plumbing on waveflow-server.
Run both servers locally:
# terminal 1 — waveflow-server on :4000
WAVEFLOW_BIND=127.0.0.1:4000 cargo run --manifest-path ../waveflow-server/Cargo.toml
# terminal 2 — waveflow-web on :3000
bun run devThe first authenticated request lazy-provisions the user in waveflow-server's users table (no separate onboarding round-trip needed — waveflow-server PR #16).
The jwt() plugin exposes two endpoints once a session is active:
GET /api/auth/jwks— public key set (ES256). Pointwaveflow-server'sWAVEFLOW_JWT_JWKS_URLat this URL.GET /api/auth/token— mints a fresh bearer token for the current session (15-minute TTL). The web app's fetch wrapper (1.c.3) will call this on demand.
Smoke-test the JWKS endpoint after bun run dev:
curl http://localhost:3000/api/auth/jwks | jq
# => { "keys": [ { "kty": "EC", "crv": "P-256", "alg": "ES256", "kid": "...", "x": "...", "y": "..." } ] }The first request lazy-generates the key pair into the jwks table — subsequent requests reuse it. Issuer = BETTER_AUTH_URL; audience defaults to waveflow-server (override with WAVEFLOW_JWT_AUDIENCE).
Routes:
/sign-up— create a new account (email + password, 12–128 chars)/sign-in— sign back in- The header auto-swaps to a user chip + sign-out button once a session is active
Smoke-test from the CLI once the dev server is up:
curl -X POST http://localhost:3000/api/auth/sign-up/email \
-H 'Content-Type: application/json' \
-d '{"email":"daisy@example.com","password":"correct-horse-battery","name":"Daisy"}'
curl -X POST http://localhost:3000/api/auth/sign-in/email \
-H 'Content-Type: application/json' \
-d '{"email":"daisy@example.com","password":"correct-horse-battery"}'The Nitro build produces a self-contained Node server in .output/:
bun run build
node .output/server/index.mjsFor host-specific presets (Vercel, Cloudflare, AWS Lambda, etc.), see nitro.build/deploy.
AGPL-3.0-only. The web client is part of the SaaS-hosted backend story — same license as waveflow-server. Forks of the hosted product must publish their client-side modifications too.
The companion desktop app at InstaZDLL/WaveFlow is GPL-3.0-only — no network clause needed since it's a local-only player.
See CONTRIBUTING.md. Every commit must carry a Signed-off-by: trailer (DCO) — git commit -s adds it automatically.
Conventional commits enforced locally via husky + commitlint (commit-msg hook). Header ≤ 100 chars, kebab-case scopes, lowercase subject.
For TanStack-specific reference (file routing, server functions, layouts, data loaders), see the TanStack documentation.