Excalidraw solves the friction of creating diagrams that look hand-drawn without requiring design tools. It provides an infinite canvas with a sketch-like rendering style, supporting arrows, shapes, freehand drawing, and export to PNG/SVG. The core pain point is building a customizable whiteboard component that can be embedded in other applications rather than building one from scratch.
What This Does
This is a fork of the popular excalidraw/excalidraw repository (130k+ stars). It contains the core @excalidraw/excalidraw npm package in packages/, a full-featured whiteboard app in excalidraw-app/, and supporting projects. The core package exports a React component that renders the editor, handles drawing state, and supports features like undo/redo, zoom, and shape libraries.
The excalidraw-app/ directory is a PWA with real-time collaboration, end-to-end encryption, and local-first autosave. The repo also includes dev-docs/ (Docusaurus-based documentation), examples/ (integration demos with Next.js and script tags), and scripts/ for build tooling. TypeScript dominates at 312 files plus 290 TSX files.
How It Is Wired
The entry point for the npm package is packages/excalidraw/index.tsx, which exports the main Excalidraw component. The component tree flows through packages/excalidraw/components/ where icons.tsx (88 importers) provides UI icons, and i18n.tsx (91 importers) handles localization. The types.ts file is the most connected module with 122 modules importing it and sits inside a circular dependency cycle.
The measured import graph shows 621 internal modules with 1,955 import edges and 83 modules in circular dependencies. The most unstable module is packages/element/src/index.ts (instability 0.96), which exports 47 modules but is only imported by 2. This means changes to element types ripple outward with high blast radius.
The app boots via the React component, which initializes the canvas, loads initial data from props, and manages state through the actions system in packages/excalidraw/actions/index.ts. This module has 16 importers and 31 imports, making it a central dispatch point for user interactions. The types.ts hub module carries the highest risk: 122 modules depend on it, so any type change forces recompilation across the codebase.
How To Use It
# Clone and install
git clone https://github.com/moses-y/excalidraw
cd excalidraw
yarn install
# Run the dev server
yarn start
The README documents installing the npm package with npm install @excalidraw/excalidraw for embedding into your own app. The repo uses yarn as the package manager, confirmed by yarn.lock files. Docker support exists via Dockerfile and docker-compose.yml.
Real-World Use
A typical integration embeds the editor in a product:
Critical - Committed credentials in .env.development - contains a Google API key pattern and a generated VITE_APP_PLUS_EXPORT_PUBLIC_KEY. Rotate immediately, remove from git history, and add a pre-commit secret gate.
High - Tracked .env files (.env.development, .env.production, .env.test) - the app loads these at boot, so the values are live. Move to .env.example and add to .gitignore.
High - 36 modules in circular import dependencies - types.ts, test-utils.ts, and helpers/api.ts are the main offenders. Breaking these cycles requires extracting shared types.
High - Oversized files - types.ts at 846 lines and element/src/bounds.ts are hard to maintain.
Medium - CI uses yarn install without --immutable, risking untested dependency sets. No Dependabot configured. A 20.7MB font blob (Xiaolai-Regular.ttf) should move to LFS.
The Bottom Line
This is a solid, well-tested codebase (156 test files) with a mature architecture. The circular dependencies and oversized files are maintenance risks, but the critical issue is the committed credentials—address those before anything else. Use it if you need an embeddable diagram editor; the npm package is production-ready, but this fork requires credential cleanup before deployment.
What the analyser found
Deployment readiness
6/7
✓Container image
✓CI pipeline
✓Lockfile committed
✓Test suite
✓README
✓License
✗No committed secrets
Composition
1,229 files
TypeScript312
TSX290
JSON93
SCSS82
JavaScript25
Markdown16
ReactNext.jsDocker
Architecture
Top-level areas of the codebase, sized by module count. Arrows show how many imports cross from one area into another.
Ranked by severity × confidence × production reach. Reach is the honest discriminator across a collection that is mostly other people's code: the same finding matters more in something that ships.
critical2
high1
medium8
low1
Rotate the credentials in the committed environment filecritical
.env.development
VITE_APP_PLUS_EXPORT_PUBLIC_KEY hold generated values
These values are loaded by the application at boot, so a generated value here is a working credential for something that runs.
Fix: Rotate each named credential, remove the file from the index, and replace it with a keys-only example.
Revoke the provider token committed in this filecritical
.env.development
Google API key pattern matched
These formats are machine-verifiable, so committed ones are harvested by automated scrapers within minutes of the push.
Fix: Revoke at the provider, purge from history, and add the pattern to a pre-commit hook.
Remove the committed .env and rotate what it holdshigh3 occurrences
.env.development
.env.development, .env.production, .env.test
A tracked .env is the most common route for a working key to reach a public clone, and it is the file the app actually loads, so the value is usually live.
Fix: git rm --cached the file, add it to .gitignore, rotate every credential it names, commit a .env.example with empty values.
Declare least-privilege permissions for GITHUB_TOKENmedium4 occurrences
.github/workflows/autorelease-excalidraw.yml
4 workflow(s) declare no permissions, 2 of them reference secrets
With no declaration the token inherits the repository default, so any injected step can push commits or mint releases from inside your own CI.
Fix: Add permissions: contents: read at the top of the workflow and widen per job only where needed.
Enable Dependabot or Renovatemedium
9 manifest(s), no update bot configured
Without a bot a published advisory sits unpatched until someone audits by hand, which across 1,322 repositories means never.
Fix: Commit .github/dependabot.yml covering the repo ecosystems plus github-actions.
Install from the lockfile in CImedium
.github/workflows
yarn install without --immutable
A fresh resolution in CI means the tested dependency set is not the locked one, so the failure only appears after merge.
Fix: Use npm ci, yarn install --immutable, or pnpm install --frozen-lockfile.
Pin the container base image by digestmedium
.codesandbox/Dockerfile
node:24-bullseye
An untagged or mutable base means today's build and last month's contain different libc and a different CVE set, with no record of which shipped.
Fix: Use image:tag@sha256:<digest> and enable Dependabot's docker ecosystem.
Gate pull requests on a dependency vulnerability scanmedium
.github/workflows
no dependency scan in CI
This is the one gate that would catch a known-vulnerable package before it reaches a build, and no repository in the sample had it.
Fix: Add dependency-review-action on pull_request, or osv-scanner on push and a schedule.
Add a pre-commit secret gatemedium
a secret-shaped file is tracked and no repo-level gate is visible
Without a gate the same class of leak recurs on the next commit, so a leak finding is the symptom and this is the cause.
Fix: Add a pre-commit hook scanning staged content, and enable push protection.
Move large binaries to Git LFS or out of the repositorymedium
scripts/woff2/assets/Xiaolai-Regular.ttf
1 blobs over 5MB: Xiaolai-Regular.ttf 20.7MB
One repository here carries twenty blobs over 5MB including a 10.9MB spreadsheet, so every clone and every CI checkout pays for data nobody diffs.
Fix: Track those extensions with LFS, or move datasets to object storage and fetch them in a setup step.
Set persist-credentials: false on checkoutmedium
.github/workflows/autorelease-excalidraw.yml
checkout keeps the token, then dependencies are installed
The token stays in .git/config for every later step, so a malicious postinstall script reads a pushable credential without one ever being passed to it.
Fix: Add with: persist-credentials: false, and pass an explicit token only to the step that pushes.
Set timeout-minutes on the workflow jobslow4 occurrences
.github/workflows/autorelease-excalidraw.yml
4 workflow(s) declare no job timeout
A wedged step runs to the six-hour platform default, which on a two-hourly schedule means three runs overlap behind it.
Fix: Add timeout-minutes with a realistic bound to each job.
Checked deterministically against the repository tree and a bounded set of its files: committed credentials, unpinned actions and base images, missing lockfiles and update bots, workflows that discard failures, published advisories against the declared dependencies, runtime configuration, licensing and notebook reproducibility. No language model is involved in this section.