# excalidraw/excalidraw — codebase unpack

> Canonical page: https://codevetter.com/unpack/excalidraw

This is a deterministic Repo Unpack scan of excalidraw/excalidraw — a structural map, not a review, security audit, or maintainer-endorsed description.

- Scanned commit: 31df3e6ef245c646b18a055ccf858cd6117c7768 (2026-09-22)
- Last upstream commit: fix(editor): Tab convert bound arrow update (#12143)
- Generated by codevetter 1.12.0 on 2026-09-22
- 1000 files scanned · 12.3 MB

## Languages

- TypeScript: 659 files
- JSON: 95 files
- Sass: 82 files
- Markdown: 51 files
- JavaScript: 26 files
- YAML: 14 files
- CSS: 3 files
- HTML: 2 files

## Repository structure

- packages/ — 795 files
- dev-docs/ — 58 files
- excalidraw-app/ — 51 files
- examples/ — 28 files
- .github/ — 16 files
- scripts/ — 13 files
- firebase-project/ — 6 files
- public/ — 4 files

Probable source entrypoint: `packages/` (inferred from the tree, not upstream docs)

## Source layout — packages/

- packages/element/ — 88 files
- packages/excalidraw/ — 65 files
- packages/fractional-indexing/ — 4 files
- packages/tsconfig.base.json — 1 file

## Package

- package.json: excalidraw-monorepo — 34 declared dependencies

## Structural health signals

Average score 6.5/10 across 96 files; 49 hotspots. These are deterministic review leads, not proof of a bug.

## Recent history

- 31df3e6ef245 (2026-09-22) fix(editor): Tab convert bound arrow update (#12143)
- 97c68dd371e1 (2026-09-19) refactor(editor): split out duplication logic into App.duplicate.ts (#12125)
- 14e1c614d477 (2026-09-19) feat(packages/excalidraw): support onDuplicate replacements, vetoes and lookups (#12124)
- c0ad61c6743a (2026-09-16) feat(editor): rightclick to pan canvas (#12110)
- a9186480121a (2026-09-15) feat(packages/excalidraw): add renderOverrides to allow overriding of element rendering (#12100)

## Overview

Excalidraw is a Yarn-workspace monorepo that publishes a React whiteboard component (@excalidraw/excalidraw 0.18.0) and ships the excalidraw.com app in excalidraw-app/. The editor is an infinite canvas of hand-drawn elements (roughjs) with local-first autosave, optional real-time collaboration, and end-to-end AES-GCM encryption of scenes before they leave the browser. The hosted app adds Firebase persistence, a Socket.IO portal, shareable encrypted links, a PWA, and AI diagram helpers; the library itself is meant to be embedded by other products.

## System map

The runtime splits into a publishable editor (packages/excalidraw) and a Vite app (excalidraw-app) that mounts it. Geometry and the element model live in @excalidraw/element, @excalidraw/math, and @excalidraw/common; export helpers live in @excalidraw/utils. The app talks to three external systems configured only by Vite env vars: a Socket.IO collab server, Firebase (Firestore scenes plus Storage blobs), and an HTTP share backend. Production is a static Vite build deployed to Vercel or an nginx image; package publishes go through scripts/release.js on the release branch.
- Root package excalidraw-monorepo is a private Yarn 1.22.22 workspace (node >=18) covering excalidraw-app, packages/*, and examples/*. Scripts delegate: yarn start runs the Vite app, yarn build:packages builds common, fractional-indexing, laser-pointer, math, element, then excalidraw, and yarn test:all chains typecheck, eslint, prettier, and vitest. — [package.json#L1-96](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/package.json#L1-L96)
- The hosted app boots from excalidraw-app/index.tsx: it imports sentry, sets window.__EXCALIDRAW_SHA__ from VITE_APP_GIT_SHA, calls registerSW() from vite-plugin-pwa, and createRoot-renders <ExcalidrawApp /> in StrictMode. — [excalidraw-app/index.tsx#L1-17](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/index.tsx#L1-L17) [excalidraw-app/package.json#L43-52](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/package.json#L43-L52)
- The npm component entry is packages/excalidraw/index.tsx. ExcalidrawBase normalizes UIOptions and imageOptions, blocks iOS pinch-zoom unless interaction.enabled.browserZoom is set, and renders EditorJotaiProvider > InitializeApp > App. Excalidraw is React.memo(ExcalidrawBase, areEqual). Package exports point at dist/prod/index.js with peer react 17, 18, or 19. — [packages/excalidraw/index.tsx#L67-266](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L67-L266) [packages/excalidraw/index.tsx#L401-402](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L401-L402) [packages/excalidraw/package.json#L1-78](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/package.json#L1-L78)
- Supporting packages are @excalidraw/common (constants, emitter, editorInterface, feature flags), @excalidraw/element (Scene, Store, element types, fractional index), @excalidraw/math, @excalidraw/utils (exportToCanvas/Svg wrappers), @excalidraw/fractional-indexing, and @excalidraw/laser-pointer. Vitest and the app Vite config alias every @excalidraw/* import at the TypeScript source, not dist. — [packages/common/src/index.ts#L1-17](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/common/src/index.ts#L1-L17) [vitest.config.mts#L6-73](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/vitest.config.mts#L6-L73) [excalidraw-app/vite.config.mts#L24-92](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/vite.config.mts#L24-L92) [tsconfig.json#L21-38](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/tsconfig.json#L21-L38)
- App-only env surface is declared in excalidraw-app/vite-env.d.ts: VITE_APP_WS_SERVER_URL, VITE_APP_FIREBASE_CONFIG, VITE_APP_BACKEND_V2_GET_URL, VITE_APP_BACKEND_V2_POST_URL, VITE_APP_AI_BACKEND, VITE_APP_PLUS_APP, VITE_APP_PLUS_LP, VITE_APP_PORTAL_URL, VITE_APP_ENABLE_PWA, VITE_APP_DISABLE_SENTRY, VITE_APP_GIT_SHA. Vite loads env from the repo root (envDir: '../') and serves on VITE_APP_PORT or 3000, writing the production bundle to excalidraw-app/build. — [excalidraw-app/vite-env.d.ts#L4-45](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/vite-env.d.ts#L4-L45) [excalidraw-app/vite.config.mts#L11-23](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/vite.config.mts#L11-L23) [excalidraw-app/vite.config.mts#L94-118](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/vite.config.mts#L94-L118)
- Vercel config sets outputDirectory to excalidraw-app/build, installCommand yarn install, CORS Access-Control-Allow-Origin https://excalidraw.com, and redirects /webex/* to for-webex.excalidraw.com plus vscode.excalidraw.com to the VS Code marketplace item. — [vercel.json#L1-68](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/vercel.json#L1-L68)
- Docker image is a multi-stage build: node:24 runs yarn --frozen-lockfile then yarn build:app:docker (VITE_APP_DISABLE_SENTRY=true), and nginx:stable-alpine-slim serves excalidraw-app/build. docker-compose publishes host 3000 to container 80 and bind-mounts the repo. Publish workflow on branch release pushes excalidraw/excalidraw:latest for linux/amd64, arm64, and arm/v7. — [Dockerfile#L1-20](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/Dockerfile#L1-L20) [docker-compose.yml#L1-24](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/docker-compose.yml#L1-L24) [excalidraw-app/package.json#L45](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/package.json#L45) [ .github/workflows/publish-docker.yml#L1-30](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/%20.github/workflows/publish-docker.yml#L1-L30)
- GitHub Actions: test.yml runs yarn test:app on push to master (Node 20); lint.yml on pull_request runs yarn test:other, yarn test:code, and yarn test:typecheck; test-coverage-pr.yml runs yarn test:coverage and posts a coverage comment. release-branch workflows run yarn release --tag=next and upload Sentry sourcemaps. — [.github/workflows/test.yml#L1-19](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/test.yml#L1-L19) [.github/workflows/lint.yml#L1-22](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/lint.yml#L1-L22) [.github/workflows/test-coverage-pr.yml#L1-25](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/test-coverage-pr.yml#L1-L25) [.github/workflows/autorelease-excalidraw.yml#L1-27](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/autorelease-excalidraw.yml#L1-L27) [.github/workflows/sentry-production.yml#L1-37](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/sentry-production.yml#L1-L37)
- Firebase is initialized lazily in excalidraw-app/data/firebase.ts from JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG). Scenes are Firestore documents at scenes/{roomId}; image bytes go to Storage via uploadBytes. There is no collaboration server implementation in this repo — the client only opens socket.io-client against VITE_APP_WS_SERVER_URL. — [excalidraw-app/data/firebase.ts#L44-79](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/firebase.ts#L44-L79) [excalidraw-app/data/firebase.ts#L145-171](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/firebase.ts#L145-L171) [excalidraw-app/collab/Collab.tsx#L518-539](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Collab.tsx#L518-L539)
- Editor state is a React class component (packages/excalidraw/components/App.tsx) owning Scene, Renderer, Store, History, ActionManager, Library, and binary files. Two Jotai stores exist (editor-jotai inside the package, app-jotai in the host). ESLint forbids importing jotai directly. — [packages/excalidraw/components/App.tsx#L638-949](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L638-L949) [.eslintrc.json#L29-34](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.eslintrc.json#L29-L34) [excalidraw-app/App.tsx#L83-89](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L83-L89)
- Font subsetting runs off the main thread through WorkerPool in packages/excalidraw/workers.ts, which refuses to construct a worker whose URL equals import.meta.url (WorkerInTheMainChunkError). WASM assets for harfbuzz and woff2 live under scripts/wasm. — [packages/excalidraw/workers.ts#L23-59](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/workers.ts#L23-L59) [packages/excalidraw/subset/subset-main.ts](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/subset/subset-main.ts)
- scripts/release.js publishes packages common, fractional-indexing, math, element, and excalidraw (utils is explicitly skipped). Tags are test (default), next, or latest; latest requires --version and updates the changelog via child_process execSync. — [scripts/release.js#L1-49](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/scripts/release.js#L1-L49)

## Feature catalog

The editor is a tool-driven canvas plus a large ActionName command set (clipboard, z-order, align, frames, text binding, zoom, theme, export). The host app layers collaboration rooms, encrypted share links, local autosave, PWA install, Excalidraw+ chrome, and two AI surfaces (text-to-diagram and diagram-to-code). Embedders get the same canvas through props, the imperative API, and the Next.js and browser-script examples.
- Pointer dispatch in handleCanvasPointerDown branches on activeTool.type: text, arrow/line, freedraw, custom, frame/magicframe, laser, autoshape, bucketfill, eraser, and a generic creator for rectangle-like tools. Element types include rectangle, diamond, ellipse, stickynote (baseHeight), embeddable, plus linear, image, text, and frame types in packages/element/src/types.ts. — [packages/excalidraw/components/App.tsx#L8927-9019](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L8927-L9019) [packages/element/src/types.ts#L40-118](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/types.ts#L40-L118)
- ActionName enumerates the command surface: copy/cut/paste, copyAsPng/copyAsSvg, z-index, align and distribute, flip, group, undo/redo, font and stroke properties, grid/zen/view/stats modes, hyperlink, cropEditor, frame membership, lock, linear editor, searchMenu, and commandPalette. Each action module calls register() so the side-effect import in App.tsx fills the actions array before ActionManager.registerAll. — [packages/excalidraw/actions/types.ts#L45-144](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/types.ts#L45-L144) [packages/excalidraw/actions/register.ts#L1-15](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/register.ts#L1-L15) [packages/excalidraw/actions/actionDeleteSelected.tsx#L208](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/actionDeleteSelected.tsx#L208) [packages/excalidraw/components/App.tsx#L301-348](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L301-L348)
- History is delta-based. Store emits durable increments for CaptureUpdateAction.IMMEDIATELY, never records NEVER (remote updates and init), and defers EVENTUALLY. History.record consumes those deltas; undo/redo are actions created with createUndoAction/createRedoAction and registered beside the static action list. History deltas deliberately drop version and versionNonce so a local undo still looks like a new collab edit. — [packages/element/src/store.ts#L38-69](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/store.ts#L38-L69) [packages/excalidraw/history.ts#L15-96](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/history.ts#L15-L96) [packages/excalidraw/components/App.tsx#L941-943](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L941-L943) [packages/excalidraw/components/App.tsx#L3848-3850](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3848-L3850)
- Export to disk uses serializeAsJSON, which writes type excalidraw, VERSIONS.excalidraw (2), source, elements, and appState. type local keeps files (minus those only referenced by deleted elements); type database sets files undefined so blobs are stored elsewhere. saveAsJSON uses the File System Access wrappers in data/filesystem.ts (browser-fs-access). PNG/SVG/clipboard exports are re-exported from @excalidraw/utils/export. — [packages/excalidraw/data/json.ts#L52-90](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/json.ts#L52-L90) [packages/common/src/constants.ts#L397-402](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/common/src/constants.ts#L397-L402) [packages/excalidraw/data/filesystem.ts#L1-39](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/filesystem.ts#L1-L39) [packages/utils/src/export.ts#L1-18](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/utils/src/export.ts#L1-L18) [dev-docs/docs/codebase/json-schema.mdx#L1-20](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/dev-docs/docs/codebase/json-schema.mdx#L1-L20)
- Shape libraries persist through LibraryPersistenceAdapter (load/save). The app implements LibraryIndexedDBAdapter (idb-keyval store excalidraw-library) and LibraryLocalStorageMigrationAdapter for the legacy localStorage key. Remote library installs are restricted to hostnames excalidraw.com and raw.githubusercontent.com/excalidraw/excalidraw-libraries. — [packages/excalidraw/data/library.ts#L54-95](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/library.ts#L54-L95) [excalidraw-app/data/LocalData.ts#L229-277](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/LocalData.ts#L229-L277)
- i18n defaults to en from packages/excalidraw/locales/en.json. A locale is listed only if Crowdin coverage meets COMPLETION_THRESHOLD 85 (percentages.json). RTL is flagged for ar-SA, fa-IR, and he-IL. Crowdin syncs en.json to locales/%locale%.json. Vite manualChunks splits non-English locale JSON so the PWA precache can cache them separately. — [packages/excalidraw/i18n.ts#L1-40](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/i18n.ts#L1-L40) [crowdin.yml#L1-2](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/crowdin.yml#L1-L2) [excalidraw-app/vite.config.mts#L106-118](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/vite.config.mts#L106-L118)
- Live collaboration is app-only. ShareDialog calls collabAPI.startCollaboration(null) to create a room or stopCollaboration to leave. Links match /^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/ and the key must be 22 characters or the UI alerts invalidEncryptionKey. The socket emits join-room and broadcasts SCENE_INIT, SCENE_UPDATE, MOUSE_LOCATION, IDLE_STATUS, and USER_VISIBLE_SCENE_BOUNDS. — [excalidraw-app/share/ShareDialog.tsx#L186-204](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/share/ShareDialog.tsx#L186-L204) [excalidraw-app/data/index.ts#L131-164](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L131-L164) [excalidraw-app/app_constants.ts#L16-30](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L16-L30) [excalidraw-app/collab/Portal.tsx#L37-61](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Portal.tsx#L37-L61)
- Readonly share links are a different hash: #json=<id>,<key>. exportToBackend generates an AES key, compresses serializeAsJSON(..., 'database'), POSTs the bytes to VITE_APP_BACKEND_V2_POST_URL, then writes the key only into location.hash. Image payloads are encrypted and uploaded under /files/shareLinks/${id} with a 4 MiB cap (FILE_UPLOAD_MAX_BYTES). — [excalidraw-app/data/index.ts#L248-306](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L248-L306) [excalidraw-app/app_constants.ts#L11-35](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L11-L35) [excalidraw-app/App.tsx#L228-281](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L228-L281)
- Text-to-diagram lives in packages/excalidraw/components/TTDDialog. useTextGeneration accepts prompts of 3–10000 characters, streams via an AbortController, and parses mermaid with @excalidraw/mermaid-to-excalidraw. The host wires TTDIndexedDBAdapter (IDB key excalidraw-ttd-chats). Paste detection isMaybeMermaidDefinition recognizes flowchart, sequenceDiagram, C4Context, mindmap, and other mermaid headers. — [packages/excalidraw/components/TTDDialog/hooks/useTextGeneration.ts#L23-99](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/TTDDialog/hooks/useTextGeneration.ts#L23-L99) [packages/excalidraw/mermaid.ts#L1-32](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/mermaid.ts#L1-L32) [excalidraw-app/app_constants.ts#L49](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L49) [excalidraw-app/components/AI.tsx#L1-18](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/components/AI.tsx#L1-L18)
- Diagram-to-code is a host plugin. DiagramToCodePlugin calls app.setPlugins({ diagramToCode }). AIComponents exports the selected frame to JPEG, collects text with getTextFromElements, and POSTs {texts, image, theme} to ${VITE_APP_AI_BACKEND}/v1/ai/diagram-to-code/generate-streaming, parsing the SSE body and treating HTTP 429 as a rate limit. — [packages/excalidraw/components/DiagramToCodePlugin/DiagramToCodePlugin.tsx#L1-19](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/DiagramToCodePlugin/DiagramToCodePlugin.tsx#L1-L19) [excalidraw-app/components/AI.tsx#L19-80](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/components/AI.tsx#L19-L80)
- Charts (bar, line, radar) are a paste path under packages/excalidraw/charts. Command palette UI is packages/excalidraw/components/CommandPalette/CommandPalette.tsx (fuzzy match over actions); defaultCommandPaletteItems.ts is an empty module. Analytics trackEvent only emits categories command_palette, export, and ai, and only when VITE_APP_ENABLE_TRACKING is the string true and window.sa_event exists. — [packages/element/src/types.ts#L18](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/types.ts#L18) [packages/excalidraw/components/CommandPalette/CommandPalette.tsx#L1-39](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/CommandPalette/CommandPalette.tsx#L1-L39) [packages/excalidraw/components/CommandPalette/defaultCommandPaletteItems.ts#L1](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/CommandPalette/defaultCommandPaletteItems.ts#L1) [packages/excalidraw/analytics.ts#L6-40](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/analytics.ts#L6-L40)
- The hosted shell adds AppMainMenu, AppWelcomeScreen, AppSidebar, ShareDialog plus QR (uqr), language picker, Excalidraw+ promo and iframe export (VITE_APP_PLUS_APP / VITE_APP_PLUS_EXPORT_PUBLIC_KEY), and a beforeinstallprompt capture for PWA install. Collaboration is disabled when isRunningInIframe() is true. Examples with-nextjs dynamically imports the editor with ssr:false and sets window.EXCALIDRAW_ASSET_PATH. — [excalidraw-app/App.tsx#L102-150](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L102-L150) [excalidraw-app/App.tsx#L173-188](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L173-L188) [excalidraw-app/App.tsx#L375-379](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L375-L379) [examples/with-nextjs/src/app/page.tsx#L6-25](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/examples/with-nextjs/src/app/page.tsx#L6-L25) [excalidraw-app/package.json#L28-40](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/package.json#L28-L40)
- Embeddables and hyperlinks are host-gated. Excalidraw props include validateEmbeddable, renderEmbeddable, onLinkOpen, and generateLinkForSelection. SVG export leaves embeddables out unless renderEmbeddables is set. Links are passed through sanitizeUrl from @braintree/sanitize-url in packages/common/src/url.ts. — [packages/excalidraw/index.tsx#L111-112](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L111-L112) [packages/excalidraw/index.tsx#L253-254](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L253-L254) [packages/excalidraw/scene/export.ts#L477-493](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/scene/export.ts#L477-L493) [packages/common/src/url.ts#L1-10](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/common/src/url.ts#L1-L10)

## Data flow

A scene is an ordered list of elements plus AppState, with image bytes kept in a separate BinaryFiles map. Local edits pass through the Store, which both feeds undo history and the public onChange/onIncrement callbacks. The app then fans changes into localStorage, IndexedDB, and — when a room is open — an encrypted socket broadcast plus a Firebase transaction. Anything loaded back in goes through restoreElements/restoreAppState and, for multiplayer, reconcileElements on version plus versionNonce.
- Elements carry collab metadata on the base type: monotonically increasing version, random versionNonce for tie-breaks, fractional index (string brand) for z-order, updated timestamp, and isDeleted. index is kept aligned with array order by syncMovedIndices and syncInvalidIndices. — [packages/element/src/types.ts#L40-87](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/types.ts#L40-L87) [packages/element/src/Scene.ts#L15-19](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/Scene.ts#L15-L19)
- Local autosave is owned by LocalData.save, debounced by SAVE_TO_LOCAL_STORAGE_TIMEOUT (300ms). It writes non-deleted elements to localStorage key excalidraw and cleared app state to excalidraw-state, then puts image binaries in IndexedDB database files-db / store files-store via idb-keyval. QuotaExceededError flips localStorageQuotaExceededAtom. Saves are skipped when document.hidden or the collaboration locker is held. — [excalidraw-app/data/LocalData.ts#L49-165](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/LocalData.ts#L49-L165) [excalidraw-app/data/LocalData.ts#L204-226](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/LocalData.ts#L204-L226) [excalidraw-app/app_constants.ts#L1-7](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L1-L7) [excalidraw-app/app_constants.ts#L39-53](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L39-L53)
- The app onChange callback is the fan-out. If collabAPI.isCollaborating() it calls collabAPI.syncElements(elements). Otherwise, when LocalData.isSavePaused() is false, it calls LocalData.save and afterwards rewrites image elements whose status should become saved via newElementWith. — [excalidraw-app/App.tsx#L715-739](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L715-L739)
- Cross-tab sync stores timestamps under version-dataState and version-files. updateBrowserStateVersion writes Date.now() into both localStorage and an in-memory LOCAL_STATE_VERSIONS map. On focus or visibility the app calls isBrowserStorageStateNewer and, for files, LocalData.fileStorage.getFiles for image ids not already in memory. — [excalidraw-app/data/tabSync.ts#L1-39](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/tabSync.ts#L1-L39) [excalidraw-app/App.tsx#L624-670](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L624-L670)
- Socket payloads are JSON, UTF-8 encoded, then AES-GCM encrypted with the room key. Portal._broadcastSocketData emits WS_EVENTS.SERVER (server-broadcast) or SERVER_VOLATILE. broadcastScene sends only elements whose version is newer than broadcastedElementVersions unless syncAll is set, and SCENE_INIT requires syncAll. File upload is throttled at FILE_UPLOAD_TIMEOUT (300ms) and flips image status to saved so peers know to pull bytes. — [excalidraw-app/collab/Portal.tsx#L85-183](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Portal.tsx#L85-L183) [excalidraw-app/app_constants.ts#L16-21](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L16-L21) [packages/excalidraw/data/encryption.ts#L50-78](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/encryption.ts#L50-L78)
- Firebase scene documents store {sceneVersion, ciphertext: Bytes, iv: Bytes}. saveToFirebase runs a Firestore transaction on scenes/{roomId}: if the doc exists it decrypts the previous elements, reconcileElements against the local syncable set, and writes the merged ciphertext. A WeakMap FirebaseSceneVersionCache skips writes when the socket already persisted that getSceneVersion. — [excalidraw-app/data/firebase.ts#L87-116](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/firebase.ts#L87-L116) [excalidraw-app/data/firebase.ts#L174-266](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/firebase.ts#L174-L266)
- reconcileElements keeps the local element when it is being edited (editingTextElement, resizingElement, newElement), when local.version is greater, or when versions match and local.versionNonce is less than or equal to the remote nonce. Fractional indices are revalidated in dev and test (validateFractionalIndices throws); in production the check is throttled to once a minute and does not throw. — [packages/excalidraw/data/reconcile.ts#L23-77](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/reconcile.ts#L23-L77)
- Syncable elements exclude invisibly small elements. Deleted elements stay syncable only until DELETED_ELEMENT_TIMEOUT (24h) so peers can observe the deletion, then they drop out of getSyncableElements. — [excalidraw-app/data/index.ts#L46-63](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L46-L63) [excalidraw-app/app_constants.ts#L9](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L9)
- Startup scene load order in initializeScene: importFromLocalStorage, then optionally #json= via importFromBackend (decompress + decrypt, falling back to legacy_decodeFromBackend), then #url= via fetch + loadFromBlob, then #room= via collabAPI.startCollaboration. External scenes prompt with openConfirmModal unless the canvas is empty or the link is a collab room. Non-room imports then history.replaceState back to the origin. — [excalidraw-app/App.tsx#L217-373](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L217-L373) [excalidraw-app/data/index.ts#L166-241](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L166-L241)
- Share-link import tries the new compressed format first (decompressData with decryptionKey). On failure legacy_decodeFromBackend slices a 12-byte IV off the front, and if that throws it retries with a zero IV for the oldest files. — [excalidraw-app/data/index.ts#L166-236](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L166-L236) [packages/excalidraw/data/encryption.ts#L5](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/encryption.ts#L5)
- Keys are 128-bit AES-GCM (ENCRYPTION_KEY_BITS). generateEncryptionKey uses crypto.subtle.generateKey and exports the JWK k field. getCryptoKey imports alg A128GCM. IVs are 12 random bytes from crypto.getRandomValues. Room ids are 10 random bytes hex-encoded (ROOM_ID_BYTES). — [packages/excalidraw/data/encryption.ts#L5-94](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/encryption.ts#L5-L94) [packages/common/src/constants.ts#L397](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/common/src/constants.ts#L397) [excalidraw-app/data/index.ts#L68-72](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L68-L72) [excalidraw-app/app_constants.ts#L37](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L37)
- Collab image bytes and share-link image bytes use different Storage prefixes: FIREBASE_STORAGE_PREFIXES.collabFiles is /files/rooms and shareLinkFiles is /files/shareLinks. Cache-Control on upload is public, max-age=31536000. — [excalidraw-app/app_constants.ts#L13-35](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L13-L35) [excalidraw-app/data/firebase.ts#L145-171](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/firebase.ts#L145-L171) [excalidraw-app/data/index.ts#L288-291](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L288-L291)

## Behavior traces

Three flows dominate the app: boot (service worker, Sentry, scene restore), a local drawing gesture (pointer down through action finalize into the store), and collaboration (room key, socket join, encrypted broadcast, Firebase reconcile). Local persistence is a side effect of onChange and is explicitly paused for the whole time a room is open.
- App boot sequence: excalidraw-app/index.tsx imports sentry.ts (Sentry.init, DSN only when hostname contains excalidraw.com, staging.excalidraw.com, or vercel.app and VITE_APP_DISABLE_SENTRY is not true), registerSW(), then renders ExcalidrawApp. A window beforeinstallprompt listener is installed at module scope in App.tsx so the install event is not missed. — [excalidraw-app/index.tsx#L1-17](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/index.tsx#L1-L17) [excalidraw-app/sentry.ts#L5-25](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/sentry.ts#L5-L25) [excalidraw-app/App.tsx#L173-188](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L173-L188)
- Editor constructor order: getDefaultAppState, refreshEditorInterface, nanoid id, new Library, new ActionManager(syncActionResult), new Scene, offscreen canvas plus rough.canvas, new Renderer, new Store, new History, new Fonts, registerAll(actions) plus undo/redo, then createExcalidrawAPI. componentDidMount rebuilds the API, in dev/test installs window.h.{state,setState,app,history,store,fonts}, subscribes store durable increments to history.record, calls scene.onUpdate(triggerRender), and unless ?web-share-target is set calls updateDOMRect(this.initializeScene). — [packages/excalidraw/components/App.tsx#L865-949](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L865-L949) [packages/excalidraw/components/App.tsx#L3807-3902](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3807-L3902)
- Host scene init effect waits until excalidrawAPI exists (and collabAPI exists unless iframes disable collab), calls initializeScene, then applies the result. It also listens for hashchange, unload (LocalData.flushSave), and blur/visibility/focus. beforeunload flushes local save and preventUnload when file storage still has unsaved images, unless VITE_APP_DISABLE_PREVENT_UNLOAD is true. — [excalidraw-app/App.tsx#L561-712](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L561-L712)
- Drawing a generic shape: canvas onPointerDown -> handleCanvasPointerDown. For tools other than text, arrow, line, freedraw, custom, frame, laser, autoshape, bucketfill, eraser, hand, or image, it calls createGenericElementOnPointerDown(activeTool.type). It then fires props.onPointerDown and onPointerDownEmitter, and if the editor is not in a non-capturing view mode registers window pointermove, pointerup, keydown, and keyup. Later pointer-up paths call actionManager.executeAction(actionFinalize). — [packages/excalidraw/components/App.tsx#L8927-9019](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L8927-L9019) [packages/excalidraw/components/App.tsx#L4381](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L4381)
- Action execution: ActionManager.handleKeyDown filters actions with keyTest, respecting UIOptions.canvasActions and keyPriority, and refuses the key if more than one action matches. registerAction stores by ActionName. The updater passed at construction is syncActionResult, which is wrapped in withBatchedUpdates. Navigation actions are blocked while viewport.isLockedTransitionPending. — [packages/excalidraw/actions/manager.tsx#L52-119](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/manager.tsx#L52-L119) [packages/excalidraw/components/App.tsx#L917-922](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L917-L922) [packages/excalidraw/components/App.tsx#L3153](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3153)
- Starting a new room from the share dialog calls startCollaboration(null). That generates roomId and roomKey, pushState's #room=id,key, sets collaborating, and LocalData.pauseSave('collaboration'). It dynamically imports socket.io-client, opens Portal with transports websocket and polling, and for a brand-new room strips nothing but rewrites saved images to status pending and calls saveCollabRoomToFirebase. Joining an existing room calls excalidrawAPI.resetScene() first so local elements are not merged in. — [excalidraw-app/collab/Collab.tsx#L481-568](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Collab.tsx#L481-L568) [excalidraw-app/data/index.ts#L148-164](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L148-L164) [excalidraw-app/share/ShareDialog.tsx#L186-204](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/share/ShareDialog.tsx#L186-L204)
- Socket lifecycle inside Portal.open: on init-room the client emits join-room; on new-user it broadcastScene(SCENE_INIT, elements, syncAll=true); on room-user-change it calls collab.setCollaborators. Incoming client-broadcast is decrypted in Collab.decryptPayload and switched on SCENE_INIT (reconcile, handleRemoteSceneUpdate, resolve the scene promise), SCENE_UPDATE (reconcile), or MOUSE_LOCATION (updateCollaborator). A 5s INITIAL_SCENE_UPDATE_TIMEOUT falls back to initializeRoom({fetchScene:true}) which reads Firebase. — [excalidraw-app/collab/Portal.tsx#L37-61](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Portal.tsx#L37-L61) [excalidraw-app/collab/Collab.tsx#L458-619](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Collab.tsx#L458-L619) [excalidraw-app/app_constants.ts#L2](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L2)
- Stopping collaboration cancels queueBroadcastAllElements, queueSaveToFirebase, and loadImageFiles, then saveCollabRoomToFirebase of the current syncable elements. Unless keepRemoteState is false, a window.confirm asks before destroying the socket, resetBrowserStateVersions, and pushing history back to the origin. destroySocketClient closes the portal, clears collaborators, and LocalData.resumeSave('collaboration'). — [excalidraw-app/collab/Collab.tsx#L365-427](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Collab.tsx#L365-L427)
- Full-scene resync is periodic: SYNC_FULL_SCENE_INTERVAL_MS is 20000, separate from per-element version filtering in broadcastScene. Cursor broadcasts are volatile and throttled conceptually by CURSOR_SYNC_TIMEOUT 33ms. Deleted-element retention for the wire protocol is 24h. — [excalidraw-app/app_constants.ts#L5-8](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L5-L8) [excalidraw-app/collab/Portal.tsx#L142-183](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Portal.tsx#L142-L183)
- Unmount tears down the editor aggressively: it replaces get* API methods so they throw if called after isDestroyed, emits editor:unmount, destroys renderer, viewport, library, laser and eraser trails, clears both store emitters, and resets ShapeCache and SnapCache. The comment in the constructor says the API must be recreated in componentDidMount because StrictMode unmount would otherwise invalidate it. — [packages/excalidraw/components/App.tsx#L3905-3960](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3905-L3960) [packages/excalidraw/components/App.tsx#L945-949](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L945-L949)
- Web Share Target: if the query contains web-share-target, componentDidMount calls restoreFileFromShare instead of initializeScene. #url=<encoded> on the host fetches that URL, loadFromBlob's the response, and on failure sets appState.errorMessage to alerts.invalidSceneUrl. — [packages/excalidraw/components/App.tsx#L3877-3886](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3877-L3886) [excalidraw-app/App.tsx#L304-326](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L304-L326)

## Testing signals

Tests are Vitest 3 on jsdom, with Testing Library, vitest-canvas-mock, and fake-indexeddb. Coverage thresholds are enforced (lines 60, branches 70, functions 63, statements 60). The editor and element packages are heavily tested, including snapshots; the collaboration suite is a single file that mocks Firebase and socket.io and its own comment says multi-client cases are missing. CI runs unit tests on master pushes, and lint, types, prettier, and coverage on pull requests.
- vitest.config.mts sets environment jsdom, globals, setupFiles ./setupTests.ts, sequence.hooks parallel, hideSkippedTests, and coverage reporters text, json-summary, json, html, lcovonly with ignoreEmptyLines false. Thresholds: lines 60, branches 70, functions 63, statements 60. Root scripts: yarn test and yarn test:app invoke vitest; yarn test:update passes --update; yarn test:coverage and yarn test:ui exist. — [vitest.config.mts#L75-98](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/vitest.config.mts#L75-L98) [package.json#L68-77](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/package.json#L68-L77)
- setupTests.ts loads vitest-canvas-mock, @testing-library/jest-dom, the editor polyfill, fake-indexeddb/auto, a localStorage polyfill, and stubs setPointerCapture, matchMedia, and FontFace. It replaces @excalidraw/common throttleRAF with mockThrottleRAF. DOM dumps on Testing Library failures are suppressed unless VITE_DEBUG_DOM is true or 1. — [setupTests.ts#L1-80](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/setupTests.ts#L1-L80)
- Editor interaction tests live in packages/excalidraw/tests (App, history, export, clipboard, drag create, selection, laser, lasso, library, mermaid, search, wheel, regression, plus data/restore.test.ts and data/reconcile.test.ts). Element geometry tests live in packages/element/tests (binding, elbow arrows, frames, fractional index, text wrapping, z-index). Math tests are in packages/math/tests. Common tests cover colors, binary heap, keys, queue, and url. — [packages/excalidraw/tests/history.test.tsx](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/tests/history.test.tsx) [packages/excalidraw/tests/data/reconcile.test.ts](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/tests/data/reconcile.test.ts) [packages/element/tests/fractionalIndex.test.ts](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/tests/fractionalIndex.test.ts) [packages/element/tests/elbowArrow.test.tsx](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/tests/elbowArrow.test.tsx) [packages/math/tests/curve.test.ts](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/math/tests/curve.test.ts)
- App-level tests are only excalidraw-app/tests/collab.test.tsx, LanguageList.test.tsx, and MobileMenu.test.tsx (with a snapshot). collab.test renders the whole ExcalidrawApp and listens to h.store.onStoreIncrementEmitter. It mocks socket.io-client as a no-op emitter and stubs firebase load/save. The file comment says the suite should be extended to two simultaneous clients and that this is not done. — [excalidraw-app/tests/collab.test.tsx#L1-80](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/tests/collab.test.tsx#L1-L80)
- CI does not run the full yarn test:all. Push-to-master runs only yarn test:app. Pull requests run prettier (test:other), eslint --max-warnings=0 (test:code), and tsc (test:typecheck) in lint.yml, and separately yarn test:coverage with davelosert/vitest-coverage-report-action. Node is pinned to 20.x in those workflows while package.json engines say >=18 and the Docker build uses node 24. — [.github/workflows/test.yml#L1-19](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/test.yml#L1-L19) [.github/workflows/lint.yml#L1-22](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/lint.yml#L1-L22) [.github/workflows/test-coverage-pr.yml#L1-25](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/test-coverage-pr.yml#L1-L25) [package.json#L46-72](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/package.json#L46-L72) [Dockerfile#L1](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/Dockerfile#L1)
- Many editor tests snapshot rendered output under packages/excalidraw/tests/__snapshots__ (App, history, export, selection, drag, mermaid, context menu) and packages/excalidraw/components/__snapshots__/App.test.tsx.snap. yarn test:update is the documented way to refresh them (CLAUDE.md). — [CLAUDE.md#L13-24](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/CLAUDE.md#L13-L24) [packages/excalidraw/tests/__snapshots__/history.test.tsx.snap](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap) [packages/excalidraw/components/__snapshots__/App.test.tsx.snap](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/__snapshots__/App.test.tsx.snap)
- Dev and test builds expose the live editor on window.h (state, setState, app, history, store, fonts). Tests reach it as const { h } = window after render. That hook is not installed in production because componentDidMount guards it with isTestEnv() || isDevEnv(). — [packages/excalidraw/components/App.tsx#L3814-3846](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3814-L3846) [excalidraw-app/tests/collab.test.tsx#L17](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/tests/collab.test.tsx#L17)
- There are no automated tests for firebase-project rules, the Docker/nginx image, the real Socket.IO server, VITE_APP_AI_BACKEND, Sentry release upload, or the self-unregistering public/service-worker.js. Firebase and socket modules are mocked out of the only collab test. — [excalidraw-app/tests/collab.test.tsx#L30-64](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/tests/collab.test.tsx#L30-L64) [firebase-project/firestore.rules](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/firebase-project/firestore.rules) [public/service-worker.js](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/public/service-worker.js)
- tsconfig.json typechecks packages and excalidraw-app only (strict, noEmit, jsx react-jsx). examples and the top-level tests folder are excluded, so example apps are not part of yarn test:typecheck. — [tsconfig.json#L1-39](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/tsconfig.json#L1-L39)
- Fractional-index invariants are part of the test contract: reconcile.ts calls validateFractionalIndices with shouldThrow when isTestEnv() or isDevEnv(), including bound-text checks and a reconciliationContext of local versus remote elements. A bad index in a test fails the test instead of being repaired quietly. — [packages/excalidraw/data/reconcile.ts#L46-68](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/reconcile.ts#L46-L68)

## Risk map

The serious boundary is collaboration storage: Firestore and Storage rules allow public get and write, and confidentiality is entirely the AES-GCM room key that also sits in the URL hash. The editor class in App.tsx is a single very large state owner, so small input bugs have a wide blast radius. Several legacy paths (zero-IV decrypt, CRA service worker, index-node canvas script) are still in tree. Collaboration, Firebase, and the AI backends are the least tested production paths.
- firebase-project/firestore.rules allows get and write on every document if true, and explicitly disables list so collections cannot be enumerated. storage.rules allows get and write with no auth on /{files}/rooms/{room}/{file} and /{files}/shareLinks/{shareLink}/{file}. A comment in the Firestore file says list must never be enabled or anyone could delete others' drawings. Anyone who knows or guesses a room id can overwrite the ciphertext; they cannot read it without the key. — [firebase-project/firestore.rules#L1-9](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/firebase-project/firestore.rules#L1-L9) [firebase-project/storage.rules#L1-10](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/firebase-project/storage.rules#L1-L10)
- The room key and the share-link key are placed in the URL hash on purpose so they are not sent to the server (getCollaborationLink and exportToBackend comments). Sentry's beforeSend strips hash from event.request.url for the same reason. The key still leaks via screenshots, browser history, and Referrer-Policy origin (not no-referrer) if a navigation includes a hash the browser keeps locally. — [excalidraw-app/data/index.ts#L159-164](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L159-L164) [excalidraw-app/data/index.ts#L276-286](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L276-L286) [excalidraw-app/sentry.ts#L39-42](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/sentry.ts#L39-L42) [vercel.json#L16-21](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/vercel.json#L16-L21)
- Encryption is AES-GCM with a 128-bit key (A128GCM), 12-byte random IV, and Web Crypto. The legacy share decoder still accepts a 12-byte zero IV if the length-prefixed IV fails, which is a weaker historical format that remains reachable through importFromBackend. — [packages/excalidraw/data/encryption.ts#L12-94](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/encryption.ts#L12-L94) [packages/common/src/constants.ts#L397](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/common/src/constants.ts#L397) [excalidraw-app/data/index.ts#L166-188](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L166-L188)
- initializeScene will fetch an arbitrary decoded URL when location.hash matches /^#url=(.*)$/, then parse the body with loadFromBlob. That is an open client-side fetch of attacker-controlled content, followed by scene restore if the user confirms or the canvas is empty. — [excalidraw-app/App.tsx#L304-326](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L304-L326)
- isExcalidrawPlusSignedUser is document.cookie.includes('excplus-auth'). It detects cookie presence, not a verified session. ExcalidrawPlusIframeExport posts to VITE_APP_PLUS_APP and attaches VITE_APP_PLUS_EXPORT_PUBLIC_KEY. — [excalidraw-app/app_constants.ts#L55-61](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L55-L61) [excalidraw-app/ExcalidrawPlusIframeExport.tsx#L16](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/ExcalidrawPlusIframeExport.tsx#L16)
- Sentry DSN https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260 is hardcoded. It is disabled when VITE_APP_DISABLE_SENTRY=true (the Docker build sets that) or when the hostname is not in SentryEnvHostnameMap. beforeSend fabricates an exception from callsites() for console errors, and captureConsoleIntegration records level error. — [excalidraw-app/sentry.ts#L5-42](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/sentry.ts#L5-L42) [excalidraw-app/package.json#L45](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/package.json#L45) [Dockerfile#L14](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/Dockerfile#L14)
- vercel.json sets Feature-Policy to * on all routes, which is a permissive feature policy, alongside nosniff and Referrer-Policy origin. Font woff2 routes under Virgil, Cascadia, and Assistant-Regular allow Access-Control-Allow-Origin *. — [vercel.json#L4-48](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/vercel.json#L4-L48)
- packages/excalidraw/index-node.ts is a Node script that require()s fs and the canvas package, registers Virgil and Cascadia, calls exportToCanvas on three hardcoded elements, and writes test.png. It is not the browser entry (package.json main is dist/prod/index.js) and it shells into the filesystem. — [packages/excalidraw/index-node.ts#L1-80](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index-node.ts#L1-L80) [packages/excalidraw/package.json#L5-7](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/package.json#L5-L7)
- public/service-worker.js is a leftover Create-React-App worker whose activate handler unregisters itself and reloads every client. The comment says Vite now uses sw.js from vite-plugin-pwa and this file should be removed once old clients are gone. It still ships in public/. — [public/service-worker.js#L1-20](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/public/service-worker.js#L1-L20)
- QR rendering uses dangerouslySetInnerHTML={{ __html: svgData }} in excalidraw-app/share/QRCode.tsx. The SVG is produced locally by the uqr dependency, but the sink is still raw HTML injection if that string is ever attacker-controlled. — [excalidraw-app/share/QRCode.tsx#L53](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/share/QRCode.tsx#L53)
- Release and doc scripts shell out: scripts/release.js and scripts/build-version.js use child_process, scripts/buildDocs.js runs git diff, scripts/updateChangelog.js execs git. These are maintainer entrypoints, not the browser app, but they take CLI arguments such as --tag and --version. — [scripts/release.js#L1-36](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/scripts/release.js#L1-L36) [scripts/buildDocs.js](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/scripts/buildDocs.js)
- The editor App class is the main blast-radius file: constructor, pointer handling, keyboard, export, and unmount all live there, with methods past line 11000 (actionFinalize calls around 10134–11687). Store.scheduleCapture is commented as suspicious because it is called from many sites. A mistaken CaptureUpdateAction.IMMEDIATELY on a remote update would pollute undo; NEVER on a local edit would drop it. — [packages/excalidraw/components/App.tsx#L638-949](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L638-L949) [packages/excalidraw/components/App.tsx#L10134](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L10134) [packages/element/src/store.ts#L106-112](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/store.ts#L106-L112)
- Library installs only allow excalidraw.com and the excalidraw-libraries GitHub raw host, but collab image and scene writes are not similarly origin-locked. Firebase config JSON parse failure only console.warns and continues with {}. A missing or malformed VITE_APP_FIREBASE_CONFIG therefore fails later, at first collab save, rather than at startup. — [packages/excalidraw/data/library.ts#L54-58](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/library.ts#L54-L58) [excalidraw-app/data/firebase.ts#L44-54](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/firebase.ts#L44-L54)
- window.h in dev and test exposes setState and the store. Combined with StrictMode double-mount, calling the imperative API after unmount throws a specific destroyed-API error. Hosts that cache the first onExcalidrawAPI value across StrictMode will hit that. — [packages/excalidraw/components/App.tsx#L3814-3922](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3814-L3922)

## Extension points

New editor commands are actions registered through register() and ActionManager, or at runtime via the imperative registerAction. Hosts extend rendering and behavior through Excalidraw props (UI slots, embeddables, tools, initial data) rather than forks. Persistence of libraries is an adapter interface. AI diagram-to-code is a setPlugins hook. Locale files and the library URL allowlist are data extension points.
- register(action) in packages/excalidraw/actions/register.ts appends to a module-level actions array and returns the action. Action files such as actionDeleteSelected.tsx call it at export time. App.tsx imports those modules for their side effects, then actionManager.registerAll(actions) plus undo/redo. The public API also exposes api.registerAction so hosts can add actions after mount. — [packages/excalidraw/actions/register.ts#L1-15](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/register.ts#L1-L15) [packages/excalidraw/actions/actionDeleteSelected.tsx#L34-35](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/actionDeleteSelected.tsx#L34-L35) [packages/excalidraw/components/App.tsx#L841-843](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L841-L843) [packages/excalidraw/components/App.tsx#L941-943](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L941-L943)
- Action objects carry name (ActionName union), perform, keyTest, keyPriority, PanelComponent, trackEvent, and an optional navigation flag. Adding a command requires extending the ActionName union in actions/types.ts or TypeScript will reject the register call. Key collisions are not merged: handleKeyDown warns and cancels when more than one keyTest matches. — [packages/excalidraw/actions/types.ts#L17-44](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/types.ts#L17-L44) [packages/excalidraw/actions/types.ts#L45-144](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/types.ts#L45-L144) [packages/excalidraw/actions/manager.tsx#L92-119](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/actions/manager.tsx#L92-L119)
- Host integration props on Excalidraw include onChange, onIncrement, onPointerDown/Up, onPaste, onDuplicate, onLibraryChange, onLinkOpen, onScrollChange, onUserFollow, renderTopLeftUI, renderTopRightUI, renderCustomStats, renderEmbeddable, validateEmbeddable, renderScrollbars, initialData, UIOptions, activeTool, interaction, and aiEnabled. aiEnabled defaults to on unless passed false. — [packages/excalidraw/index.tsx#L67-120](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L67-L120) [packages/excalidraw/index.tsx#L207-260](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L207-L260)
- interaction and ui config objects are compared semantically inside areEqual so hosts can inline them. Supported interaction.enabled keys include links, embeds, interactiveContent, navigation, browserZoom, and tools.laser / tools.custom. When interaction is false or a partial object, isInteractionEnabled returns false and the editor forces viewModeEnabled. — [packages/excalidraw/index.tsx#L306-347](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L306-L347) [packages/excalidraw/components/App.tsx#L960-964](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L960-L964) [packages/excalidraw/components/App.tsx#L896-902](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L896-L902)
- Custom tools use activeTool.type === 'custom' (plus customType in the memo comparator). The pointer-down branch only applies the cursor; the host is expected to subscribe through onPointerDown. props.activeTool cannot force the image tool — the constructor console.warns and ignores it. — [packages/excalidraw/index.tsx#L294-300](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L294-L300) [packages/excalidraw/components/App.tsx#L880-889](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L880-L889) [packages/excalidraw/components/App.tsx#L8944-8946](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L8944-L8946)
- LibraryPersistenceAdapter requires load({source}) and save(libraryData). The editor also has LibraryMigrationAdapter for one-shot legacy reads. useHandleLibrary and parseLibraryTokensFromUrl are public exports for hosts that supply library tokens on the URL. — [packages/excalidraw/data/library.ts#L76-100](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/library.ts#L76-L100) [packages/excalidraw/index.tsx#L460-462](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L460-L462)
- DiagramToCodePlugin is the AI extension slot: it calls app.setPlugins({ diagramToCode: { generate } }) in a layout effect. The package does not implement the network call; excalidraw-app/components/AI.tsx supplies generate. TTD chat storage is a separate adapter (TTDIndexedDBAdapter) rather than this plugin map. — [packages/excalidraw/components/DiagramToCodePlugin/DiagramToCodePlugin.tsx#L7-18](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/DiagramToCodePlugin/DiagramToCodePlugin.tsx#L7-L18) [excalidraw-app/components/AI.tsx#L19-28](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/components/AI.tsx#L19-L28)
- Imperative ExcalidrawImperativeAPI methods created in createExcalidrawAPI include updateScene, applyDeltas, mutateElement, updateLibrary, addFiles, resetScene, setViewport, setElementRenderOverrides, setActiveTool, toggleSidebar, updateFrameRendering, onChange, onIncrement, onPointerDown, onPointerUp, onScrollChange, onUserFollow, onStateChange, and onEvent. history on the API currently only exposes clear. — [packages/excalidraw/components/App.tsx#L819-862](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L819-L862)
- CaptureUpdateAction is the extension contract for programmatic scene writes. IMMEDIATELY pushes undo, NEVER is required for remote or init updates, EVENTUALLY batches multi-step async edits. It is re-exported from the package entry so hosts can pass it to updateScene. — [packages/element/src/store.ts#L38-69](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/store.ts#L38-L69) [packages/excalidraw/index.tsx#L460](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L460)
- Feature flags are a localStorage map under excalidraw-feature-flags, read by getFeatureFlag and written by setFeatureFlag. The only flag today is COMPLEX_BINDINGS, default false. Pointer-up consults it to decide whether to reset delayed bind mode. Sentry also calls getFeatureFlag and enables featureFlagsIntegration. — [packages/common/src/utils.ts#L1184-1226](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/common/src/utils.ts#L1184-L1226) [packages/excalidraw/components/App.tsx#L9031-9033](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L9031-L9033) [excalidraw-app/sentry.ts#L1](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/sentry.ts#L1)
- New locales are a data plug-in: add packages/excalidraw/locales/<code>.json (Crowdin writes these from en.json) and an entry in the languages array in i18n.ts, including rtl when needed. Vite already chunks every locale file except en.json and percentages.json. — [packages/excalidraw/i18n.ts#L19-40](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/i18n.ts#L19-L40) [crowdin.yml#L1-2](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/crowdin.yml#L1-L2) [excalidraw-app/vite.config.mts#L106-118](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/vite.config.mts#L106-L118)
- WorkerPool.create(workerUrl) is the hook for short-lived module workers (font subsetting). Callers must pass a URL that is not the main bundle. Idle workers are terminated after ttl milliseconds, default 1000, and the pool does not cap concurrency — the comment says the caller must. — [packages/excalidraw/workers.ts#L19-59](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/workers.ts#L19-L59)
- UI composition components are exported for hosts: Sidebar, Button, Footer, MainMenu, WelcomeScreen, LiveCollaborationTrigger, Stats, plus useExcalidrawAPI and ExcalidrawAPIProvider so the API can be read outside the editor tree. Tunnels for slot rendering live in packages/excalidraw/context/tunnels.ts. — [packages/excalidraw/index.tsx#L52-65](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L52-L65) [packages/excalidraw/index.tsx#L464-477](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L464-L477) [packages/excalidraw/context/tunnels.ts](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/context/tunnels.ts)
- Examples are the supported embed templates: examples/with-nextjs (App Router page plus a Pages Router route, client-only dynamic import) and examples/with-script-in-browser (Vite, ExampleApp, CustomFooter). yarn start:example builds packages then starts the script example. dev-docs is a separate Docusaurus site and is not a yarn workspace of the root package.json. — [examples/with-nextjs/src/app/page.tsx#L6-25](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/examples/with-nextjs/src/app/page.tsx#L6-L25) [package.json#L4-8](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/package.json#L4-L8) [package.json#L67](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/package.json#L67)

## Contributor handoff

Work in packages/* for editor behavior and excalidraw-app/ for collab, Firebase, share links, and PWA. Read AGENTS.md before touching DOM or types. Run yarn test:typecheck and the relevant vitest file; refresh snapshots with yarn test:update only when the assertion change is intentional. Do not import jotai directly, do not bump element version by hand, and do not assume the collab socket server or Firebase rules are covered by tests.
- AGENTS.md requires new DOM access to go through app.ownerDocument and app.ownerWindow (or the mounted node's ownerDocument and defaultView), not global document/window. Type overrides must use Merge<Base, Overrides> from @excalidraw/common/utility-types rather than Omit intersections. The App class already exposes ownerDocument and ownerWindow getters that follow that rule. — [AGENTS.md#L1-4](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/AGENTS.md#L1-L4) [packages/excalidraw/components/App.tsx#L652-663](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L652-L663)
- ESLint rule no-restricted-imports errors on importing 'jotai'. Use packages/excalidraw/editor-jotai.ts inside the editor and excalidraw-app/app-jotai.ts in the app. import/order is warn and wants a newline between groups, with @excalidraw/** treated as external. — [.eslintrc.json#L3-35](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.eslintrc.json#L3-L35)
- Safe first reads for an editor change: packages/excalidraw/components/App.tsx (constructor and the specific handler), packages/excalidraw/actions/types.ts plus the action file, packages/element/src/types.ts, and packages/element/src/store.ts. For an app change: excalidraw-app/App.tsx initializeScene and onChange, excalidraw-app/collab/Collab.tsx, excalidraw-app/data/firebase.ts, and excalidraw-app/app_constants.ts. — [packages/excalidraw/components/App.tsx#L865-949](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L865-L949) [excalidraw-app/App.tsx#L217-373](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L217-L373) [excalidraw-app/collab/Collab.tsx#L481-568](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Collab.tsx#L481-L568) [excalidraw-app/app_constants.ts#L1-63](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/app_constants.ts#L1-L63)
- Before committing, CLAUDE.md says yarn test:update (vitest with snapshot update, watch off) and yarn test:typecheck (tsc). yarn fix runs prettier then eslint --fix. yarn test:all is the full local gate but CI splits it: PRs do not run test:app unless the coverage workflow does. Prefer a targeted vitest path while iterating. — [CLAUDE.md#L13-24](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/CLAUDE.md#L13-L24) [package.json#L68-80](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/package.json#L68-L80) [.github/workflows/lint.yml#L17-22](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.github/workflows/lint.yml#L17-L22)
- Do not hand-edit version or versionNonce. mutateElement / newElementWith / bumpVersion own them. History intentionally excludes those fields when applying undo so the result still collabs. Remote scene writes must pass CaptureUpdateAction.NEVER or they enter the local undo stack. — [packages/element/src/types.ts#L57-65](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/types.ts#L57-L65) [packages/excalidraw/history.ts#L27-34](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/history.ts#L27-L34) [packages/element/src/store.ts#L38-56](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/store.ts#L38-L56) [packages/excalidraw/index.tsx#L454-460](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/index.tsx#L454-L460)
- LocalData.save is paused for the entire collaboration session (pauseSave('collaboration') in startCollaboration, resumeSave in destroySocketClient). A change that writes localStorage during collab will fight that locker. isSavePaused is also true while document.hidden, and flushSave is what unload and blur call. — [excalidraw-app/collab/Collab.tsx#L515-516](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Collab.tsx#L515-L516) [excalidraw-app/collab/Collab.tsx#L413-427](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/collab/Collab.tsx#L413-L427) [excalidraw-app/data/LocalData.ts#L136-164](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/LocalData.ts#L136-L164) [excalidraw-app/App.tsx#L656-692](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/App.tsx#L656-L692)
- Fractional index bugs throw in dev and test (validateFractionalIndices shouldThrow). If a change reorders elements, it must go through syncMovedIndices or syncInvalidIndices. Collab reconcile also checks bound text. Do not sort the elements array ad hoc. — [packages/excalidraw/data/reconcile.ts#L46-68](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/reconcile.ts#L46-L68) [packages/element/src/types.ts#L65-68](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/types.ts#L65-L68) [packages/element/src/Scene.ts#L15-19](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/element/src/Scene.ts#L15-L19)
- StrictMode is on in excalidraw-app/index.tsx. The editor comments that the imperative API is invalidated on unmount and must be recreated in componentDidMount. After unmount, get* methods throw if isDestroyed is not checked. Tests and dev code should use window.h.app rather than a stale API reference. — [excalidraw-app/index.tsx#L13-16](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/index.tsx#L13-L16) [packages/excalidraw/components/App.tsx#L945-949](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L945-L949) [packages/excalidraw/components/App.tsx#L3905-3922](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L3905-L3922)
- Collab and Firebase are easy to break without a failing test. excalidraw-app/tests/collab.test.tsx mocks both firebase.ts and socket.io-client. There is no in-repo server. Manual checks need VITE_APP_WS_SERVER_URL, VITE_APP_FIREBASE_CONFIG, and the backend V2 URLs from vite-env.d.ts. Docker builds set VITE_APP_DISABLE_SENTRY=true so container smoke tests will not report errors. — [excalidraw-app/tests/collab.test.tsx#L30-64](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/tests/collab.test.tsx#L30-L64) [excalidraw-app/vite-env.d.ts#L8-18](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/vite-env.d.ts#L8-L18) [Dockerfile#L12-14](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/Dockerfile#L12-L14)
- Package builds are esbuild via yarn build:esm (scripts/buildPackage.js) and must be run before the browser-script example. The app and tests alias source, so you usually do not need a package build to iterate inside the monorepo. Publishing is scripts/release.js; do not bump package.json versions by hand unless you are cutting a release. — [packages/excalidraw/package.json#L72-75](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/package.json#L72-L75) [package.json#L51-67](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/package.json#L51-L67) [scripts/release.js#L8-28](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/scripts/release.js#L8-L28)
- Known traps: image cannot be set via props.activeTool; #room= keys must be length 22; database JSON strips files so images must be saved separately; clearAppStateForLocalStorage drops transient UI such as the canvas search sidebar; opening the search tab is not persisted. Feature flag COMPLEX_BINDINGS defaults off and lives only in localStorage. — [packages/excalidraw/components/App.tsx#L880-883](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/components/App.tsx#L880-L883) [excalidraw-app/data/index.ts#L138-145](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/index.ts#L138-L145) [packages/excalidraw/data/json.ts#L63-71](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/excalidraw/data/json.ts#L63-L71) [excalidraw-app/data/LocalData.ts#L81-88](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/data/LocalData.ts#L81-L88) [packages/common/src/utils.ts#L1184-1191](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/packages/common/src/utils.ts#L1184-L1191)
- Type imports must use import type (verbatim style, separate type imports) or eslint errors. The repo is TypeScript strict. When overriding a field, follow AGENTS.md and use Merge. Tests that render the editor should go through packages/excalidraw/tests/test-utils and helpers/api.ts (the collab test imports API, act, render, and waitFor from there). — [.eslintrc.json#L21-27](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/.eslintrc.json#L21-L27) [tsconfig.json#L11](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/tsconfig.json#L11) [AGENTS.md#L4](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/AGENTS.md#L4) [excalidraw-app/tests/collab.test.tsx#L7-8](https://github.com/excalidraw/excalidraw/blob/31df3e6ef245c646b18a055ccf858cd6117c7768/excalidraw-app/tests/collab.test.tsx#L7-L8)

Source repository: https://github.com/excalidraw/excalidraw
Full evidence corpus: https://github.com/Codevetter/codevetter/tree/main/benchmarks/repo-unpacks/excalidraw.json

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
