Table of Contents
- Architecture
Architecture
| Meta | Value |
|---|---|
| Repo | xpltdco/fractafrag |
| Page | Architecture |
| Audience | developers, agents, newcomers |
| Last Updated | 2026-04-04 |
| Status | current |
System Overview
Fractafrag is a multi-service application orchestrated by Docker Compose. Eight containers work together: nginx routes traffic, the FastAPI backend handles business logic, a React SPA provides the UI, Celery workers process async jobs, a headless Chromium renderer captures shader thumbnails, an MCP server exposes tools for AI agents, and PostgreSQL + Redis provide persistence and caching.
graph TB
Internet["Internet / Browser"]
Nginx["nginx :80/443"]
Frontend["Frontend :5173<br/>(React + Vite)"]
API["API :8000<br/>(FastAPI)"]
MCP["MCP Server :3200<br/>(FastMCP)"]
Worker["Celery Worker"]
Renderer["Renderer :3100<br/>(Puppeteer + Chromium)"]
Postgres["PostgreSQL :5432<br/>(pgvector)"]
Redis["Redis :6379"]
Renders["Render Output<br/>(/renders volume)"]
Internet -->|"HTTP/WS"| Nginx
Nginx -->|"/"| Frontend
Nginx -->|"/api/*"| API
Nginx -->|"/mcp/*"| MCP
Nginx -->|"/renders/*"| Renders
API --> Postgres
API --> Redis
Worker --> Postgres
Worker --> Redis
Worker -->|"POST /render"| Renderer
Renderer --> Renders
MCP -->|"internal API"| API
Service Topology
| Service | Image/Base | Port | Purpose |
|---|---|---|---|
| nginx | nginx:alpine | 80 | Reverse proxy, static render serving |
| frontend | node:20-alpine | 5173 | React SPA (Vite dev/static prod) |
| api | python:3.12-slim | 8000 | FastAPI REST API |
| mcp | python:3.12-slim | 3200 | AI agent MCP interface (HTTP+SSE) |
| renderer | node:20-slim + Chromium | 3100 | Headless shader rendering |
| worker | python:3.12-slim (reuses api image) | — | Celery async task processing |
| postgres | pgvector/pgvector:pg16 | 5432 | Primary database with vector search |
| redis | redis:7-alpine | 6379 | Cache, job queue, token blocklist |
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| HTTP Proxy | nginx | Routing, TLS termination, static files |
| Frontend | React 18, Vite, Three.js | SPA with WebGL shader preview |
| Styling | Tailwind CSS | Utility-first CSS framework |
| Client State | Zustand | Lightweight state management |
| Server State | TanStack Query 5 | API caching and background refetch |
| Backend | FastAPI + Uvicorn | Async ASGI web framework |
| ORM | SQLAlchemy 2 (async) | Type-safe database access |
| Database | PostgreSQL 16 + pgvector | Relational + vector similarity search |
| Migrations | Alembic | Database schema versioning |
| Task Queue | Celery + Redis | Distributed async job processing |
| Auth | JWT (python-jose) + bcrypt | Token-based authentication |
| Payments | Stripe SDK | Subscriptions and payouts (planned) |
| AI Interface | FastMCP | MCP server for external AI agents |
| Rendering | Puppeteer Core + Chromium | Headless shader screenshot capture |
| Embeddings | scikit-learn (TF-IDF + SVD) | Text-to-vector for desire clustering |
| Vector Search | pgvector (HNSW) | Cosine similarity for recommendations |
Directory Structure
fractafrag/
├── db/
│ └── init.sql # PostgreSQL bootstrap (schema + extensions + indexes)
├── scripts/
│ └── seed.py # Sample data seeding (WIP)
├── services/
│ ├── api/ # FastAPI backend
│ │ ├── Dockerfile
│ │ ├── pyproject.toml # Python dependencies
│ │ └── app/
│ │ ├── main.py # FastAPI app setup, lifespan, CORS
│ │ ├── config.py # Pydantic Settings (env vars)
│ │ ├── database.py # Async SQLAlchemy engine + sessions
│ │ ├── redis.py # Async Redis client singleton
│ │ ├── models/
│ │ │ └── models.py # All SQLAlchemy ORM models
│ │ ├── schemas/
│ │ │ └── schemas.py # Pydantic request/response schemas
│ │ ├── middleware/
│ │ │ ├── auth.py # JWT auth, password hashing, dependencies
│ │ │ └── rate_limit.py # Redis-based rate limiting
│ │ ├── routers/
│ │ │ ├── auth.py # Register, login, refresh, logout
│ │ │ ├── shaders.py # CRUD, versioning, fork, search
│ │ │ ├── feed.py # Personalized feed, trending, similar
│ │ │ ├── votes.py # Upvote/downvote, hot score
│ │ │ ├── desires.py # Bounty board
│ │ │ ├── generate.py # AI generation (stub)
│ │ │ ├── users.py # Profile, BYOK keys
│ │ │ ├── payments.py # Stripe integration (stub)
│ │ │ ├── mcp_keys.py # API key management
│ │ │ └── health.py # Liveness check
│ │ ├── services/
│ │ │ ├── embedding.py # TF-IDF + SVD vectorizer (512-dim)
│ │ │ ├── clustering.py # Desire clustering via pgvector
│ │ │ ├── glsl_validator.py # Static GLSL syntax validation
│ │ │ ├── renderer_client.py # HTTP client to renderer service
│ │ │ └── byok.py # BYOK key encryption
│ │ └── worker/
│ │ └── __init__.py # Celery app + task definitions
│ ├── frontend/ # React SPA
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── vite.config.ts
│ │ ├── tailwind.config.js
│ │ └── src/
│ │ ├── main.tsx # React entry point
│ │ ├── stores/ # Zustand state (auth)
│ │ ├── pages/ # Route pages (Feed, Editor, Explore, etc.)
│ │ ├── components/ # ShaderCanvas, Navbar, Layout
│ │ └── ...
│ ├── mcp/ # MCP server
│ │ ├── Dockerfile
│ │ ├── requirements.txt
│ │ └── server.py # FastMCP tools + resources
│ ├── renderer/ # Headless rendering
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ └── server.js # Express + Puppeteer rendering pipeline
│ └── nginx/
│ └── conf/
│ └── default.conf # Proxy routing rules
├── docker-compose.yml # Production compose
├── docker-compose.override.yml # Dev overrides (volume mounts, hot reload)
├── docker-compose.dev.yml # Data stores only (for local dev outside Docker)
├── Makefile # Developer convenience commands
├── .env.example # Environment template
└── .forgejo/workflows/ci.yml # CI pipeline
Key Design Decisions
Microservices in a Monorepo
All services live in one repo under services/. Docker Compose orchestrates them. This gives microservice isolation (separate runtimes, independent scaling) with monorepo convenience (single clone, shared CI, atomic changes).
pgvector for Similarity Search
Instead of a dedicated vector database (Pinecone, Weaviate), Fractafrag uses pgvector as a PostgreSQL extension. This keeps everything in one database, simplifies backups, and avoids an additional service. HNSW indexes provide fast approximate nearest-neighbor search for taste vectors, style vectors, and desire embeddings.
Headless Chromium for Rendering
Shader thumbnails are generated by a real browser (Chromium via Puppeteer) rather than a lightweight WebGL library. This guarantees pixel-perfect rendering matching what users see in their browsers. The tradeoff is higher resource usage (512MB shared memory) and slower rendering.
Celery over In-Process Jobs
Unlike Tubearr's in-process queue, Fractafrag uses Celery with Redis as a proper distributed task queue. This allows the API to remain responsive while rendering, embedding, and AI generation happen asynchronously. The worker runs as a separate container with independent scaling.
JWT with Redis Blocklist
Refresh tokens are stateless JWTs but validated against a Redis blocklist on each use. This provides immediate token revocation without a database round-trip, while keeping the auth flow mostly stateless.
Immutable Shader Versions
Every shader update creates a new version snapshot in the shader_versions table. This provides full history without git-level complexity, enables version restoration, and supports future diff/compare features.
TF-IDF + SVD Embeddings
Desire prompts are embedded using a custom TF-IDF + TruncatedSVD pipeline trained on a shader/visual-art domain corpus. This avoids external embedding API calls while providing domain-relevant 512-dimensional vectors for cosine similarity clustering.
Request Lifecycle
API Request
Browser → nginx:80 → /api/* → api:8000
→ CORS middleware
→ Auth dependency (JWT validation)
→ Router handler → SQLAlchemy async → PostgreSQL
→ Pydantic serialization → JSON response
Shader Publish Flow
User submits GLSL → POST /api/v1/shaders
→ GLSL validator (static analysis)
→ Free-tier rate limit check (5/month)
→ Create Shader + ShaderVersion v1 in DB
→ Enqueue render_shader Celery task
→ Return shader (render_status=pending)
Worker picks up task:
→ POST /render to renderer:3100 with GLSL
→ Chromium renders shader, captures screenshots
→ Store thumbnail_url + preview_url
→ Update shader render_status → "ready"
Feed Personalization
GET /api/v1/feed (authenticated)
→ Over-fetch 2-3x candidates from DB (published, rendered)
→ Build tag affinity from user's votes + dwell events
→ Score each candidate: 0.5*score + 0.2*recency + 0.2*tag_affinity + 0.1*random
→ Sort, slice top N
→ Return ShaderFeedItem list
Desire Processing
POST /api/v1/desires → Create desire row → Enqueue process_desire task
Worker:
→ Embed prompt text (TF-IDF + SVD → 512-dim vector)
→ pgvector cosine search: find nearest cluster (threshold 0.82)
→ If match: join cluster, recalculate heat_score = cluster_size
→ If no match: create new cluster with this desire
→ Update desire row with embedding + cluster info