fractafrag/db/init.sql
John Lightner 05d39fdda8 M0: Foundation scaffold — Docker Compose, DB schema, FastAPI app, all service stubs
Track A (Infrastructure & Data Layer):
- docker-compose.yml with all 7 services (nginx, frontend, api, mcp, renderer, worker, postgres, redis)
- docker-compose.override.yml for local dev (hot reload, port exposure)
- PostgreSQL init.sql with full schema (15 tables, pgvector indexes, creator economy stubs)
- .env.example with all required environment variables

Track A+B (API Layer):
- FastAPI app with 10 routers (auth, shaders, feed, votes, generate, desires, users, payments, mcp_keys, health)
- SQLAlchemy ORM models for all 15 tables
- Pydantic schemas for all request/response types
- JWT auth middleware (access + refresh tokens, Redis blocklist)
- Redis rate limiting middleware
- Celery worker config with job stubs (render, embed, generate, feed cache, expire bounties)
- Alembic migration framework

Service stubs:
- MCP server (health endpoint, 501 for all tools)
- Renderer service (Express + Puppeteer scaffold, 501 for /render)
- Frontend (package.json with React/Vite/Three.js/TanStack/Tailwind deps)
- Nginx reverse proxy config (/, /api, /mcp, /renders)

Project:
- DECISIONS.md with 11 recorded architectural decisions
- README.md with architecture overview
- Sample shader seed data (plasma, fractal noise, raymarched sphere)
2026-03-24 20:45:08 -05:00

265 lines
15 KiB
SQL

-- Fractafrag Database Bootstrap
-- Runs on first container start via docker-entrypoint-initdb.d
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "vector";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- for text search
-- ════════════════════════════════════════════════════════════
-- USERS
-- ════════════════════════════════════════════════════════════
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user', -- user, moderator, admin
trust_tier TEXT NOT NULL DEFAULT 'standard', -- standard, creator, trusted_api
stripe_customer_id TEXT,
subscription_tier TEXT DEFAULT 'free', -- free, pro, studio
ai_credits_remaining INTEGER DEFAULT 0,
taste_vector vector(512), -- pgvector: learned taste embedding
-- Creator economy stubs (Section 11f — deferred, schema only)
is_verified_creator BOOLEAN DEFAULT FALSE,
verified_creator_at TIMESTAMPTZ,
stripe_connect_account_id TEXT,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
last_active_at TIMESTAMPTZ
);
-- ════════════════════════════════════════════════════════════
-- SHADERS
-- ════════════════════════════════════════════════════════════
CREATE TABLE shaders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID REFERENCES users(id) ON DELETE SET NULL,
title TEXT NOT NULL,
description TEXT,
glsl_code TEXT NOT NULL,
is_public BOOLEAN DEFAULT TRUE,
is_ai_generated BOOLEAN DEFAULT FALSE,
ai_provider TEXT, -- anthropic, openai, ollama, null
thumbnail_url TEXT,
preview_url TEXT,
render_status TEXT DEFAULT 'pending', -- pending, rendering, ready, failed
style_vector vector(512), -- pgvector: visual style embedding
style_metadata JSONB, -- { chaos_level, color_temp, motion_type, ... }
tags TEXT[],
shader_type TEXT DEFAULT '2d', -- 2d, 3d, audio-reactive
forked_from UUID REFERENCES shaders(id) ON DELETE SET NULL,
view_count INTEGER DEFAULT 0,
score FLOAT DEFAULT 0, -- cached hot score for feed ranking
-- Creator economy stubs (Section 11f)
access_tier TEXT DEFAULT 'open', -- open, source_locked, commercial
source_unlock_price_cents INTEGER,
commercial_license_price_cents INTEGER,
verified_creator_shader BOOLEAN DEFAULT FALSE,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ════════════════════════════════════════════════════════════
-- VOTES
-- ════════════════════════════════════════════════════════════
CREATE TABLE votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE,
value SMALLINT NOT NULL CHECK (value IN (-1, 1)),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (user_id, shader_id)
);
-- ════════════════════════════════════════════════════════════
-- ENGAGEMENT EVENTS (dwell time, replays, shares)
-- ════════════════════════════════════════════════════════════
CREATE TABLE engagement_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL, -- null for anonymous
session_id TEXT, -- anonymous session token
shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE,
event_type TEXT NOT NULL, -- dwell, replay, share, generate_similar
dwell_secs FLOAT,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ════════════════════════════════════════════════════════════
-- DESIRES / BOUNTIES
-- ════════════════════════════════════════════════════════════
CREATE TABLE desires (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID REFERENCES users(id) ON DELETE SET NULL,
prompt_text TEXT NOT NULL,
prompt_embedding vector(512), -- embedded for similarity grouping
style_hints JSONB, -- { chaos_level, color_temp, etc }
tip_amount_cents INTEGER DEFAULT 0,
status TEXT DEFAULT 'open', -- open, in_progress, fulfilled, expired
heat_score FLOAT DEFAULT 1, -- updated as similar desires accumulate
fulfilled_by_shader UUID REFERENCES shaders(id) ON DELETE SET NULL,
fulfilled_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Similar desire grouping (many-to-many)
CREATE TABLE desire_clusters (
cluster_id UUID,
desire_id UUID REFERENCES desires(id) ON DELETE CASCADE,
similarity FLOAT,
PRIMARY KEY (cluster_id, desire_id)
);
-- ════════════════════════════════════════════════════════════
-- BOUNTY TIPS
-- ════════════════════════════════════════════════════════════
CREATE TABLE bounty_tips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
desire_id UUID REFERENCES desires(id) ON DELETE CASCADE,
tipper_id UUID REFERENCES users(id) ON DELETE SET NULL,
amount_cents INTEGER NOT NULL,
stripe_payment_intent_id TEXT,
status TEXT DEFAULT 'held', -- held, released, refunded
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ════════════════════════════════════════════════════════════
-- CREATOR PAYOUTS
-- ════════════════════════════════════════════════════════════
CREATE TABLE creator_payouts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID REFERENCES users(id) ON DELETE SET NULL,
desire_id UUID REFERENCES desires(id) ON DELETE SET NULL,
gross_amount_cents INTEGER,
platform_fee_cents INTEGER, -- 10%
net_amount_cents INTEGER, -- 90%
stripe_transfer_id TEXT,
status TEXT DEFAULT 'pending', -- pending, processing, completed, failed
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ════════════════════════════════════════════════════════════
-- API KEYS (for MCP clients)
-- ════════════════════════════════════════════════════════════
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
key_hash TEXT UNIQUE NOT NULL, -- bcrypt hash of the actual key
key_prefix TEXT NOT NULL, -- first 8 chars for display (ff_key_XXXXXXXX)
name TEXT, -- user-given label
trust_tier TEXT DEFAULT 'probation', -- probation, trusted, premium
submissions_approved INTEGER DEFAULT 0,
rate_limit_per_hour INTEGER DEFAULT 10,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
revoked_at TIMESTAMPTZ
);
-- ════════════════════════════════════════════════════════════
-- AI GENERATION LOG
-- ════════════════════════════════════════════════════════════
CREATE TABLE generation_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
shader_id UUID REFERENCES shaders(id) ON DELETE SET NULL,
provider TEXT NOT NULL,
prompt_text TEXT,
tokens_used INTEGER,
cost_cents INTEGER, -- platform cost for credit-based generations
success BOOLEAN,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ════════════════════════════════════════════════════════════
-- COMMENTS (schema in place, feature deferred to post-M5)
-- ════════════════════════════════════════════════════════════
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE,
author_id UUID REFERENCES users(id) ON DELETE SET NULL,
body TEXT NOT NULL,
parent_id UUID REFERENCES comments(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ════════════════════════════════════════════════════════════
-- CREATOR ECONOMY STUBS (Section 11f — dormant until activated)
-- ════════════════════════════════════════════════════════════
CREATE TABLE source_unlocks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE,
buyer_id UUID REFERENCES users(id) ON DELETE SET NULL,
license_type TEXT NOT NULL, -- personal, commercial
amount_cents INTEGER NOT NULL,
platform_fee_cents INTEGER NOT NULL,
stripe_payment_intent_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE creator_engagement_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID REFERENCES users(id) ON DELETE CASCADE,
month DATE NOT NULL,
total_score FLOAT NOT NULL,
pool_share FLOAT,
payout_cents INTEGER,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ════════════════════════════════════════════════════════════
-- INDEXES
-- ════════════════════════════════════════════════════════════
-- Feed performance
CREATE INDEX idx_shaders_score ON shaders(score DESC) WHERE is_public = TRUE;
CREATE INDEX idx_shaders_created ON shaders(created_at DESC) WHERE is_public = TRUE;
CREATE INDEX idx_shaders_tags ON shaders USING GIN(tags);
CREATE INDEX idx_shaders_render_status ON shaders(render_status) WHERE render_status != 'ready';
-- Recommendation (pgvector ANN — ivfflat, will rebuild after data exists)
-- NOTE: ivfflat indexes require data in the table to build properly.
-- Run these AFTER seeding initial data:
-- CREATE INDEX idx_shaders_style_vector ON shaders
-- USING ivfflat (style_vector vector_cosine_ops) WITH (lists = 100);
-- CREATE INDEX idx_users_taste_vector ON users
-- USING ivfflat (taste_vector vector_cosine_ops) WITH (lists = 50);
-- CREATE INDEX idx_desires_embedding ON desires
-- USING ivfflat (prompt_embedding vector_cosine_ops) WITH (lists = 50);
-- For now, use HNSW (works on empty tables, better perf at small scale)
CREATE INDEX idx_shaders_style_vector ON shaders
USING hnsw (style_vector vector_cosine_ops) WITH (m = 16, ef_construction = 64);
CREATE INDEX idx_users_taste_vector ON users
USING hnsw (taste_vector vector_cosine_ops) WITH (m = 16, ef_construction = 64);
CREATE INDEX idx_desires_embedding ON desires
USING hnsw (prompt_embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
-- Engagement
CREATE INDEX idx_engagement_user ON engagement_events(user_id, created_at DESC);
CREATE INDEX idx_engagement_shader ON engagement_events(shader_id, event_type);
CREATE INDEX idx_engagement_session ON engagement_events(session_id, created_at DESC)
WHERE session_id IS NOT NULL;
-- Desires / bounties
CREATE INDEX idx_desires_status ON desires(status, heat_score DESC);
CREATE INDEX idx_desires_author ON desires(author_id);
-- API keys
CREATE INDEX idx_api_keys_user ON api_keys(user_id) WHERE revoked_at IS NULL;
CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix);
-- Votes
CREATE INDEX idx_votes_shader ON votes(shader_id);
CREATE INDEX idx_votes_user ON votes(user_id);
-- Comments
CREATE INDEX idx_comments_shader ON comments(shader_id, created_at);
CREATE INDEX idx_comments_parent ON comments(parent_id);
-- Text search
CREATE INDEX idx_shaders_title_trgm ON shaders USING GIN(title gin_trgm_ops);
CREATE INDEX idx_desires_prompt_trgm ON desires USING GIN(prompt_text gin_trgm_ops);