diff --git a/.serena/.gitignore b/.serena/.gitignore
index 2e510aff58..964f4226bb 100644
--- a/.serena/.gitignore
+++ b/.serena/.gitignore
@@ -1,2 +1,3 @@
/cache
/project.local.yml
+/diagrams/out/
diff --git a/.serena/diagrams/architecture-flow.mmd b/.serena/diagrams/architecture-flow.mmd
new file mode 100644
index 0000000000..ee18ed9b3b
--- /dev/null
+++ b/.serena/diagrams/architecture-flow.mmd
@@ -0,0 +1,60 @@
+%% Penpot runtime architecture: client, backend, services, workers, export.
+flowchart TB
+ user(["User
web browser"])
+
+ subgraph client["Client · browser"]
+ fe["Frontend SPA
ClojureScript + React"]
+ ww["Web Worker
background computation"]
+ wasm["render-wasm
Rust / Skia → WASM"]
+ plugins["Plugin runtime
sandboxed"]
+ end
+
+ subgraph server["Backend · JVM / Clojure"]
+ rpc["RPC API
/api/rpc/query · mutation"]
+ ws["Notifications
WebSocket"]
+ http["HTTP routes
assets · media · export"]
+ dispatcher["Dispatcher
claims task rows"]
+ runner["Runners
execute tasks"]
+ end
+
+ subgraph data["Stateful services"]
+ pg[("PostgreSQL
durable store + task queue")]
+ redis[("Redis / Valkey
pub/sub · cache · rate limit")]
+ obj[("Object storage
S3 or filesystem")]
+ end
+
+ subgraph export["Export pipeline"]
+ exporter["Exporter
ClojureScript / Node"]
+ chrome["Headless Chrome
Playwright"]
+ end
+
+ media["media-processor
sharp · FontForge"]
+ mail["SMTP mailer"]
+
+ user --> fe
+ fe <--> ww
+ fe --> wasm
+ fe --> plugins
+ fe -->|"HTTPS · Transit"| rpc
+ fe <-->|"WebSocket"| ws
+ fe -->|"HTTPS"| http
+ fe -->|"HTTPS export"| exporter
+
+ rpc -->|"SQL"| pg
+ rpc -->|"enqueue task"| pg
+ rpc --> redis
+ rpc --> obj
+ ws <--> redis
+ http --> obj
+
+ pg -->|"FOR UPDATE SKIP LOCKED"| dispatcher
+ dispatcher -->|"RPUSH claimed ids"| redis
+ redis -->|"BLPOP"| runner
+ runner --> pg
+ runner --> obj
+ runner --> mail
+ runner --> media
+ media --> obj
+
+ exporter --> chrome
+ chrome -->|"loads app"| fe
diff --git a/.serena/diagrams/file-edit-flow.mmd b/.serena/diagrams/file-edit-flow.mmd
new file mode 100644
index 0000000000..42c56299ab
--- /dev/null
+++ b/.serena/diagrams/file-edit-flow.mmd
@@ -0,0 +1,19 @@
+%% Collaborative edit flow: one user's change reaches the other clients.
+sequenceDiagram
+ autonumber
+ participant U as User / browser
+ participant FE as Frontend SPA
+ participant RPC as Backend RPC
+ participant PG as PostgreSQL
+ participant MB as Redis pub/sub
+ participant WS as WebSocket
+ participant O as Other clients
+
+ U->>FE: edits a shape
+ FE->>RPC: mutation update-file (Transit)
+ RPC->>PG: persist change, bump revision
+ RPC->>MB: publish file-change on file topic
+ RPC-->>FE: mutation result
+ MB-->>WS: deliver file-change
+ WS-->>O: push change over WebSocket
+ O->>O: apply change locally
diff --git a/.serena/memories/architecture/core.md b/.serena/memories/architecture/core.md
new file mode 100644
index 0000000000..d70ae43dcf
--- /dev/null
+++ b/.serena/memories/architecture/core.md
@@ -0,0 +1,31 @@
+# Architecture Diagrams
+
+Canonical, versioned diagrams of the Penpot runtime and its flows, meant for humans and agents.
+
+## Format and location
+
+- Source of truth: `.serena/diagrams/**/*.mmd`, one Mermaid diagram per file; plain text, git-versioned, diff-friendly, and readable by an LLM without rendering.
+- Mermaid is the chosen format because LLMs generate it reliably and it renders in a browser and in HTML without a build step.
+- `architecture-flow.mmd` is the runtime topology: browser client, JVM backend, stateful services, task-worker model, and export pipeline.
+- `file-edit-flow.mmd` is the collaborative edit sequence: RPC mutation → PostgreSQL → Redis pub/sub → WebSocket → peer clients.
+- The rendered HTML index is generated, not versioned: `.serena/diagrams/out/` is gitignored.
+
+## Rendering
+
+- HTML index: `scripts/render-diagrams` writes `.serena/diagrams/out/index.html`; Mermaid is imported from a pinned jsDelivr CDN, so opening the page needs network.
+- SVG or PNG: `scripts/render-diagrams --format svg` (or `png`) calls `mmdc` from `@mermaid-js/mermaid-cli`; install it with `pnpm add -D @mermaid-js/mermaid-cli`.
+- Render a single diagram with `scripts/render-diagrams --diagram architecture-flow`.
+- Tests for the renderer: `python3 scripts/test_render_diagrams.py`.
+
+## Content sources
+
+- Module graph and cross-module dependencies: `mem:critical-info` (Dependency graph).
+- Runtime services and the task-queue versus Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
+- Backend internals, RPC, notifications, and storage backends: `mem:backend/core` and `mem:backend/storage`.
+- The prose architecture pages, in C4 with PlantUML, live in `docs/technical-guide/developer/architecture/`; keep the Mermaid flow views consistent with them instead of duplicating their content.
+
+## Maintenance invariants
+
+- Update the affected `.mmd` in the same change that alters the topology it shows: a new module, a new service, or a changed flow.
+- One concern per file; name files by topic with a `.mmd` suffix; keep node labels quoted so punctuation does not break the parser.
+- Do not put diagram source inline in memories; reference the file so the graph stays small and the diagram stays renderable.
diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md
index f0cec128dc..982466db2a 100644
--- a/.serena/memories/critical-info.md
+++ b/.serena/memories/critical-info.md
@@ -63,6 +63,10 @@ The memory is structured in a way that you can get the critical information abou
`frontend -> common`, `backend -> common`, `exporter -> common`, and `frontend -> render-wasm`. Changes in `common` can affect frontend, backend, exporter, file migrations, and design-library behavior; validate across consumers when semantics change.
+# Architecture diagrams
+
+- Versioned Mermaid diagrams of the runtime topology and the collaborative edit flow, plus how to render them to HTML or SVG/PNG: `mem:architecture/core`.
+
# Working with Penpot designs
- Before automating or inspecting Penpot designs through the Plugin API, call the Penpot MCP `high_level_overview` tool.
diff --git a/scripts/render-diagrams b/scripts/render-diagrams
new file mode 100755
index 0000000000..8154c737b3
--- /dev/null
+++ b/scripts/render-diagrams
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+"""Render the versioned Mermaid architecture diagrams.
+
+Reads ``*.mmd`` sources from a directory (``.serena/diagrams`` by
+default) and writes a single browsable HTML index. With ``--format
+svg`` or ``--format png`` it shells out to ``mmdc`` from
+``@mermaid-js/mermaid-cli`` instead.
+
+Examples::
+
+ scripts/render-diagrams
+ scripts/render-diagrams --format svg
+ scripts/render-diagrams --diagram architecture-flow
+"""
+
+from __future__ import annotations
+
+import argparse
+import html
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+DEFAULT_SRC = Path(".serena/diagrams")
+DEFAULT_MERMAID_VERSION = "11"
+SOURCE_SUFFIXES = (".mmd", ".mermaid")
+
+
+class RenderError(RuntimeError):
+ """Raised when a diagram cannot be rendered."""
+
+
+def discover_diagrams(src: Path, out: Path | None = None) -> list[Path]:
+ """Return the diagram sources under ``src``, sorted by path.
+
+ ``out``, when given, is skipped so a previous render is never read
+ back as a source.
+ """
+ if not src.is_dir():
+ return []
+
+ ignored = out.resolve() if out is not None else None
+ found: list[Path] = []
+ for path in src.rglob("*"):
+ if not path.is_file() or path.suffix.lower() not in SOURCE_SUFFIXES:
+ continue
+ if ignored is not None and ignored in path.resolve().parents:
+ continue
+ found.append(path)
+ return sorted(found)
+
+
+def filter_diagrams(diagrams: list[Path], names: list[str] | None) -> list[Path]:
+ """Keep only the diagrams whose file stem is in ``names``."""
+ if not names:
+ return diagrams
+ wanted = set(names)
+ return [path for path in diagrams if path.stem in wanted]
+
+
+def build_html(
+ diagrams: list[Path],
+ src: Path,
+ mermaid_version: str = DEFAULT_MERMAID_VERSION,
+) -> str:
+ """Build a self-contained HTML index embedding every diagram source."""
+ sections = []
+ for path in diagrams:
+ source = path.read_text(encoding="utf-8").strip()
+ name = path.relative_to(src).as_posix()
+ sections.append(
+ f'{html.escape(name)}
\n"
+ f'{html.escape(source)}\n'
+ f"
No diagrams found.
" + return f""" + + + + +