Development Guide
Dev setup, devtools, testing, and contributing workflow.
Table of Contents
- Dev Setup
- Mix Aliases
- Dev Server Management
- Testing
- Code Quality
- Asset Pipeline
- Phoenix LiveDashboard
- Debugging Tips
- Project Structure
Dev Setup
Follow the Installation Guide to get all prerequisites. Then:
# Full setup in one command
mix setup
This runs: deps.get, ecto.setup, assets.setup, assets.build.
Mix Aliases
| Alias | Command | Description |
|---|---|---|
mix setup |
deps.get + ecto.setup + assets.setup + assets.build |
Full project setup |
mix ecto.setup |
ecto.create + ecto.migrate + run seeds.exs |
Database setup |
mix ecto.reset |
ecto.drop + ecto.setup |
Full DB reset |
mix assets.setup |
Install Tailwind and esbuild | Asset tooling setup |
mix assets.build |
Build assets for dev | Compile CSS + JS |
mix assets.deploy |
Minify + digest assets | Production build |
mix precommit |
compile --warnings-as-errors + deps.unlock --unused + format + test |
Pre-commit checks |
Running Tests
# Run all tests
mix test
# Run a specific file
mix test test/sound_forge/music_test.exs
# Run previously failed tests
mix test --failed
# Run with verbose output
mix test --trace
Dev Server Management
# Start with hot reload
source .env && mix phx.server
# Start in IEx (interactive Elixir)
source .env && iex -S mix phx.server
# Check for running servers on standard ports
lsof -ti:4000,4001
# Kill existing server
kill $(lsof -ti:4000)
The phoenix_live_reload dependency watches for file changes and automatically reloads:
- Elixir source files (recompiles + reloads)
- HEEx templates (live reload)
- CSS/JS assets (browser reload)
Testing
Test Stack
| Layer | Tool |
|---|---|
| Unit tests | ExUnit |
| LiveView tests | Phoenix.LiveViewTest |
| HTML assertions | LazyHTML |
| Mocks | Mox |
| Oban testing | Oban.Testing |
Test Configuration
In config/test.exs:
- Oban runs in
testing: :inlinemode — workers execute synchronously - Spotify client is mocked:
SoundForge.Spotify.MockClient - Database uses a separate
sound_forge_testdatabase with Ecto sandbox
Writing Tests
# Unit test example
defmodule SoundForge.MusicTest do
use SoundForge.DataCase
test "creates a track" do
{:ok, track} = Music.create_track(%{title: "Test Track"})
assert track.title == "Test Track"
end
end
# LiveView test example
defmodule SoundForgeWeb.DashboardLiveTest do
use SoundForgeWeb.ConnCase
import Phoenix.LiveViewTest
test "renders dashboard", %{conn: conn} do
{:ok, view, html} = live(conn |> log_in_user(user), "/")
assert has_element?(view, "#track-list")
end
end
Test Count
653 tests, 0 failures (as of v4.0.0).
Code Quality
Credo (Static Analysis)
mix credo
mix credo --strict
Dialyxir (Type Checking)
# First run builds PLT (slow, ~5 min)
mix dialyzer
# Subsequent runs are fast
mix dialyzer --format dialyzer
Formatting
# Format all Elixir code
mix format
# Check formatting without changing files
mix format --check-formatted
Pre-commit Hook
The precommit alias runs all checks:
mix precommit
# Equivalent to:
# mix compile --warnings-as-errors
# mix deps.unlock --unused
# mix format
# mix test
Asset Pipeline
Tailwind CSS v4
# Dev build (with watcher)
mix tailwind sound_forge
# Production build (minified)
mix tailwind sound_forge --minify
Tailwind v4 no longer needs tailwind.config.js. Source scanning configured in assets/css/app.css:
@import "tailwindcss" source(none);
@source "../css";
@source "../js";
@source "../../lib/sound_forge_web";
esbuild
# Dev build
mix esbuild sound_forge
# Production build (minified)
mix esbuild sound_forge --minify
Full Production Asset Build
mix assets.deploy
# Runs: tailwind --minify + esbuild --minify + phx.digest
Phoenix LiveDashboard
Available in development at: http://localhost:4000/dev/dashboard
Features:
- Real-time metrics (requests/sec, memory, process count)
- Oban queue monitoring
- ETS table inspector
- Process list with memory/reductions
Mailbox Preview
In development, emails are sent to the Swoosh mailbox (not actually delivered):
http://localhost:4000/dev/mailbox
Debugging Tips
IEx Helpers
# Inspect all tracks
SoundForge.Music.list_tracks()
# Check Oban queue
Oban.check_queue(:download)
# View LLM model registry
SoundForge.LLM.ModelRegistry.list_models()
# Test Spotify connection
SoundForge.Spotify.fetch_metadata("https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC")
PubSub Debugging
# Subscribe to job events in IEx
Phoenix.PubSub.subscribe(SoundForge.PubSub, "jobs:some-job-id")
flush() # Print any received messages
Python Port Debugging
# Test analyzer directly
SoundForge.Audio.AnalyzerPort.analyze("/path/to/audio.wav", ["tempo", "key"])
# Test Demucs port
SoundForge.Audio.DemucsPort.separate("/path/to/audio.wav", model: "htdemucs")
Common Errors
| Error | Cause | Fix |
|---|---|---|
current_scope assign missing |
Route not in authenticated scope | Move route inside :require_authenticated_user scope |
spotdl: command not found |
spotdl not on PATH | pip install spotdl and ensure venv activated |
Ecto.NoResultsError |
Missing preload | Add Repo.preload/2 before accessing association |
Oban.ConstraintError |
Duplicate unique job | Check unique options on worker |
Project Structure
flowchart TD
Root["sfa/"]
Config["config/\nElixir configuration files"]
ConfigExs["config.exs\nCompile-time config"]
DevExs["dev.exs\nDevelopment overrides"]
TestExs["test.exs\nTest overrides"]
RuntimeExs["runtime.exs\nRuntime config (env vars)"]
Lib["lib/"]
SF["sound_forge/\nCore business logic"]
Accounts["accounts/\nUser auth"]
AdminCtx["admin/\nAdmin context"]
Agents["agents/\nAI agent system"]
Audio["audio/\nErlang Port wrappers"]
DAW["daw/\nDAW context"]
DJ["dj/\nDJ context"]
Jobs["jobs/\nOban worker contexts"]
LLM["llm/\nLLM providers"]
MIDI["midi/\nMIDI interface"]
Music["music/\nCore domain (tracks, stems)"]
Processing["processing/\nDemucs config"]
Spotify["spotify/\nSpotify API client"]
Telemetry["telemetry/\nMetrics"]
SFWeb["sound_forge_web/\nPhoenix web layer"]
Components["components/\nReusable LiveView components"]
Controllers["controllers/\nHTTP controllers"]
Live["live/\nLiveView modules"]
Router["router.ex\nRoute definitions"]
Priv["priv/"]
Python["python/\nPython scripts (analyzer, demucs)"]
Migrations["repo/migrations/\nEcto migrations"]
Uploads["uploads/\nAudio file storage"]
Assets["assets/"]
CSS["css/app.css\nTailwind v4 entry"]
JS["js/app.js\nJS entry (hooks, LiveSocket)"]
Test["test/\nExUnit tests"]
Docs["docs/\nThis documentation"]
Root --> Config
Config --> ConfigExs
Config --> DevExs
Config --> TestExs
Config --> RuntimeExs
Root --> Lib
Lib --> SF
SF --> Accounts
SF --> AdminCtx
SF --> Agents
SF --> Audio
SF --> DAW
SF --> DJ
SF --> Jobs
SF --> LLM
SF --> MIDI
SF --> Music
SF --> Processing
SF --> Spotify
SF --> Telemetry
Lib --> SFWeb
SFWeb --> Components
SFWeb --> Controllers
SFWeb --> Live
SFWeb --> Router
Root --> Priv
Priv --> Python
Priv --> Migrations
Priv --> Uploads
Root --> Assets
Assets --> CSS
Assets --> JS
Root --> Test
Root --> Docs
See Also
| ← Deployment | Next: Features → |