This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
WaveFlow is a local music player desktop app built with Tauri 2 + React 19 + TypeScript + Vite and a bun toolchain. Spotify / Apple Music-inspired UI on top of a Rust audio engine.
This file is an index, not a manual. It carries the map plus the one-line form of each invariant. The reasoning, algorithms, schema and flow diagrams live under docs/ β that's the source of truth, and new detail belongs there, not here.
| Need | Read |
|---|---|
| Why a rule exists / how not to break it | docs/architecture/invariants.md |
| How a feature actually works | docs/features/ |
| Crate split, audio topology, DB layout | docs/architecture/ |
| Design decisions not yet built | docs/rfcs/ |
| Release / packaging procedure | docs/RELEASING.md |
bun install # dependencies
bun run tauri dev # full desktop app (Vite + Rust)
bun run tauri build # production bundle
bun run dev # Vite dev server only (no Tauri shell), port 1420
bun run typecheck # tsc --noEmit
bun run lint # eslint
bun run build # tsc + Vite prod build
cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check
cargo check --manifest-path src-tauri/Cargo.toml --workspace --all-targets
cargo test --manifest-path src-tauri/Cargo.toml --workspaceThe PR checklist is typecheck / lint / cargo fmt --check / cargo check. cargo fmt is not optional β CI runs it as the first step of the Rust job, so an unformatted file fails the whole job before a single test runs, and neither cargo check nor clippy will have warned you.
React 19 + TypeScript. Entry: src/main.tsx β src/App.tsx.
- Contexts (provider tree in
App.tsx):ThemeContext,PlayerContext,LibraryContext,PlaylistContext,ProfileContext.PageScrollContextmounts lower (inAppLayout) and exposes the main scrollable area to virtualized tables β one page-driven scrollbar. - Hooks wrap each context:
useTheme,usePlayer,useLibrary,usePlaylist,useProfile,usePageScroll. - Tauri wrappers (
src/lib/tauri/): one typedinvoke()per backend command. - Views:
HomeView,LibraryView,PlaylistView,AlbumDetailView,ArtistDetailView,LikedView,HistoryView,StatisticsView,WrappedView,SettingsView, β¦ - Layout: Apple-Music-style sidebar, TopBar with search, PlayerBar at the bottom, right-edge panels (
NowPlayingPanel/QueuePanel/LyricsPanel) mutex'd viaPlayerContext. A secondWebviewWindow(labelmini,?mini=1) ships the always-on-top mini-player βdocs/features/ui.mdβ and a third (labellyrics,?lyrics=1, created by the backend) the transparent desktop lyrics overlay β#desktop-lyrics.
crates/core/(waveflow-core) β portable business logic, reusable fromwaveflow-server. Domain DTOs, repository traits + SQLite and Postgres impls (Cargo features: desktop =sqlite, server =postgres), scanner helpers + upserts, smart-playlist engine, audio analysis, DSDβPCM, HTTP clients (Deezer / Last.fm / LRCLIB / TheAudioDB), artwork pipeline, the wasmtime plugin host. Zero Tauri /cpal. Split rules + feature matrix:docs/architecture/crates.md.crates/app/(waveflow) β the Tauri 2 app. Entrycrates/app/src/main.rsβlib.rs.#[tauri::command]handlers (thin wrappers over core's repositories), the real-timecpal+rtrbaudio engine, DLNA / MPD / OS media controls / Discord RPC, the fs watcher, the tray, the profile pool wiring.
Inside crates/app/src/:
commands/β one module per domain (library,playlist,smart_playlists,track,browse,player,scan,edit,profile,analysis,deezer,similar,lyrics,stats,wrapped,maintenance,radio,duplicates,preferences,plugins,canvas, β¦), all registered inlib.rs::generate_handler![]. CRUD delegates towaveflow_core::repository::sqlite::*; IPC + state + filesystem + emit glue stays in the command.audio/β 3-thread lock-free engine:decoder.rs(symphonia + rubato),output.rs(cpal callback on its own thread, SPSCrtrbring),state.rs(SharedPlaybackatomics),analytics.rs,crossfade.rs,eq.rs,spectrum.rs, the three exclusive backends (wasapi_exclusive.rs,alsa_exclusive.rs,coreaudio_exclusive.rs). Topology:docs/architecture/audio.md.dlna/(axum + SSDP, opt-in) Β·mpd/(TCP MPD protocol, opt-in) Β·media_controls.rs(souvlaki β SMTC / MPRIS / MediaRemote) Β·discord_presence.rsΒ·queue.rsΒ·player_actions.rs(shared control sequence) Β·remote/(remote source + sync v2, featuresync_v2, now in the default feature set;remote/mirror.rswalks the server's catalogue into the projection so both sources can be browsed from one library;remote/download.rskeeps a track's bytes in a managed folder the scanner never sees whileremote/import.rscopies them into a scanned one, where they become a local track linked to the server's;remote/upload.rsis the fourth direction β offering the server what it lacks, deduplicated offline against the mirror and hashed once viaremote/hashing.rs;mod syncis now a permanent no-op stub β v1 was removed in the RFC-005 cutover) Β·backup.rsΒ·db/(pool wiring +migration_heal+ the pre-migration snapshot).- Scanner β the orchestrator
scan_folder_innerstays app-side (it emitsscan:progress); every pure helper lives inwaveflow_core::scanner::{extract, upserts}. - Database β per-profile SQLite via sqlx + a global
app.dbfor the profile list and app-wide settings. Migrations atsrc-tauri/migrations/{app,profile}/, compiled in viasqlx::migrate!. Layout:docs/architecture/storage.md.
One line each. The reasoning, the failure mode and the exceptions are in docs/architecture/invariants.md β read the matching section before touching one of these.
- Tauri commands β
commands/*.rs+generate_handler![]; frontend camelCase, backend snake_case. - Profile-scoped pool β
state.require_profile_pool().await?for anything touching user data; it's a leasedProfilePool, so query with&*pool, keep the handle bound, and never re-resolve it mid-batch. β - Settings persistence β
profile_setting/app_settingviaINSERT β¦ ON CONFLICT DO UPDATE; a React preference hook builds onuseProfileSetting, never hand-rolled. β - Events β backend emits
player:*,track:updated,library:rescanned,scan:progress,lyrics:updated, β¦; frontend useslisten(). β - Audio callback is hot β no allocation, no locks, no logging; all DSP happens on the decoder thread, whose last stage is a
[-1.0, 1.0]clamp. β - Load commands carry a
LoadIntentβ claimed when the intent starts (before the awaits), not before the send; the decoder drops any load older than one already delivered, and a producer claims the dispatch before its first side effect, publishing underlock_publish. An output rebuild is the one exception: it re-dispatches the decoder's own recorded load, under that load's intent. β - Anything written into a library folder goes under
.waveflow/βmp4is an audio extension taken on sight, so a clip anywhere else becomes a track; the scanner prunes that branch and the watcher ignores events inside it. β - Never
DROP TABLEa parent table in a migration βforeign_keys = ONturns it into a cascading delete. UseALTER TABLE β¦ ADD COLUMN. β - Migrations are immutable once merged β sqlx checksums them; always add a new dated
YYYYMMDDhhmmss_<slug>.sql. β - Single writer to SQLite β batch in transactions, pass
&mut SqliteConnectionto upsert helpers, and any background writer parks behind the scan + retries onSQLITE_BUSY. β - Multi-artist split is
"; "only β never", "; queries rebuild credits by joiningtrack_artistwithGROUP_CONCATordered byposition. β - Album grouping =
(canonical_title, album_artist_id)βis_compilationis sticky and renames reuse the old album's flags. β - File-write safety on Windows β pause playback before rewriting the current track's file, then re-hash blake3 into
track.file_hash. β - Tag writes go through the concrete tag β
edit::patch_file, neverread_from_path+save_to_path; the genericTagdrops every non-standard Vorbis comment. β - One codec registry β playback, analysis and the scanner all open streams through
waveflow_core::audio_format::opus::codecs(), neversymphonia::default::get_codecs(); a disagreement between them puts an unplayable track in the library. β - Virtual scroll everywhere β
@tanstack/react-virtual+usePageScroll(), never a nestedoverflow-y-auto. β - Modal accessibility β every modal calls
useModalA11y; no bespoke Escape handlers. β - Overlays are portalled, not z-indexed β any
backdrop-filter/transformancestor caps the stacking context; z-100+ goes throughcreatePortal(β¦, document.body). Layer scale is documented at the top ofsrc/app.css. β - Right panels are flex siblings, not overlays β the center column carries
min-w-0. β - Process-wide offline mode β every outbound HTTP path checks
offline::is_offline()first. β - Non-frontend control surfaces go through
player_actionsβ tray, media keys, taskbar thumbnail buttons and MPD must not re-derive the advance + emit sequence. β - Long-running work announces itself β a scan, a sweep, a prefetch, a mirror walk, a backup takes a
TaskHandlefromtasks.rs; the registry routes cancellation to the task's own stopping point rather than inventing one. β - New player-bar action β lands in the "β―" overflow menu first, promoted only when usage warrants it. β
- Plugins β loaded at runtime, distributed from separate repos, blake3-pinned by the registry; published manifest strings use
*_i18nsiblings;uiplugins return a JSON descriptor and get only redacted library reads;canvasfan-out is fail-soft; a plugin that cannot load is shown as broken, never silently skipped. β Β· full surface
Names in commands/, audio/ and src/components/ are predictable β read the file. For anything that isn't obvious from the name, these are the deep dives:
| Area | Doc | Covers |
|---|---|---|
| Playback | playback.md |
decoder (incl. Opus via libopus) + DSD pipeline, native DSD via DoP, crossfade (static / smart / dynamic), gapless, ReplayGain (track / album / auto), EQ, speed, network pre-load, exclusive output (WASAPI / ALSA / CoreAudio), spectrum visualizer, A-B loop, queue + album shuffle |
| Library | library.md |
scanner + watcher, folder covers, local artist images, search + filters, tag editor, ratings, duplicates, import, history, multi-artist split |
| Playlists | playlists.md Β· smart-playlists.md |
CRUD, sorting, auto-covers, M3U, Daily Mix + On Repeat generators, rule tree |
| Integrations | integrations.md |
Deezer, Last.fm, TheAudioDB, lyrics providers + editor, artist overrides, Discord RPC, OS notifications, scrobbling |
| Plugins | plugins.md |
WASM host + sandbox, store, options, source / metadata / ui / canvas worlds, Web Radio + offline catalogue |
| UI & UX | ui.md |
layout, 5 skins Γ 14 themes, immersive view, Canvas, cover slideshow, artist hero, mini-player, desktop lyrics, Wrapped, profiles, onboarding, settings, updater, backups |
| LAN servers | dlna.md Β· mpd.md |
opt-in MediaServer and MPD control surface |
- Conventional commits, enforced locally by husky
commit-msgβbunx commitlint --edit. Config in.commitlintrc.cjs(header β€ 100, kebab-case scopes). Subject stays lowercase β not sentence/start/pascal/upper case. - PR labels are automatic (
.github/workflows/label-pr.yml):scope:*by path,type:*from the title prefix,size:*from the diff. - Never hand-tag a release. release-please owns version bumps across
package.json(canonical),src-tauri/crates/app/tauri.conf.json,src-tauri/Cargo.toml+Cargo.lock. Tag push drives bundles, the signed updater manifest and the downstream AUR / winget / copr / apt dispatches. Beta channel + full procedure:docs/RELEASING.md. - The macOS bundle must stay codesigned β a linker-only ad-hoc binary has no sealed bundle and no stable identity, so macOS re-asks for folder access on every launch. Signing runs inside
tauri build(never as a post-build pass) andrelease.ymlverifies it. β - Flatpak sources are generated and go stale silently β a lockfile bump without regenerating
packaging/flatpak/generated/fails inside Flathub's offline sandbox, not in CI.check-sources.pyguards coverage per PR; note thebun.lockvs npm-lockfile split when pinning a dependency. β - Issue + PR templates live under
.github/.
UI copy ships in 17 locales via i18next β fr (source of truth), en, es, de, it, nl, pt, pt-BR, ru, tr, id, ja, ko, zh-CN, zh-TW, ar, hi. Strings in src/i18n/locales/<code>.json; index.ts sets document.documentElement.dir so Arabic renders RTL. The legacy kr code stays accepted as an alias for ko (a startup migration rewrites stored preferences). The README is in English.
fallbackLng: "en" is set, but the convention is that every locale carries every key β no language-mixing in the UI. When you add a key, propagate it to all 17 files (a small Python script with json.load/dump + ensure_ascii=False, indent=2 preserves the formatting).
Keep verbatim across locales: brand tokens (WaveFlow, Last.fm, Deezer, ReplayGain, LRCLIB, BPM), smart-playlist family names (Daily Mix, On Repeat β Spotify and Apple Music keep theirs untranslated in every market, and translating ours would split the user's mental model), and i18next {{placeholder}} tokens.