86 KiB
ORIGIN_DATABASE_SCHEMA.md
RÉSUMÉ
Ce document définit le schéma de la base de données PostgreSQL 15 de la plateforme Veza. Tables organisées par domaine métier (DDD), avec colonnes, types, contraintes, indexes, foreign keys, triggers et vues matérialisées. Le schéma respecte les contraintes éthiques du projet : aucune table ML/IA, aucune table blockchain/NFT/crypto, aucune mécanique de gamification addictive. Les données utilisateur sont protégées par des politiques de rétention strictes et une conformité GDPR intégrée au schéma.
OBJECTIFS
Objectif principal
Définir un schéma de base de données normalisé (3NF), optimisé pour la performance, conforme GDPR, et respectant les principes éthiques du projet Veza.
Objectifs secondaires
- Assurer l'intégrité référentielle stricte
- Optimiser les requêtes fréquentes (indexes appropriés)
- Supporter la scalabilité horizontale (partitioning)
- Faciliter les migrations (versioning, rollback)
- Garantir la conformité GDPR (soft delete, export, consentement, rétention)
- Exclure toute structure de données servant le profilage comportemental, la spéculation ou la manipulation
TABLE DES MATIÈRES
- Vue d'Ensemble
- Conventions de Nommage
- Types de Données Standards
- Module Auth & Security
- Module Users & Profiles
- Module File Management
- Module Audio Streaming
- Module Chat & Messaging
- Module Social & Community
- Module Marketplace
- Module Education
- Module Hardware
- Module Cloud Storage
- Module Search
- Module Analytics
- Module Administration
- Exclusions et Raisons Éthiques
- Politique de Rétention des Données
- Conformité GDPR
- Indexes Stratégie
- Partitioning Stratégie
- Triggers & Functions
- Materialized Views
- Migration Stratégie
RÈGLES IMMUABLES
- Toutes les tables DOIVENT avoir
idPRIMARY KEY (type UUID v4) - Toutes les tables DOIVENT avoir
created_atetupdated_at(timestamp with time zone) - Soft delete OBLIGATOIRE pour tables user-facing (colonne
deleted_at) - Foreign keys TOUJOURS avec ON DELETE CASCADE ou RESTRICT explicite
- Indexes OBLIGATOIRES sur toutes foreign keys
- NOT NULL par défaut sauf si explicitement nullable
- Nommage snake_case strict (tables, colonnes, indexes, constraints)
- Pas de colonnes JSON sans index GIN si utilisées dans WHERE
- Timestamps TOUJOURS
timestamptz(avec timezone) - Enums PostgreSQL pour statuts avec max 20 valeurs
- Aucune table de stockage ML/IA (feature vectors, embeddings, model outputs, predictions)
- Aucune table blockchain/NFT/crypto (wallets, smart contracts, tokens, transactions blockchain)
- Aucune mécanique de gamification addictive (XP, streaks, leaderboards compétitifs, niveaux)
- Consentement GDPR explicite requis sur tables utilisateur (processing + marketing séparés)
- Rétention des données bornée selon la politique définie en section 18
1. VUE D'ENSEMBLE
1.1 Diagramme Global (High-Level)
erDiagram
USERS ||--o{ TRACKS : creates
USERS ||--o{ PLAYLISTS : owns
USERS ||--o{ MESSAGES : sends
USERS ||--o{ ORDERS : places
USERS ||--o{ COURSES : enrolls
TRACKS ||--o{ PLAYLIST_TRACKS : "in"
TRACKS }o--|| FILES : "stored as"
MESSAGES }o--|| ROOMS : "sent in"
PRODUCTS ||--o{ ORDERS : contains
PRODUCTS }o--|| USERS : "sold by"
COURSES ||--o{ LESSONS : contains
COURSES }o--|| USERS : "created by"
1.2 Organisation par Domaine
| Domaine | Tables | Description |
|---|---|---|
| Auth & Security | 8 | Users, sessions, tokens, 2FA |
| Profiles | 5 | User profiles, roles, badges |
| Files | 4 | Uploads, metadata, storage |
| Streaming | 8 | Tracks, playlists, queue, playback |
| Chat | 7 | Rooms, messages, presence |
| Social | 9 | Follows, posts, comments, likes |
| Marketplace | 12 | Products, orders, payments, reviews |
| Education | 7 | Courses, lessons, progress |
| Hardware | 4 | Equipment, warranties |
| Cloud | 3 | Backups, sync jobs |
| Search | 2 | Indexed data |
| Analytics | 6 | Events, metrics, reports |
| Admin | 5 | Moderation, configs |
| Other | 10+ | Notifications, integrations, etc. |
| TOTAL | ~85 tables | Après exclusion ML/blockchain/gamification addictive |
1.3 Statistiques Estimées (Après 1 an)
| Table | Rows Estimé | Size | Growth Rate |
|---|---|---|---|
users |
50,000 | ~50 MB | 1,000/month |
tracks |
500,000 | ~500 MB | 10,000/month |
messages |
50,000,000 | ~25 GB | 5M/month |
analytics_events |
500,000,000 | ~200 GB | 50M/month |
audit_logs |
100,000,000 | ~50 GB | 10M/month |
2. CONVENTIONS DE NOMMAGE
2.1 Tables
Format: {domain}_{entity} OU {entity} (si domaine évident)
Exemples:
- users (évident)
- user_profiles (évident)
- auth_sessions (domaine auth explicite)
- marketplace_products (domaine marketplace explicite)
2.2 Colonnes
Format: snake_case, descriptif
Exemples:
- user_id (foreign key)
- created_at (timestamp)
- is_active (boolean)
- email_verified_at (nullable timestamp)
2.3 Indexes
Format: idx_{table}_{column(s)}_{type}
Exemples:
- idx_users_email_unique
- idx_tracks_creator_id_btree
- idx_messages_content_gin
2.4 Foreign Keys
Format: fk_{source_table}_{target_table}
Exemples:
- fk_tracks_users
- fk_playlist_tracks_playlists
2.5 Constraints
Format: chk_{table}_{column}_{condition}
Exemples:
- chk_users_email_format
- chk_tracks_duration_positive
3. TYPES DE DONNÉES STANDARDS
3.1 Types Primitifs
| Type SQL | Usage | Exemple |
|---|---|---|
UUID |
Primary keys, references | id UUID PRIMARY KEY DEFAULT gen_random_uuid() |
VARCHAR(n) |
Strings avec limite | email VARCHAR(255) |
TEXT |
Strings illimités | bio TEXT |
INTEGER |
Nombres entiers 32-bit | view_count INTEGER DEFAULT 0 |
BIGINT |
Nombres entiers 64-bit | file_size BIGINT |
DECIMAL(p,s) |
Montants monétaires | price DECIMAL(10,2) |
BOOLEAN |
True/False | is_active BOOLEAN DEFAULT true |
TIMESTAMPTZ |
Timestamps avec timezone | created_at TIMESTAMPTZ DEFAULT NOW() |
JSONB |
Documents JSON | metadata JSONB |
BYTEA |
Données binaires | encrypted_data BYTEA |
3.2 Enums PostgreSQL
-- User roles
CREATE TYPE user_role AS ENUM ('user', 'creator', 'premium', 'moderator', 'admin');
-- Track visibility
CREATE TYPE visibility AS ENUM ('public', 'unlisted', 'private');
-- Order status
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'processing', 'completed', 'cancelled', 'refunded');
-- Message type
CREATE TYPE message_type AS ENUM ('text', 'image', 'audio', 'video', 'file');
-- Notification type
CREATE TYPE notification_type AS ENUM ('follow', 'like', 'comment', 'message', 'mention', 'system');
3.3 Types Personnalisés
-- Money with currency
CREATE TYPE money AS (
amount DECIMAL(10,2),
currency CHAR(3) -- ISO 4217 (USD, EUR, etc.)
);
-- Geolocation
CREATE TYPE point AS (
latitude DECIMAL(10,8),
longitude DECIMAL(11,8)
);
4. MODULE AUTH & SECURITY
4.1 Table users
Description: Table principale des utilisateurs.
CREATE TABLE users (
-- Primary Key
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Authentication
email VARCHAR(255) NOT NULL UNIQUE,
email_verified_at TIMESTAMPTZ,
password_hash VARCHAR(255), -- bcrypt, nullable if OAuth only
-- Profile Basic
username VARCHAR(30) NOT NULL UNIQUE,
first_name VARCHAR(100),
last_name VARCHAR(100),
display_name VARCHAR(100),
-- Role & Status
role user_role NOT NULL DEFAULT 'user',
is_active BOOLEAN NOT NULL DEFAULT true,
is_verified BOOLEAN NOT NULL DEFAULT false,
is_banned BOOLEAN NOT NULL DEFAULT false,
-- Security
token_version INTEGER NOT NULL DEFAULT 0, -- Invalidate all JWTs
last_password_change_at TIMESTAMPTZ,
-- Tracking
last_login_at TIMESTAMPTZ,
login_count INTEGER NOT NULL DEFAULT 0,
last_login_ip INET,
-- GDPR Consent
data_processing_consent BOOLEAN NOT NULL DEFAULT false,
data_processing_consent_at TIMESTAMPTZ,
marketing_consent BOOLEAN NOT NULL DEFAULT false,
marketing_consent_at TIMESTAMPTZ,
-- GDPR Data Export
export_requested_at TIMESTAMPTZ,
exported_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ, -- Soft delete
-- Constraints
CONSTRAINT chk_users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
CONSTRAINT chk_users_username_format CHECK (username ~* '^[a-zA-Z0-9_]{3,30}$')
);
-- Indexes
CREATE INDEX idx_users_email_btree ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_username_btree ON users(username) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_role_btree ON users(role);
CREATE INDEX idx_users_created_at_desc ON users(created_at DESC);
CREATE INDEX idx_users_deleted_at_btree ON users(deleted_at) WHERE deleted_at IS NOT NULL;
CREATE INDEX idx_users_export_requested_at ON users(export_requested_at) WHERE export_requested_at IS NOT NULL AND exported_at IS NULL;
-- Comments
COMMENT ON TABLE users IS 'Main users table with authentication, basic profile, and GDPR consent';
COMMENT ON COLUMN users.token_version IS 'Incremented to invalidate all existing JWTs';
COMMENT ON COLUMN users.data_processing_consent IS 'Explicit GDPR consent for data processing, separate from marketing';
COMMENT ON COLUMN users.marketing_consent IS 'Separate GDPR consent for marketing communications';
COMMENT ON COLUMN users.export_requested_at IS 'GDPR Art.20 - timestamp of data portability request';
COMMENT ON COLUMN users.exported_at IS 'GDPR Art.20 - timestamp when data export was fulfilled';
4.2 Table refresh_tokens
Description: Tokens de rafraîchissement JWT pour sessions longues.
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Token
token VARCHAR(255) NOT NULL UNIQUE,
token_hash VARCHAR(255) NOT NULL, -- SHA-256 for security
-- Metadata
device_name VARCHAR(255),
device_type VARCHAR(50), -- mobile, desktop, tablet
user_agent TEXT,
ip_address INET,
-- Expiration
expires_at TIMESTAMPTZ NOT NULL,
last_used_at TIMESTAMPTZ,
-- Status
is_revoked BOOLEAN NOT NULL DEFAULT false,
revoked_at TIMESTAMPTZ,
revoked_reason VARCHAR(255),
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT chk_refresh_tokens_expires_future CHECK (expires_at > created_at)
);
-- Indexes
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
CREATE INDEX idx_refresh_tokens_is_revoked ON refresh_tokens(is_revoked) WHERE is_revoked = false;
4.3 Table password_reset_tokens
CREATE TABLE password_reset_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Token
token VARCHAR(255) NOT NULL UNIQUE,
token_hash VARCHAR(255) NOT NULL,
-- Status
used BOOLEAN NOT NULL DEFAULT false,
used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL,
-- Metadata
ip_address INET,
user_agent TEXT,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_password_reset_expires CHECK (expires_at > created_at)
);
-- Indexes
CREATE INDEX idx_password_reset_tokens_user_id ON password_reset_tokens(user_id);
CREATE INDEX idx_password_reset_tokens_token_hash ON password_reset_tokens(token_hash);
CREATE INDEX idx_password_reset_tokens_expires_at ON password_reset_tokens(expires_at);
4.4 Table email_verification_tokens
CREATE TABLE email_verification_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Token
token VARCHAR(255) NOT NULL UNIQUE,
token_hash VARCHAR(255) NOT NULL,
-- Email
email VARCHAR(255) NOT NULL, -- Email to verify
-- Status
verified BOOLEAN NOT NULL DEFAULT false,
verified_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_email_verification_expires CHECK (expires_at > created_at)
);
-- Indexes
CREATE INDEX idx_email_verification_tokens_user_id ON email_verification_tokens(user_id);
CREATE INDEX idx_email_verification_tokens_token_hash ON email_verification_tokens(token_hash);
CREATE INDEX idx_email_verification_tokens_email ON email_verification_tokens(email);
4.5 Table password_history
CREATE TABLE password_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Password
password_hash VARCHAR(255) NOT NULL, -- bcrypt
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_password_history_user_id_created_at ON password_history(user_id, created_at DESC);
-- Comment
COMMENT ON TABLE password_history IS 'Store last 5 password hashes to prevent reuse';
4.6 Table two_factor_configs
CREATE TABLE two_factor_configs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
-- TOTP
totp_secret VARCHAR(255),
totp_enabled BOOLEAN NOT NULL DEFAULT false,
totp_enabled_at TIMESTAMPTZ,
-- Backup Codes
backup_codes JSONB, -- Array of hashed codes
-- SMS (optional)
sms_phone VARCHAR(20),
sms_enabled BOOLEAN NOT NULL DEFAULT false,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_two_factor_configs_user_id ON two_factor_configs(user_id);
4.7 Table federated_identities
Description: OAuth/SSO identities (Google, GitHub, Discord, Spotify).
CREATE TABLE federated_identities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Provider
provider VARCHAR(50) NOT NULL, -- google, github, discord, spotify
provider_user_id VARCHAR(255) NOT NULL,
-- OAuth Data
access_token TEXT,
refresh_token TEXT,
token_expires_at TIMESTAMPTZ,
-- Profile Data (from provider)
provider_email VARCHAR(255),
provider_username VARCHAR(255),
provider_avatar_url TEXT,
provider_profile_data JSONB, -- Full profile response
-- Status
is_primary BOOLEAN NOT NULL DEFAULT false, -- Primary login method
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_federated_identities_provider_user UNIQUE (provider, provider_user_id)
);
-- Indexes
CREATE INDEX idx_federated_identities_user_id ON federated_identities(user_id);
CREATE INDEX idx_federated_identities_provider ON federated_identities(provider);
CREATE UNIQUE INDEX idx_federated_identities_provider_user_id ON federated_identities(provider, provider_user_id);
4.8 Table login_attempts
Description: Track failed login attempts for brute-force protection.
CREATE TABLE login_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Identifier (email or username)
identifier VARCHAR(255) NOT NULL,
-- Result
success BOOLEAN NOT NULL,
failure_reason VARCHAR(100), -- invalid_password, account_locked, etc.
-- Metadata
ip_address INET NOT NULL,
user_agent TEXT,
-- Timestamp
attempted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_login_attempts_identifier_attempted_at ON login_attempts(identifier, attempted_at DESC);
CREATE INDEX idx_login_attempts_ip_address_attempted_at ON login_attempts(ip_address, attempted_at DESC);
CREATE INDEX idx_login_attempts_success ON login_attempts(success);
-- Partitioning (by month)
-- Implementation: Create partitions dynamically or use pg_partman
5. MODULE USERS & PROFILES
5.1 Table user_profiles
CREATE TABLE user_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
-- Profile Info
bio TEXT,
tagline VARCHAR(255),
location VARCHAR(255),
website_url VARCHAR(500),
-- Personal Info
birthdate DATE,
gender VARCHAR(50),
-- Media
avatar_url TEXT,
banner_url TEXT,
-- Preferences
language VARCHAR(5) DEFAULT 'en', -- ISO 639-1
timezone VARCHAR(50) DEFAULT 'UTC',
theme VARCHAR(20) DEFAULT 'auto', -- light, dark, auto
-- Privacy
profile_visibility visibility NOT NULL DEFAULT 'public',
show_email BOOLEAN NOT NULL DEFAULT false,
show_location BOOLEAN NOT NULL DEFAULT true,
-- Counts (denormalized for performance)
follower_count INTEGER NOT NULL DEFAULT 0,
following_count INTEGER NOT NULL DEFAULT 0,
track_count INTEGER NOT NULL DEFAULT 0,
playlist_count INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_user_profiles_user_id ON user_profiles(user_id);
CREATE INDEX idx_user_profiles_location ON user_profiles(location) WHERE location IS NOT NULL;
5.2 Table user_settings
CREATE TABLE user_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
-- Notification Preferences
email_notifications BOOLEAN NOT NULL DEFAULT true,
push_notifications BOOLEAN NOT NULL DEFAULT true,
browser_notifications BOOLEAN NOT NULL DEFAULT true,
-- Email Notification Types
email_on_follow BOOLEAN NOT NULL DEFAULT true,
email_on_like BOOLEAN NOT NULL DEFAULT true,
email_on_comment BOOLEAN NOT NULL DEFAULT true,
email_on_message BOOLEAN NOT NULL DEFAULT true,
email_on_mention BOOLEAN NOT NULL DEFAULT true,
email_marketing BOOLEAN NOT NULL DEFAULT false,
-- Privacy
allow_search_indexing BOOLEAN NOT NULL DEFAULT true,
show_activity BOOLEAN NOT NULL DEFAULT true,
-- Content
explicit_content BOOLEAN NOT NULL DEFAULT false,
autoplay BOOLEAN NOT NULL DEFAULT true,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_user_settings_user_id ON user_settings(user_id);
5.3 Table user_roles
CREATE TABLE user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Role
role VARCHAR(50) NOT NULL, -- creator, producer, label, educator, etc.
-- Status
verified BOOLEAN NOT NULL DEFAULT false,
verified_at TIMESTAMPTZ,
verified_by UUID REFERENCES users(id),
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_user_roles_user_role UNIQUE (user_id, role)
);
-- Indexes
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_user_roles_role ON user_roles(role);
5.4 Table user_badges
CREATE TABLE user_badges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Badge
badge_id UUID NOT NULL REFERENCES badges(id) ON DELETE CASCADE,
-- Display
is_displayed BOOLEAN NOT NULL DEFAULT true,
display_order INTEGER,
-- Timestamps
earned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_user_badges_user_badge UNIQUE (user_id, badge_id)
);
CREATE INDEX idx_user_badges_user_id ON user_badges(user_id);
CREATE INDEX idx_user_badges_badge_id ON user_badges(badge_id);
5.5 Table badges
Description: Badges déclaratifs de compétences musicales (instruments, genres, expérience). Pas de rareté, pas de collection compétitive. Un badge atteste d'une compétence ou d'un rôle, il n'est pas un mécanisme de rétention.
CREATE TABLE badges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Badge Info
name VARCHAR(100) NOT NULL UNIQUE,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
-- Category
category VARCHAR(50) NOT NULL, -- instrument, genre, experience, role
-- Display
icon_url TEXT,
color VARCHAR(7), -- Hex color #RRGGBB
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_badges_slug ON badges(slug);
CREATE INDEX idx_badges_category ON badges(category);
-- Comment
COMMENT ON TABLE badges IS 'Declarative skill badges (instruments, genres, experience). No rarity, no competitive collection.';
6. MODULE FILE MANAGEMENT
6.1 Table files
CREATE TABLE files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- File Info
filename VARCHAR(255) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
file_size BIGINT NOT NULL, -- bytes
-- Storage
storage_path TEXT NOT NULL, -- S3 key or local path
storage_provider VARCHAR(50) NOT NULL DEFAULT 's3', -- s3, local, minio
bucket_name VARCHAR(255),
-- URLs
url TEXT NOT NULL,
thumbnail_url TEXT,
-- Metadata
file_hash VARCHAR(64), -- SHA-256
metadata JSONB, -- Extract metadata (dimensions, duration, etc.)
-- Processing
is_processed BOOLEAN NOT NULL DEFAULT false,
processed_at TIMESTAMPTZ,
processing_error TEXT,
-- Security
virus_scanned BOOLEAN NOT NULL DEFAULT false,
virus_scan_result VARCHAR(50),
virus_scanned_at TIMESTAMPTZ,
-- Visibility
is_public BOOLEAN NOT NULL DEFAULT false,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
-- Constraints
CONSTRAINT chk_files_size_positive CHECK (file_size > 0)
);
-- Indexes
CREATE INDEX idx_files_user_id ON files(user_id);
CREATE INDEX idx_files_mime_type ON files(mime_type);
CREATE INDEX idx_files_file_hash ON files(file_hash) WHERE file_hash IS NOT NULL;
CREATE INDEX idx_files_created_at_desc ON files(created_at DESC);
6.2 Table file_uploads
Description: Track upload sessions (for resumable uploads).
CREATE TABLE file_uploads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Upload Info
filename VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(100) NOT NULL,
-- Progress
bytes_uploaded BIGINT NOT NULL DEFAULT 0,
chunks_uploaded INTEGER NOT NULL DEFAULT 0,
total_chunks INTEGER,
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, uploading, processing, completed, failed
-- Storage
storage_key TEXT,
upload_id TEXT, -- S3 multipart upload ID
-- Metadata
metadata JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL, -- Auto-cleanup incomplete uploads
CONSTRAINT chk_file_uploads_bytes_uploaded CHECK (bytes_uploaded >= 0 AND bytes_uploaded <= file_size)
);
-- Indexes
CREATE INDEX idx_file_uploads_user_id ON file_uploads(user_id);
CREATE INDEX idx_file_uploads_status ON file_uploads(status);
CREATE INDEX idx_file_uploads_expires_at ON file_uploads(expires_at);
6.3 Table file_metadata
CREATE TABLE file_metadata (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE UNIQUE,
-- Audio Metadata (if audio file)
title VARCHAR(255),
artist VARCHAR(255),
album VARCHAR(255),
genre VARCHAR(100),
year INTEGER,
duration INTEGER, -- seconds
bitrate INTEGER, -- kbps
sample_rate INTEGER, -- Hz
channels INTEGER,
codec VARCHAR(50),
-- Image Metadata (if image file)
width INTEGER,
height INTEGER,
format VARCHAR(50),
-- Video Metadata (if video file)
video_codec VARCHAR(50),
audio_codec VARCHAR(50),
framerate DECIMAL(10,2),
-- Advanced Metadata
bpm INTEGER, -- Beats per minute
musical_key VARCHAR(10), -- C, C#, D, etc.
time_signature VARCHAR(10), -- 4/4, 3/4, etc.
-- Raw Metadata
raw_metadata JSONB, -- Full ID3/EXIF data
-- Timestamps
extracted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_file_metadata_file_id ON file_metadata(file_id);
CREATE INDEX idx_file_metadata_genre ON file_metadata(genre) WHERE genre IS NOT NULL;
CREATE INDEX idx_file_metadata_duration ON file_metadata(duration) WHERE duration IS NOT NULL;
6.4 Table file_conversions
CREATE TABLE file_conversions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE,
converted_file_id UUID REFERENCES files(id) ON DELETE SET NULL,
-- Conversion
target_format VARCHAR(50) NOT NULL,
target_quality VARCHAR(50),
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed
progress INTEGER NOT NULL DEFAULT 0, -- 0-100%
-- Error
error_message TEXT,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
-- Indexes
CREATE INDEX idx_file_conversions_source_file_id ON file_conversions(source_file_id);
CREATE INDEX idx_file_conversions_status ON file_conversions(status);
7. MODULE AUDIO STREAMING
7.1 Table tracks
CREATE TABLE tracks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
file_id UUID NOT NULL REFERENCES files(id) ON DELETE RESTRICT,
-- Track Info
title VARCHAR(255) NOT NULL,
description TEXT,
artist VARCHAR(255),
album VARCHAR(255),
genre VARCHAR(100),
-- Audio Properties
duration INTEGER NOT NULL, -- seconds
bpm INTEGER,
musical_key VARCHAR(10),
-- Visibility
visibility visibility NOT NULL DEFAULT 'public',
is_downloadable BOOLEAN NOT NULL DEFAULT false,
-- Media
cover_art_file_id UUID REFERENCES files(id) ON DELETE SET NULL,
waveform_data JSONB, -- Waveform visualization data
-- Counts (denormalized)
play_count INTEGER NOT NULL DEFAULT 0,
like_count INTEGER NOT NULL DEFAULT 0,
comment_count INTEGER NOT NULL DEFAULT 0,
download_count INTEGER NOT NULL DEFAULT 0,
-- Timestamps
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
-- Constraints
CONSTRAINT chk_tracks_duration_positive CHECK (duration > 0)
);
-- Indexes
CREATE INDEX idx_tracks_creator_id ON tracks(creator_id);
CREATE INDEX idx_tracks_genre ON tracks(genre);
CREATE INDEX idx_tracks_visibility ON tracks(visibility);
CREATE INDEX idx_tracks_published_at_desc ON tracks(published_at DESC) WHERE published_at IS NOT NULL;
CREATE INDEX idx_tracks_play_count_desc ON tracks(play_count DESC);
CREATE INDEX idx_tracks_created_at_desc ON tracks(created_at DESC);
-- Full-text search
CREATE INDEX idx_tracks_search_gin ON tracks USING GIN(to_tsvector('english', title || ' ' || COALESCE(artist, '') || ' ' || COALESCE(album, '')));
7.2 Table playlists
CREATE TABLE playlists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Playlist Info
name VARCHAR(255) NOT NULL,
description TEXT,
-- Media
cover_url TEXT,
-- Properties
visibility visibility NOT NULL DEFAULT 'public',
is_collaborative BOOLEAN NOT NULL DEFAULT false,
-- Counts
track_count INTEGER NOT NULL DEFAULT 0,
duration_seconds INTEGER NOT NULL DEFAULT 0,
follower_count INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Indexes
CREATE INDEX idx_playlists_user_id ON playlists(user_id);
CREATE INDEX idx_playlists_visibility ON playlists(visibility);
CREATE INDEX idx_playlists_created_at_desc ON playlists(created_at DESC);
7.3 Table playlist_tracks
CREATE TABLE playlist_tracks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
playlist_id UUID NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
-- Order
position INTEGER NOT NULL,
-- Metadata
added_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_playlist_tracks_playlist_track UNIQUE (playlist_id, track_id)
);
-- Indexes
CREATE INDEX idx_playlist_tracks_playlist_id_position ON playlist_tracks(playlist_id, position);
CREATE INDEX idx_playlist_tracks_track_id ON playlist_tracks(track_id);
CREATE INDEX idx_playlist_tracks_added_by ON playlist_tracks(added_by);
7.4 Table playback_history
CREATE TABLE playback_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
-- Playback
played_duration INTEGER NOT NULL, -- seconds actually played
completion_percentage INTEGER NOT NULL, -- 0-100
-- Context
source VARCHAR(50), -- playlist, album, search, direct
source_id UUID, -- ID of playlist, album, etc.
-- Device
device_type VARCHAR(50), -- mobile, desktop, web
-- Timestamps
played_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_playback_history_completion CHECK (completion_percentage >= 0 AND completion_percentage <= 100)
);
-- Indexes
CREATE INDEX idx_playback_history_user_id_played_at ON playback_history(user_id, played_at DESC);
CREATE INDEX idx_playback_history_track_id ON playback_history(track_id);
-- Partitioning by month (pg_partman)
-- This table will grow very large, partition by played_at
7.5 Table track_likes
CREATE TABLE track_likes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_track_likes_user_track UNIQUE (user_id, track_id)
);
-- Indexes
CREATE INDEX idx_track_likes_user_id ON track_likes(user_id);
CREATE INDEX idx_track_likes_track_id_created_at ON track_likes(track_id, created_at DESC);
7.6 Table track_comments
CREATE TABLE track_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Comment
content TEXT NOT NULL,
-- Threading
parent_comment_id UUID REFERENCES track_comments(id) ON DELETE CASCADE,
-- Timestamp in track (for waveform comments)
timestamp_seconds INTEGER, -- NULL if general comment
-- Moderation
is_edited BOOLEAN NOT NULL DEFAULT false,
is_deleted BOOLEAN NOT NULL DEFAULT false,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_track_comments_content_length CHECK (LENGTH(content) >= 1 AND LENGTH(content) <= 5000)
);
-- Indexes
CREATE INDEX idx_track_comments_track_id_created_at ON track_comments(track_id, created_at DESC);
CREATE INDEX idx_track_comments_user_id ON track_comments(user_id);
CREATE INDEX idx_track_comments_parent_comment_id ON track_comments(parent_comment_id) WHERE parent_comment_id IS NOT NULL;
CREATE INDEX idx_track_comments_timestamp_seconds ON track_comments(track_id, timestamp_seconds) WHERE timestamp_seconds IS NOT NULL;
7.7 Table queues
Description: User playback queues (current listening session).
CREATE TABLE queues (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
-- Current Track
current_track_id UUID REFERENCES tracks(id) ON DELETE SET NULL,
current_position INTEGER NOT NULL DEFAULT 0, -- seconds
-- Playback State
is_playing BOOLEAN NOT NULL DEFAULT false,
shuffle BOOLEAN NOT NULL DEFAULT false,
repeat_mode VARCHAR(20) NOT NULL DEFAULT 'off', -- off, track, queue
volume INTEGER NOT NULL DEFAULT 100, -- 0-100
-- Timestamps
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_queues_user_id ON queues(user_id);
7.8 Table queue_items
CREATE TABLE queue_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
queue_id UUID NOT NULL REFERENCES queues(id) ON DELETE CASCADE,
track_id UUID NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
-- Order
position INTEGER NOT NULL,
-- Timestamps
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_queue_items_queue_id_position ON queue_items(queue_id, position);
8. MODULE CHAT & MESSAGING
8.1 Table rooms
CREATE TABLE rooms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Room Info
name VARCHAR(255),
slug VARCHAR(100) UNIQUE, -- For public rooms
description TEXT,
-- Type
room_type VARCHAR(50) NOT NULL, -- public, private, dm (direct message)
-- Visibility
is_private BOOLEAN NOT NULL DEFAULT false,
password_hash VARCHAR(255), -- For password-protected rooms
-- Limits
max_members INTEGER,
-- Creator
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Counts
member_count INTEGER NOT NULL DEFAULT 0,
message_count INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Indexes
CREATE INDEX idx_rooms_creator_id ON rooms(creator_id);
CREATE INDEX idx_rooms_room_type ON rooms(room_type);
CREATE UNIQUE INDEX idx_rooms_slug ON rooms(slug) WHERE slug IS NOT NULL;
8.2 Table room_members
CREATE TABLE room_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Role
role VARCHAR(50) NOT NULL DEFAULT 'member', -- owner, admin, moderator, member
-- Status
is_banned BOOLEAN NOT NULL DEFAULT false,
is_muted BOOLEAN NOT NULL DEFAULT false,
-- Read Status
last_read_at TIMESTAMPTZ,
-- Timestamps
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_room_members_room_user UNIQUE (room_id, user_id)
);
-- Indexes
CREATE INDEX idx_room_members_room_id ON room_members(room_id);
CREATE INDEX idx_room_members_user_id ON room_members(user_id);
CREATE INDEX idx_room_members_role ON room_members(role);
8.3 Table messages
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Message Content
content TEXT NOT NULL,
message_type message_type NOT NULL DEFAULT 'text',
-- Attachments
attachment_file_id UUID REFERENCES files(id) ON DELETE SET NULL,
-- Threading
reply_to_id UUID REFERENCES messages(id) ON DELETE SET NULL,
-- Status
is_edited BOOLEAN NOT NULL DEFAULT false,
edited_at TIMESTAMPTZ,
is_deleted BOOLEAN NOT NULL DEFAULT false,
is_pinned BOOLEAN NOT NULL DEFAULT false,
-- Metadata
metadata JSONB, -- Embeds, mentions, etc.
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_messages_content_length CHECK (LENGTH(content) >= 1 AND LENGTH(content) <= 10000)
);
-- Indexes
CREATE INDEX idx_messages_room_id_created_at ON messages(room_id, created_at DESC);
CREATE INDEX idx_messages_sender_id ON messages(sender_id);
CREATE INDEX idx_messages_reply_to_id ON messages(reply_to_id) WHERE reply_to_id IS NOT NULL;
CREATE INDEX idx_messages_is_pinned ON messages(room_id, is_pinned) WHERE is_pinned = true;
-- Full-text search
CREATE INDEX idx_messages_content_gin ON messages USING GIN(to_tsvector('english', content));
-- Partitioning by created_at (monthly)
-- This is a high-volume table
8.4 Table message_reactions
CREATE TABLE message_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Reaction
emoji VARCHAR(10) NOT NULL, -- Unicode emoji
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_message_reactions_message_user_emoji UNIQUE (message_id, user_id, emoji)
);
-- Indexes
CREATE INDEX idx_message_reactions_message_id ON message_reactions(message_id);
CREATE INDEX idx_message_reactions_user_id ON message_reactions(user_id);
8.5 Table direct_messages
Description: Direct messages 1-to-1 (simplified, not using rooms).
CREATE TABLE direct_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Message
content TEXT NOT NULL,
message_type message_type NOT NULL DEFAULT 'text',
attachment_file_id UUID REFERENCES files(id) ON DELETE SET NULL,
-- Status
is_read BOOLEAN NOT NULL DEFAULT false,
read_at TIMESTAMPTZ,
is_deleted_by_sender BOOLEAN NOT NULL DEFAULT false,
is_deleted_by_recipient BOOLEAN NOT NULL DEFAULT false,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_direct_messages_content_length CHECK (LENGTH(content) >= 1 AND LENGTH(content) <= 10000),
CONSTRAINT chk_direct_messages_different_users CHECK (sender_id != recipient_id)
);
-- Indexes
CREATE INDEX idx_direct_messages_sender_id_created_at ON direct_messages(sender_id, created_at DESC);
CREATE INDEX idx_direct_messages_recipient_id_created_at ON direct_messages(recipient_id, created_at DESC);
CREATE INDEX idx_direct_messages_is_read ON direct_messages(recipient_id, is_read) WHERE is_read = false;
-- Composite index for conversation view
CREATE INDEX idx_direct_messages_conversation ON direct_messages(
LEAST(sender_id, recipient_id),
GREATEST(sender_id, recipient_id),
created_at DESC
);
8.6 Table user_presence
CREATE TABLE user_presence (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'offline', -- online, away, busy, offline
custom_status VARCHAR(255),
-- Activity
current_activity VARCHAR(100), -- listening_to, in_room, etc.
activity_data JSONB,
-- Timestamps
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_user_presence_status ON user_presence(status);
CREATE INDEX idx_user_presence_last_seen_at ON user_presence(last_seen_at DESC);
8.7 Table typing_indicators
Description: Ephemeral typing indicators (Redis preferred, but DB fallback).
CREATE TABLE typing_indicators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Timestamps
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '10 seconds',
CONSTRAINT uq_typing_indicators_room_user UNIQUE (room_id, user_id)
);
-- Indexes
CREATE INDEX idx_typing_indicators_room_id_expires_at ON typing_indicators(room_id, expires_at);
-- Auto-cleanup with trigger or cron job
9. MODULE SOCIAL & COMMUNITY
9.1 Table follows
CREATE TABLE follows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
follower_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
following_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_follows_follower_following UNIQUE (follower_id, following_id),
CONSTRAINT chk_follows_not_self CHECK (follower_id != following_id)
);
-- Indexes
CREATE INDEX idx_follows_follower_id ON follows(follower_id);
CREATE INDEX idx_follows_following_id ON follows(following_id);
CREATE INDEX idx_follows_created_at_desc ON follows(created_at DESC);
9.2 Table blocks
CREATE TABLE blocks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
blocker_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
blocked_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Reason
reason VARCHAR(255),
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_blocks_blocker_blocked UNIQUE (blocker_id, blocked_id),
CONSTRAINT chk_blocks_not_self CHECK (blocker_id != blocked_id)
);
-- Indexes
CREATE INDEX idx_blocks_blocker_id ON blocks(blocker_id);
CREATE INDEX idx_blocks_blocked_id ON blocks(blocked_id);
9.3 Table posts
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Content
content TEXT NOT NULL,
-- Attachments
image_file_ids UUID[], -- Array of file IDs
audio_file_id UUID REFERENCES files(id) ON DELETE SET NULL,
video_file_id UUID REFERENCES files(id) ON DELETE SET NULL,
-- Repost
repost_of_id UUID REFERENCES posts(id) ON DELETE CASCADE,
-- Visibility
visibility visibility NOT NULL DEFAULT 'public',
-- Counts
like_count INTEGER NOT NULL DEFAULT 0,
comment_count INTEGER NOT NULL DEFAULT 0,
repost_count INTEGER NOT NULL DEFAULT 0,
-- Moderation
is_pinned BOOLEAN NOT NULL DEFAULT false,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_posts_content_length CHECK (LENGTH(content) >= 1 AND LENGTH(content) <= 5000)
);
-- Indexes
CREATE INDEX idx_posts_user_id_created_at ON posts(user_id, created_at DESC);
CREATE INDEX idx_posts_created_at_desc ON posts(created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_posts_repost_of_id ON posts(repost_of_id) WHERE repost_of_id IS NOT NULL;
CREATE INDEX idx_posts_visibility ON posts(visibility);
-- Full-text search
CREATE INDEX idx_posts_content_gin ON posts USING GIN(to_tsvector('english', content));
9.4 Table post_likes
CREATE TABLE post_likes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_post_likes_user_post UNIQUE (user_id, post_id)
);
-- Indexes
CREATE INDEX idx_post_likes_user_id ON post_likes(user_id);
CREATE INDEX idx_post_likes_post_id_created_at ON post_likes(post_id, created_at DESC);
9.5 Table post_comments
CREATE TABLE post_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Comment
content TEXT NOT NULL,
-- Threading
parent_comment_id UUID REFERENCES post_comments(id) ON DELETE CASCADE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_post_comments_content_length CHECK (LENGTH(content) >= 1 AND LENGTH(content) <= 2000)
);
-- Indexes
CREATE INDEX idx_post_comments_post_id_created_at ON post_comments(post_id, created_at DESC);
CREATE INDEX idx_post_comments_user_id ON post_comments(user_id);
CREATE INDEX idx_post_comments_parent_comment_id ON post_comments(parent_comment_id) WHERE parent_comment_id IS NOT NULL;
9.6 Table hashtags
CREATE TABLE hashtags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Hashtag
tag VARCHAR(100) NOT NULL UNIQUE,
slug VARCHAR(100) NOT NULL UNIQUE,
-- Counts
usage_count INTEGER NOT NULL DEFAULT 0,
-- Timestamps
first_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_hashtags_tag ON hashtags(LOWER(tag));
CREATE INDEX idx_hashtags_usage_count_desc ON hashtags(usage_count DESC);
9.7 Table post_hashtags
CREATE TABLE post_hashtags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
hashtag_id UUID NOT NULL REFERENCES hashtags(id) ON DELETE CASCADE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_post_hashtags_post_hashtag UNIQUE (post_id, hashtag_id)
);
-- Indexes
CREATE INDEX idx_post_hashtags_post_id ON post_hashtags(post_id);
CREATE INDEX idx_post_hashtags_hashtag_id_created_at ON post_hashtags(hashtag_id, created_at DESC);
9.8 Table groups
CREATE TABLE groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Group Info
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
-- Media
avatar_url TEXT,
banner_url TEXT,
-- Type
group_type VARCHAR(50) NOT NULL DEFAULT 'public', -- public, private
-- Settings
requires_approval BOOLEAN NOT NULL DEFAULT false,
-- Creator
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Counts
member_count INTEGER NOT NULL DEFAULT 0,
post_count INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Indexes
CREATE UNIQUE INDEX idx_groups_slug ON groups(slug);
CREATE INDEX idx_groups_creator_id ON groups(creator_id);
CREATE INDEX idx_groups_group_type ON groups(group_type);
9.9 Table group_members
CREATE TABLE group_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Role
role VARCHAR(50) NOT NULL DEFAULT 'member', -- owner, admin, moderator, member
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'active', -- pending, active, banned
-- Timestamps
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
approved_at TIMESTAMPTZ,
CONSTRAINT uq_group_members_group_user UNIQUE (group_id, user_id)
);
-- Indexes
CREATE INDEX idx_group_members_group_id ON group_members(group_id);
CREATE INDEX idx_group_members_user_id ON group_members(user_id);
CREATE INDEX idx_group_members_status ON group_members(status);
10. MODULE MARKETPLACE
10.1 Table products
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
seller_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Product Info
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
description TEXT NOT NULL,
-- Category
category VARCHAR(100) NOT NULL, -- sample, beat, preset, template, service
tags VARCHAR(50)[],
-- Pricing
price DECIMAL(10,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
pricing_model VARCHAR(50) NOT NULL DEFAULT 'fixed', -- fixed, pwyw (pay what you want), free
minimum_price DECIMAL(10,2), -- For PWYW
-- Files
preview_file_id UUID REFERENCES files(id) ON DELETE SET NULL,
demo_url TEXT,
download_file_ids UUID[],
-- Images
image_file_ids UUID[],
thumbnail_url TEXT,
-- Audio Properties (if applicable)
bpm INTEGER,
musical_key VARCHAR(10),
genre VARCHAR(100),
-- Formats
formats VARCHAR(50)[], -- WAV, MP3, FLAC, VST, etc.
-- License
license_type VARCHAR(100),
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'draft', -- draft, active, inactive, suspended
-- Counts
view_count INTEGER NOT NULL DEFAULT 0,
favorite_count INTEGER NOT NULL DEFAULT 0,
sale_count INTEGER NOT NULL DEFAULT 0,
review_count INTEGER NOT NULL DEFAULT 0,
-- Rating
average_rating DECIMAL(3,2) DEFAULT 0, -- 0.00-5.00
-- Timestamps
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_products_price_positive CHECK (price >= 0),
CONSTRAINT chk_products_rating_range CHECK (average_rating >= 0 AND average_rating <= 5)
);
-- Indexes
CREATE UNIQUE INDEX idx_products_slug ON products(slug);
CREATE INDEX idx_products_seller_id ON products(seller_id);
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_status ON products(status);
CREATE INDEX idx_products_published_at_desc ON products(published_at DESC) WHERE published_at IS NOT NULL;
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_products_sale_count_desc ON products(sale_count DESC);
CREATE INDEX idx_products_tags_gin ON products USING GIN(tags);
-- Full-text search
CREATE INDEX idx_products_search_gin ON products USING GIN(to_tsvector('english', name || ' ' || description));
10.2 Table product_licenses
CREATE TABLE product_licenses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
-- License Info
name VARCHAR(255) NOT NULL,
description TEXT,
-- Pricing
price DECIMAL(10,2) NOT NULL,
-- Terms
terms TEXT NOT NULL,
usage_rights JSONB, -- Structured usage rights
-- Limits
is_exclusive BOOLEAN NOT NULL DEFAULT false,
distribution_limit INTEGER, -- Max units can be sold
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_product_licenses_product_id ON product_licenses(product_id);
10.3 Table carts
CREATE TABLE carts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE,
-- Totals (denormalized)
item_count INTEGER NOT NULL DEFAULT 0,
subtotal DECIMAL(10,2) NOT NULL DEFAULT 0,
tax_total DECIMAL(10,2) NOT NULL DEFAULT 0,
total DECIMAL(10,2) NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE UNIQUE INDEX idx_carts_user_id ON carts(user_id);
10.4 Table cart_items
CREATE TABLE cart_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cart_id UUID NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
license_id UUID REFERENCES product_licenses(id) ON DELETE SET NULL,
-- Price (snapshot at add time)
price DECIMAL(10,2) NOT NULL,
-- Timestamps
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_cart_items_cart_product UNIQUE (cart_id, product_id)
);
-- Indexes
CREATE INDEX idx_cart_items_cart_id ON cart_items(cart_id);
CREATE INDEX idx_cart_items_product_id ON cart_items(product_id);
10.5 Table orders
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Order Number
order_number VARCHAR(50) NOT NULL UNIQUE, -- Human-readable (ORD-2025-00001)
-- Pricing
subtotal DECIMAL(10,2) NOT NULL,
tax_total DECIMAL(10,2) NOT NULL,
discount_total DECIMAL(10,2) NOT NULL DEFAULT 0,
total DECIMAL(10,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
-- Payment
payment_method VARCHAR(50), -- stripe, paypal
payment_intent_id VARCHAR(255), -- Stripe payment intent ID
-- Status
status order_status NOT NULL DEFAULT 'pending',
-- Billing
billing_email VARCHAR(255) NOT NULL,
billing_name VARCHAR(255),
billing_address JSONB,
-- Timestamps
paid_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
refunded_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_orders_total_positive CHECK (total >= 0)
);
-- Indexes
CREATE UNIQUE INDEX idx_orders_order_number ON orders(order_number);
CREATE INDEX idx_orders_user_id_created_at ON orders(user_id, created_at DESC);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at_desc ON orders(created_at DESC);
10.6 Table order_items
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
license_id UUID REFERENCES product_licenses(id) ON DELETE SET NULL,
-- Product Snapshot (at purchase time)
product_name VARCHAR(255) NOT NULL,
product_description TEXT,
seller_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
-- Pricing Snapshot
price DECIMAL(10,2) NOT NULL,
-- Download
download_file_ids UUID[],
download_count INTEGER NOT NULL DEFAULT 0,
-- License
license_key VARCHAR(255), -- Generated license key
license_terms TEXT,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
CREATE INDEX idx_order_items_seller_id ON order_items(seller_id);
10.7 Table product_reviews
CREATE TABLE product_reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
order_item_id UUID NOT NULL REFERENCES order_items(id) ON DELETE CASCADE,
-- Review
rating INTEGER NOT NULL, -- 1-5
title VARCHAR(255),
content TEXT,
-- Verification
is_verified_purchase BOOLEAN NOT NULL DEFAULT true,
-- Response
seller_response TEXT,
seller_responded_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_product_reviews_order_item UNIQUE (order_item_id),
CONSTRAINT chk_product_reviews_rating CHECK (rating >= 1 AND rating <= 5),
CONSTRAINT chk_product_reviews_content_length CHECK (LENGTH(content) >= 10 AND LENGTH(content) <= 2000)
);
-- Indexes
CREATE INDEX idx_product_reviews_product_id_created_at ON product_reviews(product_id, created_at DESC);
CREATE INDEX idx_product_reviews_user_id ON product_reviews(user_id);
CREATE INDEX idx_product_reviews_rating ON product_reviews(product_id, rating);
10.8 Table product_favorites
CREATE TABLE product_favorites (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_product_favorites_user_product UNIQUE (user_id, product_id)
);
-- Indexes
CREATE INDEX idx_product_favorites_user_id ON product_favorites(user_id);
CREATE INDEX idx_product_favorites_product_id_created_at ON product_favorites(product_id, created_at DESC);
10.9 Table seller_payouts
CREATE TABLE seller_payouts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
seller_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Payout Info
payout_number VARCHAR(50) NOT NULL UNIQUE,
-- Amount
amount DECIMAL(10,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
-- Method
payout_method VARCHAR(50) NOT NULL, -- stripe_connect, paypal, bank_transfer
payout_account_id VARCHAR(255), -- Stripe Connect account ID
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed
-- Timestamps
processed_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
failure_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_seller_payouts_amount_positive CHECK (amount > 0)
);
-- Indexes
CREATE INDEX idx_seller_payouts_seller_id_created_at ON seller_payouts(seller_id, created_at DESC);
CREATE INDEX idx_seller_payouts_status ON seller_payouts(status);
10.10 Table discount_codes
CREATE TABLE discount_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Code
code VARCHAR(50) NOT NULL UNIQUE,
-- Discount
discount_type VARCHAR(50) NOT NULL, -- percentage, fixed_amount
discount_value DECIMAL(10,2) NOT NULL,
-- Constraints
minimum_purchase_amount DECIMAL(10,2),
maximum_discount_amount DECIMAL(10,2),
-- Usage Limits
usage_limit INTEGER,
usage_count INTEGER NOT NULL DEFAULT 0,
-- Validity
valid_from TIMESTAMPTZ NOT NULL,
valid_until TIMESTAMPTZ NOT NULL,
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Creator
creator_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_discount_codes_value_positive CHECK (discount_value > 0),
CONSTRAINT chk_discount_codes_validity CHECK (valid_until > valid_from)
);
-- Indexes
CREATE UNIQUE INDEX idx_discount_codes_code ON discount_codes(UPPER(code));
CREATE INDEX idx_discount_codes_valid_period ON discount_codes(valid_from, valid_until) WHERE is_active = true;
10.11 Table discount_code_usage
CREATE TABLE discount_code_usage (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
discount_code_id UUID NOT NULL REFERENCES discount_codes(id) ON DELETE CASCADE,
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE UNIQUE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Discount Applied
discount_amount DECIMAL(10,2) NOT NULL,
-- Timestamps
used_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_discount_code_usage_discount_code_id ON discount_code_usage(discount_code_id);
CREATE INDEX idx_discount_code_usage_user_id ON discount_code_usage(user_id);
10.12 Table transactions
Description: Financial transactions (payments, refunds, payouts).
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Type
transaction_type VARCHAR(50) NOT NULL, -- payment, refund, payout, commission
-- Related Entities
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
order_id UUID REFERENCES orders(id) ON DELETE SET NULL,
payout_id UUID REFERENCES seller_payouts(id) ON DELETE SET NULL,
-- Amount
amount DECIMAL(10,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
-- Payment Provider
provider VARCHAR(50) NOT NULL, -- stripe, paypal
provider_transaction_id VARCHAR(255),
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, completed, failed, cancelled
-- Metadata
metadata JSONB,
-- Timestamps
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_transactions_user_id_created_at ON transactions(user_id, created_at DESC);
CREATE INDEX idx_transactions_order_id ON transactions(order_id);
CREATE INDEX idx_transactions_transaction_type ON transactions(transaction_type);
CREATE INDEX idx_transactions_status ON transactions(status);
CREATE INDEX idx_transactions_created_at_desc ON transactions(created_at DESC);
11-16. MODULES RESTANTS (STRUCTURE)
11. Module Education (7 tables)
courses- Course cataloglessons- Course lessons/modulescourse_enrollments- User enrollmentslesson_progress- Lesson completion trackingquizzes- Assessmentsquiz_attempts- User quiz submissionscertificates- Completion certificates
12. Module Hardware (4 tables)
equipment- User equipment inventoryequipment_warranties- Warranty trackingequipment_maintenance- Maintenance historyequipment_categories- Equipment types
13. Module Cloud Storage (3 tables)
cloud_accounts- Nextcloud/cloud integrationsbackup_jobs- Automated backupssync_operations- File sync tracking
14. Module Search (2 tables)
search_queries- User search historysearch_index- Global search index
15. Module Analytics (5 tables)
analytics_events- Raw event data (partitioned, rétention 12 mois)daily_metrics- Aggregated daily statstrack_analytics- Per-track metrics (play count, completion, pas de profilage)reports- Generated reportsdashboard_configs- Custom dashboards
16. Module Administration (5 tables)
moderation_reports- User reportsmoderation_actions- Moderator actionsaudit_logs- System audit trail (partitioned)system_configs- Application settingsfeature_flags- Feature toggles
17. EXCLUSIONS ET RAISONS ÉTHIQUES
Ce schéma exclut explicitement plusieurs catégories de tables qui existaient dans la version 1.0.0 ou qui avaient été envisagées. Chaque exclusion est une contrainte architecturale, pas un report à plus tard.
17.1 Tables ML/IA supprimées
| Table supprimée | Raison |
|---|---|
feature_vectors |
Stockage de vecteurs pour embeddings audio/utilisateur. Le profilage algorithmique des goûts musicaux est incompatible avec le respect de l'autonomie de l'auditeur. |
embeddings |
Représentations vectorielles pour similarité. Même raison : pas de recommandation algorithmique opaque. |
model_outputs |
Résultats de modèles ML (classification, prédiction). Veza ne délègue aucune décision éditoriale à un modèle. |
ml_predictions |
Prédictions comportementales (churn, engagement). Le profilage prédictif des utilisateurs est exclu par principe. |
recommendation_scores |
Scores de recommandation calculés. La découverte musicale repose sur la curation humaine et la recherche explicite. |
user_analytics |
Métriques comportementales par utilisateur. Le suivi individuel détaillé des habitudes d'écoute constitue du profilage. |
17.2 Tables Blockchain/NFT/Crypto supprimées
| Table supprimée | Raison |
|---|---|
nft_tokens |
Tokens NFT associés aux tracks/artworks. La spéculation sur l'art musical est contraire aux valeurs du projet. |
smart_contracts |
Contrats intelligents pour royalties/licences. Complexité et coût énergétique injustifiés ; les licences classiques suffisent. |
wallet_addresses |
Adresses de portefeuilles crypto. Aucun paiement crypto n'est supporté. |
blockchain_transactions |
Historique de transactions on-chain. Pas de dépendance à une blockchain. |
token_balances |
Soldes de tokens applicatifs. Pas de monnaie interne ni de token utilitaire. |
17.3 Tables de gamification addictive supprimées
| Table supprimée | Raison |
|---|---|
xp_events |
Points d'expérience par action. Le système XP transforme l'usage en jeu compulsif. |
level_thresholds |
Seuils de niveaux utilisateur. La hiérarchie par niveaux crée une pression sociale artificielle. |
achievement_unlocks |
Déblocage de succès/achievements. Les achievements exploitent le biais de complétion. |
streaks |
Séries consécutives d'utilisation. Les streaks exploitent l'aversion à la perte pour forcer l'usage quotidien. |
leaderboard_entries |
Classements compétitifs entre utilisateurs. Les leaderboards créent une compétition toxique sans rapport avec la musique. |
daily_challenges |
Défis quotidiens avec récompenses. Mécanisme de rétention qui conditionne l'usage à des récompenses variables. |
17.4 Éléments conservés (non-addictifs)
| Élément | Justification |
|---|---|
badges (table conservée) |
Badges déclaratifs de compétences (instrument, genre, expérience). Attestent d'un savoir-faire, pas d'un comportement. Pas de rareté, pas de collection compétitive. |
user_badges (table conservée) |
Association utilisateur-badge. Affichage optionnel, ordre configurable par l'utilisateur. |
track_analytics (table conservée) |
Métriques agrégées par track (play count, completion). Utile aux artistes pour comprendre leur audience sans profiler les individus. |
18. POLITIQUE DE RÉTENTION DES DONNÉES
18.1 Durées de rétention
| Catégorie | Durée maximale | Tables concernées | Action après expiration |
|---|---|---|---|
| Journaux d'activité | 90 jours | login_attempts, typing_indicators, user_presence |
Suppression définitive |
| Sessions expirées | 30 jours après expiration | refresh_tokens (révoqués/expirés), password_reset_tokens (utilisés/expirés), email_verification_tokens (utilisés/expirés) |
Suppression définitive |
| Analytiques détaillées | 12 mois | analytics_events, playback_history, daily_metrics |
Anonymisation puis agrégation |
| Données de compte | Jusqu'à demande de suppression | users, user_profiles, user_settings |
Soft delete puis anonymisation après 30 jours |
| Données transactionnelles | 7 ans (obligation légale) | orders, order_items, transactions |
Conservation légale, accès restreint |
| Messages | Jusqu'à suppression par l'utilisateur | messages, direct_messages |
Soft delete, purge 30 jours après |
| Journaux d'audit | 24 mois | audit_logs |
Archivage cold storage puis suppression |
18.2 Anonymisation
Après expiration de la période de rétention, les données sont anonymisées :
- Remplacement des identifiants personnels par des UUID aléatoires non-liés
- Suppression des adresses IP, user agents, et métadonnées de device
- Conservation des agrégats statistiques (compteurs, moyennes) sans lien avec un utilisateur
- Les données anonymisées ne sont pas réversibles
18.3 Implémentation
-- Job de nettoyage des sessions expirées (cron quotidien)
DELETE FROM refresh_tokens
WHERE (expires_at < NOW() - INTERVAL '30 days')
OR (is_revoked = true AND revoked_at < NOW() - INTERVAL '30 days');
DELETE FROM password_reset_tokens
WHERE expires_at < NOW() - INTERVAL '30 days';
DELETE FROM email_verification_tokens
WHERE expires_at < NOW() - INTERVAL '30 days';
-- Job de nettoyage des journaux d'activité (cron quotidien)
DELETE FROM login_attempts
WHERE attempted_at < NOW() - INTERVAL '90 days';
-- Job d'anonymisation des analytics (cron mensuel)
UPDATE playback_history
SET user_id = gen_random_uuid(),
device_type = NULL,
source = NULL
WHERE played_at < NOW() - INTERVAL '12 months';
19. CONFORMITÉ GDPR
19.1 Champs GDPR dans la table users
| Champ | Type | Usage GDPR |
|---|---|---|
deleted_at |
TIMESTAMPTZ |
Art. 17 - Droit à l'effacement (soft delete) |
export_requested_at |
TIMESTAMPTZ |
Art. 20 - Droit à la portabilité, horodatage de la demande |
exported_at |
TIMESTAMPTZ |
Art. 20 - Horodatage de l'export effectué |
data_processing_consent |
BOOLEAN |
Art. 6 - Base légale du traitement, consentement explicite |
data_processing_consent_at |
TIMESTAMPTZ |
Art. 7 - Preuve du consentement avec horodatage |
marketing_consent |
BOOLEAN |
Art. 6 - Consentement marketing séparé du traitement |
marketing_consent_at |
TIMESTAMPTZ |
Art. 7 - Preuve du consentement marketing |
19.2 Principes appliqués
- Séparation des consentements : le consentement au traitement des données et le consentement marketing sont deux champs distincts avec horodatages séparés. L'un n'implique pas l'autre.
- Soft delete systématique :
deleted_atsur toutes les tables user-facing. La suppression physique intervient après la période de rétention. - Export structuré : le champ
export_requested_atdéclenche un job asynchrone qui génère un export JSON/CSV de toutes les données de l'utilisateur.exported_atconfirme la complétion. - Audit du consentement : tout changement de consentement est tracé dans
audit_logsavec l'ancien et le nouveau état. - Minimisation des données : pas de
user_analyticsindividuel, pas de profilage comportemental, pas de feature vectors.
19.3 Procédure de suppression de compte
-- Étape 1 : Soft delete immédiat
UPDATE users SET deleted_at = NOW() WHERE id = $user_id;
-- Étape 2 : Anonymisation après 30 jours (job automatique)
UPDATE users SET
email = 'deleted_' || id || '@anonymized.local',
username = 'deleted_' || SUBSTRING(id::text, 1, 8),
password_hash = NULL,
first_name = NULL,
last_name = NULL,
display_name = '[Compte supprimé]',
last_login_ip = NULL,
data_processing_consent = false,
marketing_consent = false
WHERE deleted_at < NOW() - INTERVAL '30 days'
AND email NOT LIKE 'deleted_%@anonymized.local';
-- Étape 3 : Suppression des données associées
DELETE FROM user_profiles WHERE user_id IN (
SELECT id FROM users
WHERE deleted_at < NOW() - INTERVAL '30 days'
);
20. INDEXES STRATÉGIE
20.1 Index Types
| Type | Usage | Example |
|---|---|---|
| B-tree | Default, equality & range queries | CREATE INDEX idx_users_created_at ON users(created_at) |
| GIN | Full-text search, JSONB, arrays | CREATE INDEX idx_tracks_search_gin ON tracks USING GIN(to_tsvector('english', title)) |
| GIST | Geometric data, full-text (slower than GIN) | Less common in Veza |
| Hash | Equality only (rarely used in PostgreSQL) | Not recommended |
| Partial | Index subset of rows (WHERE clause) | CREATE INDEX idx_users_active ON users(email) WHERE is_active = true |
20.2 Critical Indexes
Performance Critical (query time < 10ms):
-- User lookups
CREATE INDEX idx_users_email_btree ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_username_btree ON users(username) WHERE deleted_at IS NULL;
-- Track queries
CREATE INDEX idx_tracks_creator_id ON tracks(creator_id);
CREATE INDEX idx_tracks_genre ON tracks(genre);
CREATE INDEX idx_tracks_published_at_desc ON tracks(published_at DESC) WHERE published_at IS NOT NULL;
-- Message queries
CREATE INDEX idx_messages_room_id_created_at ON messages(room_id, created_at DESC);
-- Social feed
CREATE INDEX idx_posts_created_at_desc ON posts(created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_follows_following_id ON follows(following_id);
-- Marketplace
CREATE INDEX idx_products_category_status ON products(category, status);
CREATE INDEX idx_orders_user_id_created_at ON orders(user_id, created_at DESC);
20.3 Index Maintenance
-- Regular VACUUM and ANALYZE (automated with autovacuum)
-- Manual when needed:
VACUUM ANALYZE users;
VACUUM ANALYZE tracks;
VACUUM ANALYZE messages;
-- Reindex if needed (rare, usually after corruption)
REINDEX INDEX CONCURRENTLY idx_users_email_btree;
-- Monitor index usage
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0 -- Unused indexes
ORDER BY schemaname, tablename;
21. PARTITIONING STRATÉGIE
21.1 Tables Candidates au Partitioning
High-Volume Tables (>10M rows expected):
messages- Partition by month (created_at)analytics_events- Partition by day (event_date)audit_logs- Partition by month (created_at)playback_history- Partition by month (played_at)login_attempts- Partition by month (attempted_at)
21.2 Example: messages Partitioning
-- Create partitioned table
CREATE TABLE messages (
id UUID DEFAULT gen_random_uuid(),
room_id UUID NOT NULL,
sender_id UUID NOT NULL,
content TEXT NOT NULL,
message_type message_type NOT NULL DEFAULT 'text',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- ... other columns
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- Create partitions (automated with pg_partman recommended)
CREATE TABLE messages_2025_01 PARTITION OF messages
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE messages_2025_02 PARTITION OF messages
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- Indexes on each partition
CREATE INDEX idx_messages_2025_01_room_id ON messages_2025_01(room_id, created_at DESC);
CREATE INDEX idx_messages_2025_02_room_id ON messages_2025_02(room_id, created_at DESC);
-- Automated partition management with pg_partman
CREATE EXTENSION pg_partman;
SELECT partman.create_parent(
p_parent_table := 'public.messages',
p_control := 'created_at',
p_type := 'native',
p_interval := '1 month',
p_premake := 3 -- Pre-create 3 future partitions
);
21.3 Partition Maintenance
-- Drop old partitions (retention policy)
DROP TABLE IF EXISTS messages_2023_01; -- After 24 months
-- Detach instead of drop (for archiving)
ALTER TABLE messages DETACH PARTITION messages_2023_01;
-- Archive to cold storage (optional)
-- pg_dump messages_2023_01 > archive/messages_2023_01.sql
22. TRIGGERS & FUNCTIONS
22.1 Update Timestamps
-- Trigger function for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply to all tables with updated_at
CREATE TRIGGER trg_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_tracks_updated_at BEFORE UPDATE ON tracks
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ... (repeat for all tables with updated_at)
22.2 Denormalized Counters
-- Increment follower_count when follow created
CREATE OR REPLACE FUNCTION increment_follower_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE user_profiles
SET follower_count = follower_count + 1
WHERE user_id = NEW.following_id;
UPDATE user_profiles
SET following_count = following_count + 1
WHERE user_id = NEW.follower_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_follows_insert AFTER INSERT ON follows
FOR EACH ROW EXECUTE FUNCTION increment_follower_count();
-- Decrement when unfollow
CREATE OR REPLACE FUNCTION decrement_follower_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE user_profiles
SET follower_count = follower_count - 1
WHERE user_id = OLD.following_id;
UPDATE user_profiles
SET following_count = following_count - 1
WHERE user_id = OLD.follower_id;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_follows_delete AFTER DELETE ON follows
FOR EACH ROW EXECUTE FUNCTION decrement_follower_count();
22.3 Audit Trail
-- Generic audit trigger
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (
table_name,
operation,
record_id,
old_data,
new_data,
user_id,
created_at
) VALUES (
TG_TABLE_NAME,
TG_OP,
COALESCE(NEW.id, OLD.id),
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END,
COALESCE(NEW.user_id, OLD.user_id),
NOW()
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- Apply to sensitive tables
CREATE TRIGGER trg_users_audit AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
CREATE TRIGGER trg_orders_audit AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
23. MATERIALIZED VIEWS
23.1 Trending Tracks
CREATE MATERIALIZED VIEW trending_tracks AS
SELECT
t.id,
t.title,
t.artist,
t.creator_id,
t.cover_art_file_id,
COUNT(DISTINCT ph.user_id) AS unique_listeners_7d,
COUNT(*) AS play_count_7d,
AVG(ph.completion_percentage) AS avg_completion,
t.like_count,
(
COUNT(DISTINCT ph.user_id) * 0.4 +
COUNT(*) * 0.3 +
AVG(ph.completion_percentage) * 0.2 +
t.like_count * 0.1
) AS trending_score
FROM tracks t
LEFT JOIN playback_history ph ON ph.track_id = t.id
AND ph.played_at > NOW() - INTERVAL '7 days'
WHERE t.deleted_at IS NULL
AND t.visibility = 'public'
GROUP BY t.id
ORDER BY trending_score DESC
LIMIT 100;
-- Indexes
CREATE INDEX idx_trending_tracks_trending_score ON trending_tracks(trending_score DESC);
-- Refresh schedule (cron or pg_cron)
-- Refresh every 1 hour
REFRESH MATERIALIZED VIEW CONCURRENTLY trending_tracks;
23.2 User Statistics
CREATE MATERIALIZED VIEW user_statistics AS
SELECT
u.id AS user_id,
u.username,
COUNT(DISTINCT t.id) AS track_count,
COUNT(DISTINCT p.id) AS playlist_count,
COUNT(DISTINCT f1.id) AS follower_count,
COUNT(DISTINCT f2.id) AS following_count,
SUM(t.play_count) AS total_plays,
SUM(t.like_count) AS total_likes,
MAX(t.created_at) AS last_track_uploaded
FROM users u
LEFT JOIN tracks t ON t.creator_id = u.id AND t.deleted_at IS NULL
LEFT JOIN playlists p ON p.user_id = u.id AND p.deleted_at IS NULL
LEFT JOIN follows f1 ON f1.following_id = u.id
LEFT JOIN follows f2 ON f2.follower_id = u.id
WHERE u.deleted_at IS NULL
GROUP BY u.id, u.username;
-- Refresh daily
REFRESH MATERIALIZED VIEW CONCURRENTLY user_statistics;
24. MIGRATION STRATÉGIE
24.1 Migration Tools
Backend (Go): GORM Auto-Migrate + SQL files
Rust Services: SQLx migrations
Versioning: Sequential numbered migrations
24.2 Migration Workflow
# GORM (Go backend)
# migrations/001_create_users.sql
# migrations/002_create_tracks.sql
# Apply with: go run migrate.go up
# SQLx (Rust services)
# migrations/0001_create_rooms.sql
# migrations/0002_create_messages.sql
# Apply with: sqlx migrate run
24.3 Example Migration (SQLx)
-- migrations/0001_create_users.sql
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(30) NOT NULL UNIQUE,
password_hash VARCHAR(255),
role user_role NOT NULL DEFAULT 'user',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email_btree ON users(email);
CREATE INDEX idx_users_username_btree ON users(username);
-- migrations/0002_add_token_version.sql
ALTER TABLE users ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0;
24.4 Rollback Strategy
-- Down migrations (SQLx supports)
-- migrations/0002_add_token_version.down.sql
ALTER TABLE users DROP COLUMN IF EXISTS token_version;
-- Execute rollback
-- sqlx migrate revert
24.5 Zero-Downtime Migrations
Principles:
- Additive changes first (add columns, tables)
- Deploy code that works with both old & new schema
- Backfill data if needed (background job)
- Remove old schema in next migration
Example (rename column):
-- Step 1: Add new column
ALTER TABLE users ADD COLUMN display_name VARCHAR(100);
-- Step 2: Backfill (background job)
UPDATE users SET display_name = first_name || ' ' || last_name WHERE display_name IS NULL;
-- Step 3: Deploy code using display_name
-- Step 4: (Next release) Drop old columns
ALTER TABLE users DROP COLUMN IF EXISTS first_name;
ALTER TABLE users DROP COLUMN IF EXISTS last_name;
CHECKLIST DE VALIDATION
Schema Completeness
- ~85 tables couvrant tous les modules retenus
- Toutes les tables ont
id,created_at,updated_at - Soft delete (
deleted_at) sur tables user-facing - Foreign keys avec ON DELETE CASCADE/RESTRICT explicites
- Indexes sur toutes les foreign keys
- Constraints pour intégrité données (CHECK, UNIQUE, NOT NULL)
Performance
- Indexes B-tree sur colonnes de recherche fréquentes
- Indexes GIN pour full-text search
- Partial indexes pour filtres WHERE fréquents
- Partitioning sur tables high-volume (>10M rows)
- Materialized views pour requêtes complexes fréquentes
Security & Compliance
- Audit logs pour actions sensibles
- GDPR compliance (soft delete, export, consentement séparé, rétention)
- Encryption at rest (pgcrypto pour colonnes sensibles)
- Row-level security policies (RLS) considérées
Contraintes éthiques
- Aucune table ML/IA (feature vectors, embeddings, predictions)
- Aucune table blockchain/NFT/crypto
- Aucune gamification addictive (XP, streaks, leaderboards, achievements)
- Badges limités aux compétences déclaratives (instrument, genre, expérience)
- Politique de rétention des données documentée et implémentable
- Consentement GDPR séparé (traitement vs marketing)
- Exclusions documentées avec raisons dans section 17
Maintenance
- Triggers pour updated_at automatiques
- Triggers pour denormalized counters
- Migration strategy documentée
- Rollback procedures définies
- Backup strategy planifiée
- Jobs de nettoyage/rétention documentés
MÉTRIQUES DE SUCCÈS
Performance Targets
- Query time p95: < 10ms (indexed queries)
- Query time p99: < 50ms
- Connection pool: 100 connections active, 1000 max
- Index hit ratio: > 99%
- Cache hit ratio: > 95%
Scalability Targets
- Database size: 1 TB+ supported
- Concurrent connections: 1,000+
- Queries per second: 10,000+ (read-heavy)
- Writes per second: 1,000+
Reliability Targets
- Uptime: 99.95%
- Backup frequency: Every 6 hours
- Backup retention: 30 days (daily), 12 months (monthly)
- RTO (Recovery Time Objective): < 1 hour
- RPO (Recovery Point Objective): < 15 minutes
HISTORIQUE DES VERSIONS
| Version | Date | Changements |
|---|---|---|
| 1.0.0 | 2025-11-02 | Version initiale - Schéma complet 105 tables |
| 2.0.0 | 2026-03-04 | Révision éthique : suppression tables ML/IA, blockchain/NFT/crypto, gamification addictive. Ajout champs GDPR (consentement séparé, export). Ajout politique de rétention des données. Badges restreints aux compétences déclaratives. Documentation des exclusions avec raisons. ~85 tables. |
AVERTISSEMENT
CE SCHÉMA EST CONTRÔLÉ
Le schéma de base de données défini ici est verrouillé. Toute modification nécessite :
- RFC Database Change avec impact analysis complet
- Migration plan détaillé (up + down)
- Performance testing (query plans, index impact)
- Vérification éthique : la modification ne doit pas réintroduire de tables ML/IA, blockchain, ou gamification addictive
- Backup complet avant exécution
- Rollback plan testé
Modifications autorisées sans RFC :
- Ajout index non-unique
- Ajout colonne nullable (sans default calculé)
- Modification comments/documentation
Modifications interdites :
- Ajout de tables ML/IA, blockchain/NFT/crypto, ou gamification addictive (voir section 17)
- Suppression table
- Suppression colonne (utiliser deprecated d'abord)
- Changement type colonne (incompatible)
- Suppression foreign key (intégrité référentielle)
- Changement partitioning strategy (migration massive)
- Réduction des durées de rétention sans justification légale
Document créé par : Database Team + Architecture Date de création : 2025-11-02 Dernière révision : 2026-03-04 (v2.0.0 - révision éthique) Prochaine révision : Q3 2026