# 02 — Data Model Authoritative relational schema for users, libraries, media, playlists, play history, recommendations, and operational tables. Provided in **PostgreSQL** dialect (normative) with **SQLite** deltas listed in §7. Migrations are managed with `golang-migrate`; this document corresponds to migration `0001_initial`. ## 1. Entity-relationship overview ```mermaid erDiagram USERS ||--o{ SESSIONS : has USERS ||--o{ API_KEYS : has USERS ||--o{ USER_LIBRARIES : "granted" LIBRARIES ||--o{ USER_LIBRARIES : "grants" SOURCES ||--|| LIBRARIES : "backs" LIBRARIES ||--o{ MEDIA_FILES : contains MEDIA_FILES ||--|| TRACKS : "manifests as" ALBUMS ||--o{ TRACKS : groups ARTISTS ||--o{ ALBUMS : "credited (album artist)" ARTISTS ||--o{ TRACK_ARTISTS : credited TRACKS ||--o{ TRACK_ARTISTS : credits GENRES ||--o{ TRACK_GENRES : tags TRACKS ||--o{ TRACK_GENRES : tagged USERS ||--o{ PLAYLISTS : owns PLAYLISTS ||--o{ PLAYLIST_ITEMS : contains TRACKS ||--o{ PLAYLIST_ITEMS : "appears in" USERS ||--o{ PLAY_HISTORY : plays TRACKS ||--o{ PLAY_HISTORY : played USERS ||--o{ ANNOTATIONS : rates TRACKS ||--o{ AUDIO_FEATURES : "has features" USERS ||--|| USER_SETTINGS : configures USERS ||--o{ PLAYERS : registers LIBRARIES ||--o{ SCANS : scanned USERS ||--o{ STREAM_TOKENS : mints ``` ## 2. Conventions - Primary keys: UUIDv7, column `id`. - `created_at` / `updated_at` on every mutable table; `updated_at` maintained by application code (not triggers) for SQLite parity. - Soft state (e.g. `missing` media files) over hard deletes where re-scans can resurrect rows; everything else uses `ON DELETE CASCADE` deliberately. - Text normalization: `order_*` columns hold case-folded, article-stripped, accent-folded sort keys computed at write time (e.g. "The Beatles" → "beatles"). Searching uses `search_text` columns (lowercased concatenation), with Postgres trigram index / SQLite FTS5 (see §6). ## 3. DDL — identity & access ```sql CREATE TABLE users ( id uuid PRIMARY KEY, username text NOT NULL UNIQUE, -- case-insensitive unique via index below email text, password_hash text NOT NULL, -- argon2id PHC string, see docs/05 §3 is_admin boolean NOT NULL DEFAULT false, is_active boolean NOT NULL DEFAULT true, last_login_at timestamptz, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL ); CREATE UNIQUE INDEX users_username_ci ON users (lower(username)); CREATE TABLE sessions ( id uuid PRIMARY KEY, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, -- sha256(token); raw token only in cookie user_agent text, ip_created text, expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL, last_seen_at timestamptz NOT NULL ); CREATE INDEX sessions_user ON sessions(user_id); CREATE INDEX sessions_expiry ON sessions(expires_at); CREATE TABLE api_keys ( id uuid PRIMARY KEY, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, name text NOT NULL, -- user-visible label, e.g. "Pixel 8" token_hash text NOT NULL UNIQUE, scopes text NOT NULL DEFAULT 'full', -- 'full' | 'readonly' last_used_at timestamptz, expires_at timestamptz, -- NULL = no expiry created_at timestamptz NOT NULL ); CREATE INDEX api_keys_user ON api_keys(user_id); -- Track-scoped, short-lived tokens for Cast devices /