fractafrag/db/init.sql
John Lightner 1047a1f5fe Versioning, drafts, resizable editor, My Shaders, 200 seed shaders
Architecture — Shader versioning & draft system:
- New shader_versions table: immutable snapshots of every edit
- Shaders now have status: draft, published, archived
- current_version counter tracks version number
- Every create/update creates a ShaderVersion record
- Restore-from-version endpoint creates new version (never destructive)
- Drafts are private, only visible to author
- Forks start as drafts
- Free tier rate limit applies only to published shaders (drafts unlimited)

Architecture — Platform identity:
- System account 'fractafrag' (UUID 00000000-...-000001) created in init.sql
- is_system flag on users and shaders
- system_label field: 'fractafrag-curated', future: 'fractafrag-generated'
- Feed/explore can filter by is_system
- System shaders display distinctly from user/AI content

API changes:
- GET /shaders/mine — user workspace (drafts, published, archived)
- GET /shaders/{id}/versions — version history
- GET /shaders/{id}/versions/{n} — specific version
- POST /shaders/{id}/versions/{n}/restore — restore old version
- POST /shaders accepts status: 'draft' | 'published'
- PUT /shaders/{id} accepts change_note for version descriptions
- PUT status transitions: draft→published, published→archived, archived→published

Frontend — Editor improvements:
- Resizable split pane with drag handle (20-80% range, smooth col-resize cursor)
- Save Draft button (creates/updates as draft, no publish)
- Publish button (validates, publishes, redirects to shader page)
- Version badge shows current version number when editing existing
- Owner detection: editing own shader vs forking someone else's
- Saved status indicator ('Draft saved', 'Published')

Frontend — My Shaders workspace:
- /my-shaders route with status tabs (All, Draft, Published, Archived)
- Count badges per tab
- Status badges on shader cards (draft=yellow, published=green, archived=grey)
- Version badges (v1, v2, etc.)
- Quick actions: Edit, Publish, Archive, Restore, Delete per status
- Drafts link to editor, published link to detail page

Seed data — 200 fractafrag-curated shaders:
- 171 2D + 29 3D shaders
- 500 unique tags across all shaders
- All 200 titles are unique
- Covers: fractals (Mandelbrot, Julia sets), noise (fbm, Voronoi, Perlin),
  raymarching (metaballs, terrain, torus knots, metall/glass),
  effects (glitch, VHS, plasma, aurora, lightning, fireworks),
  patterns (circuit, hex grid, stained glass, herringbone, moiré),
  physics (wave interference, pendulum, caustics, gravity lens),
  minimal (single shapes, gradients, dot grids),
  nature (ink, watercolor, smoke, sand garden, coral, nebula),
  color theory (RGB separation, CMY overlap, hue wheel),
  domain warping (acid trip, lava rift, storm eye),
  particles (fireflies, snow, ember, bubbles)
- Each shader has style_metadata (chaos_level, color_temperature, motion_type)
- Distributed creation times over 30 days for feed ranking variety
- Random initial scores for algorithm testing
- All authored by 'fractafrag' system account, is_system=true
- system_label='fractafrag-curated' for clear provenance

Schema:
- shader_versions table with (shader_id, version_number) unique constraint
- HNSW indexes on version lookup
- System account indexes
- Status-aware feed indexes
2026-03-24 22:00:10 -05:00

300 lines
16 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
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- platform system account (fractafrag)
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,
status TEXT NOT NULL DEFAULT 'published', -- draft, published, archived
is_public BOOLEAN DEFAULT TRUE,
is_ai_generated BOOLEAN DEFAULT FALSE,
is_system BOOLEAN DEFAULT FALSE, -- generated by fractafrag platform
ai_provider TEXT, -- anthropic, openai, ollama, null
system_label TEXT, -- e.g. 'fractafrag-curated', 'fractafrag-generated'
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,
current_version INTEGER NOT NULL DEFAULT 1, -- current version number
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',
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()
);
-- ════════════════════════════════════════════════════════════
-- SHADER VERSIONS — immutable snapshots of each edit
-- ════════════════════════════════════════════════════════════
CREATE TABLE shader_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shader_id UUID NOT NULL REFERENCES shaders(id) ON DELETE CASCADE,
version_number INTEGER NOT NULL,
glsl_code TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
tags TEXT[],
style_metadata JSONB,
change_note TEXT, -- optional: "fixed the color bleeding", "added mouse interaction"
thumbnail_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (shader_id, version_number)
);
-- ════════════════════════════════════════════════════════════
-- 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,
session_id TEXT,
shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
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),
style_hints JSONB,
tip_amount_cents INTEGER DEFAULT 0,
status TEXT DEFAULT 'open',
heat_score FLOAT DEFAULT 1,
fulfilled_by_shader UUID REFERENCES shaders(id) ON DELETE SET NULL,
fulfilled_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
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',
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,
net_amount_cents INTEGER,
stripe_transfer_id TEXT,
status TEXT DEFAULT 'pending',
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,
key_prefix TEXT NOT NULL,
name TEXT,
trust_tier TEXT DEFAULT 'probation',
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,
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,
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 AND status = 'published';
CREATE INDEX idx_shaders_created ON shaders(created_at DESC) WHERE is_public = TRUE AND status = 'published';
CREATE INDEX idx_shaders_tags ON shaders USING GIN(tags);
CREATE INDEX idx_shaders_render_status ON shaders(render_status) WHERE render_status != 'ready';
CREATE INDEX idx_shaders_status ON shaders(status);
CREATE INDEX idx_shaders_author_status ON shaders(author_id, status, updated_at DESC);
CREATE INDEX idx_shaders_system ON shaders(is_system) WHERE is_system = TRUE;
-- Versioning
CREATE INDEX idx_shader_versions_shader ON shader_versions(shader_id, version_number DESC);
-- Recommendation (pgvector HNSW — works on empty tables)
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);
-- ════════════════════════════════════════════════════════════
-- SYSTEM ACCOUNT: The "fractafrag" platform user
-- All system-generated/curated shaders are authored by this account
-- ════════════════════════════════════════════════════════════
INSERT INTO users (id, username, email, password_hash, role, trust_tier, is_system, subscription_tier, is_verified_creator)
VALUES (
'00000000-0000-0000-0000-000000000001',
'fractafrag',
'system@fractafrag.local',
'$2b$12$000000000000000000000000000000000000000000000000000000', -- not a valid login
'admin',
'trusted_api',
TRUE,
'studio',
TRUE
);