Home > Architecture > Database Schema
Database Schema
Table of Contents
- Overview
- Entity Relationships
- Accounts Tables
- Music Tables
- Job Tables
- LLM Provider Table
- Oban Jobs Table
- Indexes and Constraints
- Migration Strategy
Overview
Sound Forge Alchemy uses PostgreSQL 14+ with Ecto 3.13. All primary keys are binary_id (UUID v4) except users (integer) and Oban tables (bigserial). Oban uses the same database for job storage in oban_jobs. API keys and OAuth tokens are encrypted at rest using SoundForge.Vault (AES-256-GCM via Cloak.Ecto).
Key Design Decisions
- All PKs are UUID v4 (
binary_id) exceptusers(legacy integer PK) and Oban system tables -
Job tables share a common 5-state machine: :queued→:downloading/:processing→:completed:failed - Stem file paths are stored as relative paths (e.g.
stems/track_id/vocals.wav) to produce clean/files/stems/...URLs - Encrypted fields:
user_settings.lalalai_api_key,llm_providers.api_key,spotify_oauth_tokens.access_token,spotify_oauth_tokens.refresh_token— all useSoundForge.Encrypted.Binary/SoundForge.Vault(AES-256-GCM)
Entity Relationships
erDiagram
users ||--o| user_settings : "has one"
users ||--o| spotify_oauth_tokens : "has one"
users ||--o{ batch_jobs : "owns"
users ||--o{ llm_providers : "configures"
tracks ||--o{ download_jobs : "has"
tracks ||--o{ processing_jobs : "has"
tracks ||--o{ analysis_jobs : "has"
tracks ||--o{ stems : "has"
tracks ||--o{ analysis_results : "has"
tracks ||--o{ playlist_tracks : "in"
processing_jobs ||--o{ stems : "produces"
processing_jobs }o--|| batch_jobs : "grouped by"
analysis_jobs ||--o| analysis_results : "produces"
playlists ||--o{ playlist_tracks : "has"
playlist_tracks }|--|| tracks : "references"
llm_providers }|--|| users : "owned by"
Accounts Tables
users
Generated by phx.gen.auth. Integer PK (legacy; all other tables use UUID).
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
bigint |
PK, serial | Auto-increment integer ID |
email |
varchar(160) |
unique, required | User email address |
hashed_password |
varchar |
— | Bcrypt hash (redacted in logs) |
role |
varchar |
enum, default: user |
user, pro, enterprise, admin, super_admin, platform_admin |
status |
varchar |
enum, default: active |
active, suspended, banned |
confirmed_at |
utc_datetime |
— | Email confirmation timestamp |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Ecto module: SoundForge.Accounts.User
Role hierarchy (ascending privilege): user → pro → enterprise → admin → super_admin → platform_admin
users_tokens
Generated by phx.gen.auth. Stores session, confirmation, and reset tokens.
| Column | Type | Description |
|---|---|---|
id |
bigint |
PK |
user_id |
bigint |
FK → users |
token |
bytea |
Secure random bytes |
context |
varchar |
session, confirm, reset_password |
sent_to |
varchar |
Email address (for email tokens) |
inserted_at |
utc_datetime |
user_settings
Per-user preference overrides. All fields are nullable — nil means “use the global default from Application config”.
| Column | Type | Description |
|---|---|---|
id |
bigint |
PK |
user_id |
bigint |
FK → users, unique |
download_quality |
varchar |
128k, 192k, 256k, 320k |
audio_format |
varchar |
mp3, flac, wav, ogg |
output_directory |
varchar |
Override download directory |
ytdlp_search_depth |
integer |
1–20 |
ytdlp_preferred_format |
varchar |
yt-dlp format string |
ytdlp_bitrate |
varchar |
yt-dlp bitrate |
demucs_model |
varchar |
htdemucs, htdemucs_ft, htdemucs_6s, mdx_extra |
demucs_output_format |
varchar |
wav, flac, mp3 |
demucs_device |
varchar |
cpu, cuda |
demucs_timeout |
integer |
Seconds |
analysis_features |
varchar[] |
Array: tempo, key, energy, spectral |
analyzer_timeout |
integer |
Seconds |
storage_path |
varchar |
Override storage root |
max_file_age_days |
integer |
File retention |
retention_days |
integer |
Retention policy |
tracks_per_page |
integer |
1–100 |
max_upload_size |
integer |
Bytes |
lalalai_api_key |
bytea |
Encrypted (Cloak AES-GCM-256) |
lalalai_splitter |
varchar |
andromeda, perseus, orion, phoenix, lyra |
lalalai_dereverb |
boolean |
Enable de-reverb |
lalalai_extraction_level |
varchar |
mild, normal, clear_cut, deep_extraction |
lalalai_output_format |
varchar |
mp3, wav, flac, aac, ogg |
debug_mode |
boolean |
default: false |
inserted_at |
utc_datetime |
|
updated_at |
utc_datetime |
Ecto module: SoundForge.Accounts.UserSettings
spotify_oauth_tokens
Encrypted Spotify OAuth2 tokens. One per user.
| Column | Type | Description |
|---|---|---|
id |
bigint |
PK |
user_id |
bigint |
FK → users, unique |
access_token |
bytea |
Encrypted at rest |
refresh_token |
bytea |
Encrypted at rest |
token_type |
varchar |
default: Bearer |
expires_at |
utc_datetime |
Expiration timestamp |
scopes |
varchar |
Space-delimited OAuth scopes |
inserted_at |
utc_datetime |
|
updated_at |
utc_datetime |
Ecto module: SoundForge.Accounts.SpotifyOAuthToken
Helper: SpotifyOAuthToken.expired?/1 — returns true if expires_at is in the past.
Music Tables
tracks
The root entity. Represents a single piece of audio, typically sourced from Spotify.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 primary key |
spotify_id |
varchar |
unique | Spotify track identifier |
spotify_url |
varchar |
— | Full Spotify URL |
title |
varchar(500) |
required, min 1 | Track title |
artist |
varchar |
— | Primary artist name |
album |
varchar |
— | Album name |
album_art_url |
varchar |
— | URL to album artwork |
duration |
integer |
> 0 | Duration in seconds |
user_id |
integer |
FK → users | Owning user |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Virtual field: download_status (:string) — populated by list_tracks query with the latest download job status; not persisted.
Ecto module: SoundForge.Music.Track
Associations:
has_many :download_jobshas_many :processing_jobshas_many :analysis_jobshas_many :stemshas_many :analysis_resultshas_many :playlist_trackshas_many :playlists, through: [:playlist_tracks, :playlist]
stems
Individual audio stem files produced by processing jobs.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 primary key |
processing_job_id |
uuid |
FK, required (except DAW exports) | Parent ProcessingJob |
track_id |
uuid |
FK, required | Parent Track |
stem_type |
varchar |
enum, required | See stem types below |
file_path |
varchar |
— | Relative filesystem path (e.g. stems/track_id/vocals.wav) |
file_size |
integer |
— | File size in bytes |
options |
jsonb |
default: {} |
Additional options map |
source |
varchar |
default: local |
local (Demucs) or lalalai |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Stem type enum values:
| Value | Source |
|---|---|
vocals |
Demucs + lalal.ai |
drums |
Demucs + lalal.ai |
bass |
Demucs + lalal.ai |
other |
Demucs |
guitar |
Demucs (htdemucs_6s) |
piano |
Demucs (htdemucs_6s) |
electric_guitar |
lalal.ai |
acoustic_guitar |
lalal.ai |
synth |
lalal.ai |
strings |
lalal.ai |
wind |
lalal.ai |
Ecto module: SoundForge.Music.Stem
Two changesets: changeset/2 (requires processing_job_id), export_changeset/2 (DAW exports, no processing_job_id).
analysis_results
Structured audio feature analysis output from librosa.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 primary key |
track_id |
uuid |
FK, required | Parent Track |
analysis_job_id |
uuid |
FK, required | Parent AnalysisJob |
tempo |
float |
— | BPM (beats per minute) |
key |
varchar |
— | Musical key, e.g., "C major" |
energy |
float |
— | Normalized RMS energy (0.0–1.0) |
spectral_centroid |
float |
— | Spectral centroid in Hz |
spectral_rolloff |
float |
— | Spectral rolloff in Hz |
zero_crossing_rate |
float |
— | Zero crossing rate (0.0–1.0) |
features |
jsonb |
— | Extended features map (MFCC, chroma, beats, segments) |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Ecto module: SoundForge.Music.AnalysisResult
playlists
User playlists, optionally synced from Spotify.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 primary key |
name |
varchar(500) |
required, min 1 | Playlist name |
description |
varchar |
— | Optional description |
spotify_id |
varchar |
unique per user | Spotify playlist identifier |
spotify_url |
varchar |
— | Full Spotify URL |
cover_art_url |
varchar |
— | Cover art image URL |
source |
varchar |
default: manual |
spotify, manual, import |
user_id |
integer |
FK → users | Owning user |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Unique constraint: (spotify_id, user_id).
Ecto module: SoundForge.Music.Playlist
Associations: has_many :playlist_tracks, has_many :tracks, through: [:playlist_tracks, :track].
playlist_tracks
Join table between playlists and tracks with ordering support.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 primary key |
playlist_id |
uuid |
FK, required | Parent Playlist |
track_id |
uuid |
FK, required | Referenced Track |
position |
integer |
— | Order within playlist |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Unique constraint: (playlist_id, track_id).
Ecto module: SoundForge.Music.PlaylistTrack
Job Tables
All job tables share a common status state machine:
stateDiagram-v2
[*] --> queued
queued --> downloading : worker picks up (download_jobs)
queued --> processing : worker picks up (processing/analysis)
downloading --> processing
processing --> completed
downloading --> failed
processing --> failed
completed --> [*]
failed --> [*]
download_jobs
Tracks audio download progress via spotdl.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 |
track_id |
uuid |
FK, required | Parent Track |
status |
varchar |
enum, default: queued |
queued, downloading, processing, completed, failed |
progress |
integer |
default: 0, 0–100 | Completion percentage |
output_path |
varchar |
— | Path to downloaded audio file |
file_size |
integer |
— | Downloaded file size in bytes |
error |
text |
— | Error message on failure |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Ecto module: SoundForge.Music.DownloadJob
processing_jobs
Tracks stem separation via Demucs or lalal.ai.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 |
track_id |
uuid |
FK, required | Parent Track |
batch_job_id |
uuid |
FK | Parent BatchJob (optional) |
model |
varchar |
default: htdemucs |
Demucs model name |
engine |
varchar |
default: demucs |
demucs or lalalai |
status |
varchar |
enum, default: queued |
Job status |
progress |
integer |
default: 0, 0–100 | |
output_path |
varchar |
— | Directory containing stem files |
options |
jsonb |
— | Additional processing options |
preview |
boolean |
default: false | lalal.ai 60s preview mode |
error |
text |
— | Error message on failure |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Ecto module: SoundForge.Music.ProcessingJob
Associations: belongs_to :track, belongs_to :batch_job, has_many :stems.
analysis_jobs
Tracks audio feature extraction via librosa.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 |
track_id |
uuid |
FK, required | Parent Track |
status |
varchar |
enum, default: queued |
Job status |
progress |
integer |
default: 0, 0–100 | |
results |
jsonb |
— | Inline analysis results map |
error |
text |
— | Error message on failure |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Ecto module: SoundForge.Music.AnalysisJob
Associations: belongs_to :track, has_one :analysis_result.
batch_jobs
Groups multiple processing jobs into a single batch operation.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 |
user_id |
integer |
FK → users, required | Owning user |
status |
varchar |
enum, default: pending |
pending, processing, completed, failed |
total_count |
integer |
required, > 0 | Total jobs in batch |
completed_count |
integer |
default: 0, >= 0 | Completed job count |
options |
jsonb |
— | Batch options map |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Ecto module: SoundForge.Music.BatchJob
Associations: belongs_to :user, has_many :processing_jobs.
LLM Provider Table
llm_providers
Per-user LLM provider configuration. API keys are encrypted at rest.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PK, not null | UUID v4 |
user_id |
integer |
FK → users, required | Owning user |
provider_type |
varchar |
enum, required | See provider types below |
name |
varchar |
required | User-defined display name |
api_key |
bytea |
conditional | Encrypted (Cloak AES-256-GCM); required for cloud providers |
base_url |
varchar |
conditional | Required for local/proxy providers |
default_model |
varchar |
— | Default model to use |
enabled |
boolean |
default: true | Whether provider is active |
priority |
integer |
— | Provider preference order (lower = higher priority) |
health_status |
varchar |
enum, default: unknown |
healthy, degraded, unreachable, unknown |
last_health_check_at |
utc_datetime |
— | Last health check timestamp |
config_json |
jsonb |
default: {} |
Provider-specific configuration |
inserted_at |
utc_datetime |
not null | |
updated_at |
utc_datetime |
not null |
Unique constraint: (user_id, provider_type, name).
Provider type enum values:
| Value | Category | Requires |
|---|---|---|
:anthropic |
cloud | api_key |
:openai |
cloud | api_key |
:azure_openai |
cloud | api_key + base_url |
:google_gemini |
cloud | api_key |
:ollama |
local | base_url |
:lm_studio |
local | base_url |
:litellm |
proxy | base_url |
:custom_openai |
proxy | base_url |
Ecto module: SoundForge.LLM.Provider
Oban Jobs Table
Managed by Oban migrations. Not manually edited.
oban_jobs
Key columns:
| Column | Description |
|---|---|
id |
bigserial PK |
queue |
Queue name: download, processing, analysis |
state |
available, scheduled, executing, retryable, completed, discarded, cancelled |
worker |
Module name string (e.g. "SoundForge.Jobs.DownloadWorker") |
args |
jsonb — worker arguments |
errors |
jsonb array — per-attempt error records |
attempt |
Current attempt number |
max_attempts |
Maximum retry attempts |
inserted_at |
|
scheduled_at |
When job should run |
attempted_at |
Last attempt timestamp |
completed_at |
Completion timestamp |
Indexes and Constraints
Key indexes beyond primary keys:
| Table | Column(s) | Type | Purpose |
|---|---|---|---|
users |
email |
unique | Prevent duplicate registrations |
tracks |
spotify_id |
unique | Prevent duplicate imports |
stems |
(track_id, stem_type) |
index | Stem queries by track and type |
stems |
processing_job_id |
index | Stems by job |
analysis_results |
track_id |
index | Results lookup by track |
analysis_results |
analysis_job_id |
index | Results lookup by job |
playlist_tracks |
(playlist_id, track_id) |
unique | Prevent duplicate playlist entries |
playlists |
(spotify_id, user_id) |
unique | Prevent duplicate Spotify imports |
llm_providers |
(user_id, provider_type, name) |
unique | Prevent duplicate provider configs |
llm_providers |
(user_id, priority) |
index | Provider ordering |
spotify_oauth_tokens |
user_id |
unique | One token per user |
user_settings |
user_id |
unique | One settings row per user |
oban_jobs |
(queue, state, priority) |
index | Job dispatch |
Migration Strategy
# Generate a new migration
mix ecto.gen.migration add_feature_name
# Run all pending migrations
mix ecto.migrate
# Roll back the last migration
mix ecto.rollback
# Reset database (dev/test only)
mix ecto.reset
# Check current schema version
mix ecto.migrations
Migration files live in priv/repo/migrations/. Each is timestamped and idempotent. Production deployments apply migrations before the application starts via the Release module:
# lib/sound_forge/release.ex
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
Adding a New Schema
- Generate migration:
mix ecto.gen.migration create_my_table - Create the Ecto schema in
lib/sound_forge/<context>/<module>.ex - Add associations to related schemas (e.g.
has_manyon parent) - Add the context function to the parent context module (e.g.
lib/sound_forge/music.ex) - Run
mix ecto.migrate
See Also
| ← LLM Providers | Next: Guides → |