Penpot
Audited from github.com/penpot/penpot
Penpot is an open-source, MPL-2.0 design and prototyping platform with a Clojure and ClojureScript core, a Rust/WebAssembly canvas renderer, PostgreSQL storage, and separate Redis-backed export and media-processing services.
- Language
- Clojure
- Database
- PostgreSQL
- Hosting
- Docker
- License
- MPL-2.0
- Running for
- 4 years 2 months
- Team
- 20+ people
Why this architecture
Splitting the backend, frontend, exporter, media processing, and MCP server into independently managed pnpm/Clojure projects, and moving canvas rendering into a Rust/WebAssembly engine, lets each heavy or performance-sensitive piece evolve and scale on its own instead of living inside one ClojureScript monolith.
Tech stack by layer
10 technologies · audited Sep 17, 2026- ClojureScriptThe main app and worker are compiled by shadow-cljs (build:app:main runs "clojure -M:dev:shadow-cljs release main worker").
- Rustrender-wasm implements the canvas renderer, compiled to WebAssembly and invoked from the frontend build via build:wasm.
- WebAssemblyCompilation target for the render-wasm crate, loaded by the frontend as the canvas rendering engine.
- ClojureServer-side application code lives under backend/src with its own deps.edn.
- PostgreSQLPrimary database; a dedicated maintenance script upgrades it across major versions.
- ClojureScriptThe exporter service is a Node-targeted ClojureScript app that drives headless Chromium through Playwright.
- PostgreSQLPrimary relational datastore for the backend.
- RedisThe exporter depends on ioredis, used to coordinate export jobs outside the main backend process.
- Dockerdocker/devenv and docker/imagemagick provide Dockerfiles for the dev environment and image-processing tooling.
- pnpmEach subproject (backend, frontend, common, exporter, library, mcp, media-processor, docs, render-wasm) is its own independently versioned pnpm workspace.
- GitHub ActionsPer-module workflows (tests-backend.yml, tests-frontend.yml, tests-wasm.yml, tests-mcp.yml) plus build-docker*.yml handle CI and release bundling.
- Model Context Protocol@penpot/mcp is a standalone package exposing a penpot-mcp binary for AI-agent integration.
- PlaywrightUsed both by the exporter to render designs headlessly and, via @playwright/mcp at the repo root, for MCP-driven browser test tooling.
Penpot architecture diagram
Open SVGDiagram as text
- Penpot app (ClojureScript) → Backend (Clojure): API
- Penpot app (ClojureScript) → Canvas renderer (Rust → WebAssembly): draw
- MCP clients (@penpot/mcp) → Backend (Clojure): API
- Backend (Clojure) → PostgreSQL (primary DB): queries
- Backend (Clojure) → Exporter (ClojureScript · Playwright): export
- Exporter (ClojureScript · Playwright) → Redis (export jobs): coordinate
- Backend (Clojure) → Media processor (TypeScript · stateless): images · fonts
Key architectural decisions
6 decisions- 01
Every subsystem is its own independently managed project, not one JS monorepo
backend, frontend, common, exporter, library, mcp, media-processor, docs, and render-wasm each have their own package.json and pnpm-workspace.yaml (and, for the Clojure pieces, their own deps.edn), and the root package.json only carries end-to-end test tooling (@playwright/test, @playwright/mcp, nrepl-client) rather than a shared workspaces field.
- 02
Canvas rendering moved from ClojureScript to a Rust/WebAssembly engine
render-wasm/Cargo.toml depends on skia-safe, glam, and bezier-rs to implement the renderer in Rust, and frontend/package.json’s build:wasm script ("../render-wasm/build frontend") compiles it to WebAssembly and wires it into the ClojureScript-built app rather than keeping the renderer purely in ClojureScript.
- 03
Design export runs as a separate ClojureScript service driving headless Chromium
exporter/package.json is a shadow-cljs-targeted Node project depending directly on playwright for headless rendering and ioredis for job coordination, isolating the CPU- and memory-heavy PDF/PNG/SVG export path from the main backend process.
- 04
Image and font processing is a separate stateless TypeScript service
media-processor/package.json describes itself as a "Stateless HTTP service for Penpot image and font processing," built with Express and Sharp and its own esbuild-based build script, distinct from both the Clojure backend and the ClojureScript exporter.
- 05
MCP support ships as its own installable package
mcp/package.json defines a bin entry (penpot-mcp) and bootstrap/build/start scripts that run across an internal pnpm -r workspace, so Model Context Protocol access to Penpot is a separately versioned artifact rather than a flag on an existing service.
- 06
Production frontend builds still go through shadow-cljs, not a JS bundler
Despite a Vite config existing in the tree (frontend/vite.config.js), the actual app build script (build:app:main) invokes "clojure -M:dev:shadow-cljs release main worker", so shadow-cljs remains the ClojureScript-to-JS compiler for the shipped application.
How Penpot is built
How Penpot is structured
Penpot has no single top-level workspace file tying its JavaScript-adjacent projects together; instead, backend, frontend, common, exporter, library, mcp, media-processor, docs, and render-wasm each carry their own package.json and pnpm-workspace.yaml, and the Clojure/ClojureScript pieces (backend, frontend, common, exporter, library) additionally have their own deps.edn. The root package.json is limited to end-to-end tooling — @playwright/test, @playwright/mcp, esbuild, nrepl-client — rather than a shared workspaces field, so coordination across subsystems happens through Clojure's deps.edn alias system and explicit build scripts rather than a unified JS package manager view.
Frontend
The main application is ClojureScript compiled by shadow-cljs: frontend/package.json's build:app:main script runs clojure -M:dev:shadow-cljs release main worker, producing both the UI thread and a background worker bundle. A frontend/vite.config.js exists in the tree, but the shipped app build goes through shadow-cljs, not Vite. Styling is SCSS (777KB across the repo per language stats), and Storybook (build:storybook, build:storybook:cljs) documents UI components separately from the main app build. The canvas itself is rendered by a Rust crate, render-wasm, built on skia-safe, glam, and bezier-rs (per render-wasm/Cargo.toml) and compiled to WebAssembly via the frontend's build:wasm script, which shells out to ../render-wasm/build frontend.
Backend & APIs
The server lives under backend/, itself a Clojure project with its own deps.edn (present in the tree at backend/deps.edn) and a small backend/package.json for JS-side test tooling (eventsource-parser, sax, and Node's test runner for test/e2e/*.test.mjs). Shared logic used by both the backend and the ClojureScript frontend sits in common/, itself a separate Clojure/ClojureScript project with its own shadow-cljs.edn and deps.edn. A library/ project packages reusable design-file logic (per library/README.md and its own deps.edn/shadow-cljs.edn) as a distinct, independently published artifact.
Data & persistence
PostgreSQL is the backend's primary database; the presence of docker/postgres-upgrade.sh, a script specifically for migrating a running Postgres instance across major versions, is direct evidence of that dependency rather than a generic "any SQL database" claim. Redis shows up in the export path: exporter/package.json depends on ioredis, used to coordinate export jobs outside the main backend's own state, separate from whatever store the Clojure backend itself uses for its data (not captured in the audited manifests beyond the Postgres upgrade tooling).
Build, test & deploy
CI is split per module: .github/workflows includes tests-backend.yml, tests-frontend.yml, tests-common.yml, tests-library.yml, tests-exporter.yml, tests-wasm.yml, tests-mcp.yml, tests-plugins.yml, and tests-integration.yml, each presumably scoped to its own subproject, alongside tests-composable-suite.yml for cross-cutting checks. Release and image builds are similarly split: build-docker.yml, build-docker-admin-console.yml, and build-docker-devenv.yml build different Docker images, while build-develop.yml, build-staging.yml, build-tag.yml, and build-bundle.yml handle branch- and tag-specific release bundling. render-wasm's own Cargo.toml defines multiple release profiles (release, size) with different optimization levels (opt-level = 3 with lto = "fat" versus opt-level = "z" for a smaller binary), giving the WebAssembly build a size/speed tradeoff knob independent of the rest of the build.
Self-hosting notes
The repository ships development-environment Docker tooling under docker/devenv and docker/gitpod (the latter backing the README's "Gitpod ready-to-code" badge), plus a purpose-built docker/imagemagick Dockerfile for the image-processing dependencies some Penpot features need. Combined with the per-service Docker image workflows, this points to a self-hosting story built around Docker images per subsystem rather than a single all-in-one container, though the audited manifests do not include a top-level docker-compose.yml that ties them together.
What to copy (and what not to)
Extracting performance-critical or heavyweight paths — canvas rendering into a Rust/WebAssembly crate, PDF/PNG/SVG export into a Playwright-driven Node service, image and font processing into a stateless Express/Sharp service — out of the main application is a pattern worth copying whenever one part of an app has fundamentally different resource or language needs than the rest. Giving every subsystem its own independent package manifest and dependency file, rather than a single monorepo workspace, avoids one subsystem's dependency churn from destabilizing the others, but it is a heavier operational model: a smaller team should weigh whether nine independently versioned projects are worth the coordination cost before copying this structure wholesale.
Sources & repo audit
- render-wasm/Cargo.toml (Rust/Skia canvas renderer compiled to WebAssembly)
- frontend/package.json (shadow-cljs build scripts, Playwright e2e tests)
- exporter/package.json (ClojureScript exporter with Playwright and ioredis)
- media-processor/package.json (standalone Express/Sharp image-processing service)
- mcp/package.json (standalone @penpot/mcp package with penpot-mcp bin)
- docker/postgres-upgrade.sh (PostgreSQL as the primary datastore)
Independent analysis of repository at github.com/penpot/penpot. Spotted an inaccuracy? Use the claim form to request a correction.
Show this stack on your README or website
[](https://stackitfast.com/project/penpot)Paste it into your README. It shows your current stack and links to this page.
Scaffold it with your agent
Paste this prompt into Claude Code, Cursor, Windsurf or AGY to start a project with Penpot's architecture.
- 1Copy the promptThe full markdown spec, with every layer and decision.
- 2Open your AI toolClaude Code, Cursor, Windsurf or Copilot, in a new repo.
- 3Paste and scaffoldUse it as the first instruction; review before you ship.
# MISSION: Scaffold "Penpot" Production Architecture
You are an expert Senior Staff Software Architect and Full-Stack Engineer. Your mission is to scaffold and implement a production-grade, highly reliable, and modular codebase following the proven architecture of **Penpot**.
---
## 1. PROJECT SPECIFICATIONS & BENCHMARK
- **Reference Architecture**: Penpot
- **What It Does**: Penpot is an open-source, MPL-2.0 design and prototyping platform with a Clojure and ClojureScript core, a Rust/WebAssembly canvas renderer, PostgreSQL storage, and separate Redis-backed export and media-processing services.
- **Domain & Category**: Design & Prototyping Platform
- **Production Scale**: 20+ people
- **Development Mode**: CLASSIC
- **Architectural Rationale**: Splitting the backend, frontend, exporter, media processing, and MCP server into independently managed pnpm/Clojure projects, and moving canvas rendering into a Rust/WebAssembly engine, lets each heavy or performance-sensitive piece evolve and scale on its own instead of living inside one ClojureScript monolith.
- **Live Website Reference**: https://penpot.app
- **Source Repository**: https://github.com/penpot/penpot
---
## 2. PRODUCTION TECH STACK
- **Full Stack Array**: Clojure, ClojureScript, Rust, WebAssembly, TypeScript, PostgreSQL, Redis, Docker, pnpm, Playwright
- **Primary Language(s)**: Clojure, JavaScript, Rust
- **License of the reference repo**: MPL-2.0
- **Frontend**: ClojureScript — The main app and worker are compiled by shadow-cljs (build:app:main runs "clojure -M:dev:shadow-cljs release main worker").; Rust — render-wasm implements the canvas renderer, compiled to WebAssembly and invoked from the frontend build via build:wasm.; WebAssembly — Compilation target for the render-wasm crate, loaded by the frontend as the canvas rendering engine.
- **Backend & APIs**: Clojure — Server-side application code lives under backend/src with its own deps.edn.; PostgreSQL — Primary database; a dedicated maintenance script upgrades it across major versions.; ClojureScript — The exporter service is a Node-targeted ClojureScript app that drives headless Chromium through Playwright.
- **Data & persistence**: PostgreSQL — Primary relational datastore for the backend.; Redis — The exporter depends on ioredis, used to coordinate export jobs outside the main backend process.
- **Infrastructure & deploy**: Docker — docker/devenv and docker/imagemagick provide Dockerfiles for the dev environment and image-processing tooling.; pnpm — Each subproject (backend, frontend, common, exporter, library, mcp, media-processor, docs, render-wasm) is its own independently versioned pnpm workspace.; GitHub Actions — Per-module workflows (tests-backend.yml, tests-frontend.yml, tests-wasm.yml, tests-mcp.yml) plus build-docker*.yml handle CI and release bundling.
- **Tooling, testing & ops**: Model Context Protocol — @penpot/mcp is a standalone package exposing a penpot-mcp binary for AI-agent integration.; Playwright — Used both by the exporter to render designs headlessly and, via @playwright/mcp at the repo root, for MCP-driven browser test tooling.
---
## 3. KEY ARCHITECTURAL DECISIONS (audited from https://github.com/penpot/penpot @ ecf8436)
1. **Every subsystem is its own independently managed project, not one JS monorepo**: backend, frontend, common, exporter, library, mcp, media-processor, docs, and render-wasm each have their own package.json and pnpm-workspace.yaml (and, for the Clojure pieces, their own deps.edn), and the root package.json only carries end-to-end test tooling (@playwright/test, @playwright/mcp, nrepl-client) rather than a shared workspaces field.
2. **Canvas rendering moved from ClojureScript to a Rust/WebAssembly engine**: render-wasm/Cargo.toml depends on skia-safe, glam, and bezier-rs to implement the renderer in Rust, and frontend/package.json’s build:wasm script ("../render-wasm/build frontend") compiles it to WebAssembly and wires it into the ClojureScript-built app rather than keeping the renderer purely in ClojureScript.
3. **Design export runs as a separate ClojureScript service driving headless Chromium**: exporter/package.json is a shadow-cljs-targeted Node project depending directly on playwright for headless rendering and ioredis for job coordination, isolating the CPU- and memory-heavy PDF/PNG/SVG export path from the main backend process.
4. **Image and font processing is a separate stateless TypeScript service**: media-processor/package.json describes itself as a "Stateless HTTP service for Penpot image and font processing," built with Express and Sharp and its own esbuild-based build script, distinct from both the Clojure backend and the ClojureScript exporter.
5. **MCP support ships as its own installable package**: mcp/package.json defines a bin entry (penpot-mcp) and bootstrap/build/start scripts that run across an internal pnpm -r workspace, so Model Context Protocol access to Penpot is a separately versioned artifact rather than a flag on an existing service.
6. **Production frontend builds still go through shadow-cljs, not a JS bundler**: Despite a Vite config existing in the tree (frontend/vite.config.js), the actual app build script (build:app:main) invokes "clojure -M:dev:shadow-cljs release main worker", so shadow-cljs remains the ClojureScript-to-JS compiler for the shipped application.
---
## 4. NON-NEGOTIABLE ARCHITECTURAL GUARDRAILS
1. **Monorepo & Modular Separation**:
- Structure as a Turborepo monorepo with strict package boundaries:
- `apps/web`: Application UI, routing, layouts, and server endpoints.
- `packages/ui`: Shared design tokens, CSS variables, and Radix UI primitive components.
- `packages/db`: Database schemas, client singleton, declarative migrations, and seed scripts.
- `packages/config`: Shared TypeScript, ESLint, and build configurations.
2. **Strict Type Safety & Zero `any` Policy**:
- Enable `strict: true`, `noImplicitAny: true`, and `strictNullChecks: true`.
- Validate ALL external inputs, API request bodies, and query parameters with **Zod** schemas before execution.
3. **Frontend & Rendering Guidelines**:
- Isolate interactive UI state to leaf components. Keep core pages lightweight and performant.
4. **Design System & Aesthetics**:
- Keep every color, radius, shadow and font in a single token file (CSS variables) and consume tokens everywhere; never hardcode hex values in components.
- Prefer crisp 1px borders and one subtle shadow scale over blurry default shadows. Pair one sans-serif for body/headings with one monospace for tags, badges, metrics, and code.
5. **Data Layer & Reliability**:
- Write declarative schema definitions with foreign keys, composite indexes on queried filters, and automated timestamp triggers.
- Use connection pooling and prepared statements for serverless database execution.
---
## 5. STEP-BY-STEP SCAFFOLDING ROADMAP
- **Phase 1: Workspace & Root Config**: Initialize package manager, monorepo configuration (`turbo.json`, `tsconfig.base.json`, `package.json`).
- **Phase 2: Database Schema & Client**: Set up the data layer (PostgreSQL, Redis): client, connection pool, models, and migration scripts.
- **Phase 3: Design Tokens & UI Primitives**: Build accessible `Button`, `Input`, `Card`, `Badge`, and layout wrappers inside `packages/ui`.
- **Phase 4: Core Application Routes & Handlers**: Implement primary authentication, user session handling, and application routes.
- **Phase 5: Quality Assurance & Build Verification**: Run `tsc --noEmit`, ESLint, Prettier, and smoke test suites to ensure zero compilation or runtime errors.
---
## 6. EXECUTION INSTRUCTIONS
1. Review all specifications, architectural guardrails, and stack choices above.
2. Present the full monorepo directory tree structure.
3. Systematically generate the complete, production-ready codebase according to the 5-phase roadmap above — starting with the root workspace setup, followed by the database schema, UI design system package, and full-stack application routes until the repository is fully scaffolded and ready to run.Frequently asked about Penpot
What is Penpot built with?
A Clojure backend and ClojureScript frontend compiled with shadow-cljs, a Rust canvas renderer compiled to WebAssembly, PostgreSQL for storage, and separate Node-based services for export (Playwright) and media processing.
What database does Penpot use?
PostgreSQL. The repository ships a dedicated docker/postgres-upgrade.sh script for moving between major Postgres versions.
Can Penpot be self-hosted?
Yes. The repository includes Docker tooling for a dev environment (docker/devenv) and image-processing dependencies (docker/imagemagick), alongside per-module CI workflows that build Docker images for release.
Does Penpot support the Model Context Protocol for AI agents?
Yes, via a standalone package, @penpot/mcp, which installs a penpot-mcp binary independent of the main backend and frontend.
How does Penpot render its design canvas?
Through a Rust-based renderer in render-wasm, built on the Skia graphics library and compiled to WebAssembly, which the ClojureScript frontend loads via its build:wasm step.
How does Penpot export designs to PDF, PNG, or SVG?
A dedicated exporter service, written in ClojureScript and run on Node, drives headless Chromium through Playwright and coordinates jobs using Redis (ioredis), separate from the main backend.
One email a month: new deep dives and stack trends
New source-audited architectures, head-to-head comparisons and the monthly stack report. No spam, unsubscribe anytime.
Similar architectures
- HoppscotchClassicAPI Testing Client · 6-20 peopleShares TypeScript · PostgreSQL · Redis
- InfisicalHybridSecret Management · 6-20 peopleShares TypeScript · PostgreSQL · Redis
- Trigger.devHybridJob Queue / Dev Tool · 6-20 peopleShares TypeScript · PostgreSQL · Redis
- AppwriteClassicBackend-as-a-Service · 20+ peopleShares TypeScript · PostgreSQL · Redis
- MetabaseClassicBusiness Intelligence Platform · 20+ peopleShares Clojure · ClojureScript · TypeScript
- LocoClassicRails-Style Rust Web Framework · 2-5 peopleShares Rust · PostgreSQL · Redis