System Architecture
Canopy is a web application built from four main components. This page describes the stack, explains the design choices, and covers the optional modules you can add to extend the platform.
The four-component stack
Browser (React SPA)
│
│ /api/* (HTTP proxy)
▼
Edge Nginx ──────────────────────────► /drawio/* (self-hosted DrawIO)
│
│ proxy_pass :8000
▼
FastAPI Backend (Python 3.12, uvicorn)
├── SQLAlchemy 2 (async, asyncpg)
├── Alembic migrations
├── JWT auth (HS256) + bcrypt
├── SSE event stream
└── Rate limiting (slowapi)
│
▼
PostgreSQL 18
React SPA (frontend)
The entire UI is a single-page application built with React 18, MUI 6, and React Router 7. Vite handles the build; all route-level pages use lazy() imports for code splitting, so only the code for the current page is loaded. The SPA talks exclusively to the FastAPI backend through the /api/ prefix — there is no direct database access from the frontend.
Key frontend libraries: AG Grid (inventory table), Recharts (charts), bpmn-js (BPMN editing), TipTap (rich text), @dnd-kit (drag-and-drop), React Flow (architecture diagrams).
FastAPI backend
The backend handles all business logic, data access, permission enforcement, and real-time events. It is structured as a collection of routers (one per domain) that all mount under /api/v1/. Every route handler is async, using SQLAlchemy's async session with the asyncpg driver.
Authentication uses JWT tokens (HS256, stored in the browser's sessionStorage). Bcrypt handles password hashing. Sensitive values stored in the database (SSO secrets, SMTP passwords) are encrypted with Fernet symmetric encryption before writing and decrypted on read.
PostgreSQL
All application data lives in PostgreSQL. The schema is managed by Alembic; the application runs alembic upgrade head on every startup, so schema migrations are applied automatically on update. Tables use UUID primary keys throughout.
There is no Redis, no message queue, and no external cache. Real-time updates use Server-Sent Events (SSE) over a long-polling HTTP connection directly from the backend.
Edge Nginx
A thin Nginx container sits in front of the React and FastAPI containers. It serves the compiled React SPA, proxies /api/* to the backend (with SSE-appropriate headers), serves the self-hosted DrawIO at /drawio/*, and applies security headers (CSP, HSTS, X-Frame-Options, etc.). In production this is the only container exposed to the network.
Optional modules
Ollama (AI suggestions)
Add --profile ai to Docker Compose to start a bundled Ollama container alongside the main stack. The backend's AI suggestion pipeline calls the Ollama HTTP API to generate card descriptions using a locally-running LLM. The model runs entirely within your infrastructure.
You can also point Canopy at any Ollama-compatible external provider (OpenAI, Anthropic via a proxy, etc.) by setting AI_PROVIDER_URL.
MCP server
Add --profile mcp to start the Model Context Protocol server. This exposes Canopy's data to AI assistants (Claude Desktop, VS Code Copilot, etc.) as a set of tools — read-only queries by default, with opt-in write tools guarded by per-call size limits and a confirmation flow.
The MCP server authenticates via OAuth 2.1 delegated to the Canopy SSO provider, so users see the same permissions through MCP tools as they do in the web UI.
Data flow: a card save
To make the architecture concrete, here is what happens when a user saves a card:
- React calls
PATCH /api/v1/cards/{id}with the updated payload - Nginx proxies the request to the FastAPI backend
- The route handler validates the JWT, checks the user's
inventory.editpermission, and validates the request body with a Pydantic schema - SQLAlchemy writes the updated row to PostgreSQL
- The calculation engine runs any active formulas for this card type
- The data quality score is recomputed from the fields' weights
- An event is published to the in-memory SSE bus
- Any connected browser receives the event and updates the UI in real time
Ports and networking
| Service | Internal port | Host-exposed |
|---|---|---|
| Nginx (edge) | 80 | HOST_PORT (default 8920) |
| FastAPI | 8000 | No — internal only |
| React (dev only) | 5173 | Yes in dev, served via Nginx in prod |
| PostgreSQL | 5432 | No — internal only |
| Ollama (optional) | 11434 | No — internal only |
| MCP server (optional) | 8001 | Via Nginx /mcp/ prefix |
See also
- Understanding the Metamodel — the data model design
- MCP Integration — configuring the MCP server
- AI Capabilities — configuring the AI suggestion feature