A technical walkthrough of the open-source, browser-based creative coding IDE for p5.js.
The p5.js Web Editor is an open-source, browser-based creative coding environment designed around one core philosophy: making code accessible to everyone — artists, designers, educators, and absolute beginners — with zero setup required. Built by the Processing Foundation and a global community of volunteers, it allows users to write, run, share, and remix p5.js sketches directly in the browser.
From a software engineering perspective, it is a surprisingly sophisticated full-stack application. The codebase clocks in at over 100,000 lines of code, spans more than 7,100 commits, and has been actively developed for over a decade. It handles user authentication, real-time sketch execution in a sandboxed iframe, file management, cloud asset uploads, multi-language support, and a deeply customized code editing experience — all within a single unified monorepo.
This post is a thorough technical walkthrough of the entire system: its architecture, every major subsystem, the key design decisions made, and the ongoing evolution of the codebase.
The p5.js Web Editor is a MERN stack application:
At a high level, the application works like this:
Browser (React SPA)
│
│ HTTP / REST API calls
▼
Express.js Server (Node.js)
│
├──► MongoDB (via Mongoose) — users, sketches, files
└──► AWS S3 — binary assets (images, audio, video)
The client is a
Single Page Application (SPA) with
server-side rendering support. The server exposes a REST API that the
client consumes. When a user runs a sketch, the execution happens entirely
in the browser inside a sandboxed
<iframe>
— no sketch code ever runs on the server. This is a critical architectural
decision that enables real-time, zero-latency preview while keeping server
costs low.
The repository follows a clean monorepo structure. Every major concern has its own top-level directory:
p5.js-web-editor/
│
├── client/ # React/Redux frontend application
├── server/ # Express.js backend
├── common/ # Shared code between client and server
├── public/ # Static public assets served directly
├── translations/ # i18n locale JSON files
├── webpack/ # Webpack configuration files
├── infrastructure/ # Terraform / infrastructure-as-code
├── contributor_docs/ # Documentation for contributors
├── .storybook/ # Storybook configuration for component development
│
├── index.js # Application entry point
├── Dockerfile # Production Docker image definition
├── docker-compose.yml # Production Docker Compose configuration
├── docker-compose-development.yml # Development Docker Compose
├── kubernetes_app.yml # Kubernetes deployment manifest
├── deploy.sh # Production deployment script
├── deploy_staging.sh # Staging deployment script
├── .travis.yml # Travis CI pipeline configuration
├── .babelrc # Babel transpilation configuration
├── .eslintrc # ESLint rules
├── .prettierrc # Prettier formatting rules
├── tsconfig.json # Root TypeScript configuration
└── tsconfig.base.json # Base TypeScript configuration
The separation of
client/,
server/, and
common/
is a deliberate architectural choice that enforces clear boundaries
between concerns and allows shared TypeScript types to live in a neutral
space.
The application entry point is
index.js
at the root, which bootstraps the Express server defined in
server/. The server is a classic Express application that wires together
middleware, routes, database connections, and authentication strategies.
Key server-side dependencies include:
During development, the server runs via
nodemon
as configured in
nodemon.json, watching for file changes in both
server/
and
index.js. In production, the process is managed within a Docker container.
Authentication is handled by
Passport.js with the configuration
living in
server/config/passport.js. The application supports three authentication strategies:
1. Local Strategy (username/password)
The local strategy uses
passport-local
and validates credentials against the MongoDB
User
document. Passwords are hashed using
bcrypt
before storage. The local strategy supports login by both username and
email address.
2. GitHub OAuth Strategy The GitHub
OAuth integration uses
passport-github. When a user clicks "Log in with GitHub," they are redirected
to GitHub's OAuth authorization page. After approval, GitHub redirects
back with an authorization code, which the server exchanges for an access
token and fetches the user's profile. The server either finds an
existing user linked to that GitHub ID or creates a new account.
3. Session-Based Authentication After
login, the user's ID is serialized into the session (stored in MongoDB
via
connect-mongo). On subsequent requests,
passport.deserializeUser
rehydrates the user object from the database. The session secret is
configured via the
SESSION_SECRET
environment variable.
For API access, the editor also supports API Key authentication — users can generate API keys in their account settings to authenticate programmatic API calls without a browser session.
All API routes are mounted under the
/api
path prefix. The routes are organized by resource type inside
server/routes/:
server/routes/
├── session.routes.js # Login, logout, session info
├── user.routes.js # User CRUD, password reset, email verification
├── project.routes.js # Sketch (project) CRUD
├── files.routes.js # File upload and management within sketches
├── collection.routes.js # Collections of sketches
├── assets.routes.js # Binary asset management (S3)
└── index.js # Route aggregator
Notable API endpoints include:
| Method | Path | Description |
|---|---|---|
POST
|
/api/session
|
Log in |
DELETE
|
/api/session
|
Log out |
GET
|
/api/session
|
Get current user |
POST
|
/api/signup
|
Register a new account |
GET
|
/api/:username/projects
|
List a user's sketches |
POST
|
/api/projects
|
Create a new sketch |
GET
|
/api/projects/:id
|
Get a sketch by ID |
PUT
|
/api/projects/:id
|
Update a sketch |
DELETE
|
/api/projects/:id
|
Delete a sketch |
POST
|
/api/projects/:id/uploads
|
Upload a file asset to a sketch |
GET
|
/api/:username/collections
|
List a user's collections |
POST
|
/api/reset-password
|
Send a password reset email |
Each route module follows the MVC pattern: routes define the URL mapping and call controller functions; controller functions contain business logic and interact with Mongoose models.
MongoDB is used as the primary datastore, with
Mongoose providing schema definition
and ODM functionality. The core models live in
server/models/:
User Model (user.js)
The most complex model in the application. It stores user credentials,
OAuth linkages, preferences, and API keys. Key fields:
username
(unique, required)
email
(unique, required, stored lowercase)
password
(bcrypt-hashed, virtual setter)
github.id,
github.username,
github.token
— GitHub OAuth linkage
verified
— whether email address has been verified
verifiedToken,
verifiedTokenExpires
— email verification flow
resetPasswordToken,
resetPasswordExpires
— password reset flow
preferences
— nested object with editor theme, font size, line numbers toggle,
autosave settings
apiKeys
— array of hashed API keys with labels and creation dates
totalSize
— computed total storage used by the user's assets
The User model includes a Mongoose pre-save hook that automatically hashes the password with bcrypt whenever the password field is modified, keeping that logic encapsulated in the model.
Project (Sketch) Model (project.js)
This is the central domain object. A project represents one p5.js sketch
and contains:
name
— the sketch name
user
— reference to the owning User document
files
— an embedded array of file sub-documents (see below)
slug
— URL-friendly identifier
visibility
—
"public"
or
"private"
(recently added feature)
createdAt,
updatedAt
— automatic timestamps
File Sub-Document Files are embedded documents within a Project, not top-level collections. Each file has:
name
— filename (e.g.,
sketch.js,
index.html,
style.css)
content
— the full text content of the file (stored as a string in MongoDB)
url
— for binary assets uploaded to S3, this points to the S3 URL
fileType
—
"file"
or
"folder"
children
— array of child file IDs (enabling a virtual file system tree)
isSelectedFile
— whether this file is currently selected in the editor
This embedded approach (rather than a separate
Files
collection) is an intentional MongoDB design choice. Since files are
always loaded with their parent sketch and never queried independently,
embedding avoids expensive joins and keeps reads fast.
Collection Model (collection.js)
Collections group multiple sketches together. Fields include
name,
description,
owner
(user reference), and
items
(array of project references). Collections have their own slug for
shareable URLs.
Controllers in
server/controllers/
contain the business logic called by route handlers. For example,
server/controllers/projects.controller.js
handles creating, reading, updating, and deleting sketches, including
authorization checks (confirming the requesting user owns the sketch
before allowing modifications).
As part of the ongoing TypeScript migration, the
user.controller
was refactored into a
barrel folder structure, with separate
files for authentication vs. account management endpoints, improving
maintainability.
Transactional emails (password reset, email verification) are sent via
Nodemailer configured with a
Mailgun SMTP transport. The mail module
lives in
server/utils/
and wraps Nodemailer's
sendMail
with application-specific templates. Configuration requires
MAILGUN_KEY
and
MAILGUN_DOMAIN
environment variables.
Binary assets (images, audio, video, fonts, etc.) that users upload to their sketches are not stored in MongoDB. They are uploaded directly to AWS S3. The flow works as follows:
POST
request to
/api/projects/:id/uploads
with the file data as
multipart/form-data.
multer
middleware on the server parses the multipart body into a buffer.
/{userId}/{filename}.
url
field in the MongoDB document.
S3 configuration requires
AWS_ACCESS_KEY,
AWS_SECRET_KEY,
AWS_REGION, and
S3_BUCKET
environment variables. The total storage used per user is tracked in the
User.totalSize
field to enforce reasonable per-user limits.
The
client/
directory is a substantial React application structured around feature
modules:
client/
├── modules/
│ ├── IDE/ # Core editor experience
│ ├── User/ # Authentication, account pages
│ ├── Dashboard/ # Sketch listing, collections
│ ├── About/ # About pages
│ └── Legal/ # Terms of use, privacy policy
├── common/
│ ├── components/ # Reusable UI components (buttons, modals, etc.)
│ ├── hooks/ # Custom React hooks
│ └── icons/ # SVG icon components
├── styles/ # Global SCSS styles
├── utils/ # Client-side utility functions
├── i18n.js # i18n configuration
├── reducers.js # Root Redux reducer (combines all slice reducers)
├── store.js # Redux store configuration
└── index.jsx # Client-side React entry point
Each feature module follows the same internal structure:
modules/IDE/
├── actions/ # Redux action creators
├── components/ # Presentational React components
├── pages/ # Route-level page components
└── reducers/ # Redux reducers (state slices)
This structure separates UI components from state management logic, a pattern rooted in the classic Ducks Redux convention. Pages compose components and connect them to the Redux store, while individual components remain as presentational as possible.
The
IDE
module is the most complex part of the frontend. Its top-level page
component is
IDEView.jsx, which orchestrates the entire editor layout.
The IDE layout consists of several key regions:
Toolbar — at the top, contains the Play/Stop button to run the sketch, the sketch name (editable inline), the auto-save indicator, and menus for File, Edit, Sketch, and Help.
File Navigator (Sidebar) — a collapsible panel on the left showing the virtual file tree of the current sketch. Files can be added, renamed, and deleted here. Folders are supported to allow organizing complex projects.
Code Editor Panel — the central editing area, powered by CodeMirror. Shows the currently selected file's content.
Preview Panel — on the right (or below,
depending on viewport), an
<iframe>
that shows the running sketch.
Console Panel — a collapsible panel at
the bottom showing
console.log
output, errors, and warnings from the running sketch.
The layout is rendered using CSS flexbox/grid, with panels being resizable
and togglable. The
IDEView
component uses
useSelector
and
useDispatch
hooks from
react-redux
to wire the UI to the Redux store.
The sketch execution architecture is the most elegant part of the system.
When a user clicks "Play," the editor does
not send code to the server for
execution. Instead, it constructs a complete HTML document in memory and
loads it into an
<iframe>
using a
blob:
URL or
srcdoc.
The
PreviewFrame
component (client/modules/IDE/components/PreviewFrame.jsx) is responsible for this. The process works as follows:
index.html
file is used as the base (or a default template if none exists), and
<script>
and
<link>
tags are injected to include the p5.js library at the version the user
has selected, plus all the user's
.js
and
.css
files.
srcdoc
attribute of a sandboxed
<iframe>.
This architecture means:
To support multiple p5.js library versions (the editor lets users select
which version of p5.js they want), the list of supported versions is
maintained in a
p5Versions.js
file and the CDN URL for the selected version is injected into the
assembled document.
Console interception is handled via a
clever communication bridge: the iframe's
console.log,
console.warn,
console.error, and runtime error events are intercepted and posted via
postMessage
to the parent frame. The
PreviewFrame
component listens for these messages and dispatches them into the Redux
store, where the Console component reads them for display.
The code editing experience is built on CodeMirror 5, a mature and highly extensible browser-based code editor. The integration wraps CodeMirror's imperative API within a React component.
The editor is configured with:
.js
files, HTML mode for
.html, CSS mode for
.css
p5-dark
and
p5-light
CodeMirror themes (defined in
_p5-dark-codemirror-theme.scss
and equivalent light theme)
The editor also features context-aware autocomplete for p5.js — a Google Summer of Code 2024 contribution — which provides intelligent suggestions for p5.js functions based on the cursor context.
Editor preferences (theme, font size, line numbers, etc.) are persisted in the User document on the server and synced to the Redux store on login.
The
Console
component (client/modules/IDE/components/Console.jsx) displays the output captured from the sketch's iframe via
postMessage. It renders log entries with type-appropriate styling (info, warning,
error) and supports clearing the log. The console is built to be
accessible, with ARIA live regions so that screen readers announce new
console output.
The Redux store is the single source of truth for all client-side
application state. The store is configured in
client/store.js
using
redux-toolkit
patterns (or manual
createStore
+
applyMiddleware), with redux-thunk as the async
middleware for side effects (API calls).
The root reducer in
client/reducers.js
combines all feature reducers:
// Conceptual structure of the Redux state tree
{
user: { /* currently logged-in user data, loading/error state */ },
project: { /* current sketch metadata (name, id, timestamps) */ },
files: [ /* array of file objects in the current sketch */ ],
ide: {
/* UI state: is console open, is sidebar open, is preview playing,
which file is selected, console messages, error messages */
},
preferences: { /* theme, font size, autosave, etc. */ },
collections: [ /* user's collections */ ],
sketches: [ /* user's list of sketches (for dashboard) */ ],
toast: { /* notification messages */ }
}
Each module has its own
actions/
and
reducers/
directories:
File content updates in the editor are a hot path: every keystroke
dispatches an action to update the file's
content
in the Redux store. Auto-save is then implemented by debouncing a
"save project" thunk that fires a
PUT /api/projects/:id
request to the server after a configurable idle period.
Client-side routing is handled by React Router. Key routes include:
| Route | Component | Description |
|---|---|---|
/
|
IDEView
|
New untitled sketch (anonymous) |
/:username/sketches/:sketchId
|
IDEView
|
Load a specific sketch |
/:username/sketches
|
Dashboard
|
User's sketch list |
/:username/collections
|
Dashboard
|
User's collections |
/login
|
LoginView
|
Login page |
/signup
|
SignupView
|
Registration page |
/reset-password
|
ResetPasswordView
|
Password reset flow |
/about
|
AboutView
|
About page |
/terms-of-use
|
TermsOfUse
|
Legal page |
The server is configured to serve the React app's
index.html
for all routes that don't match the
/api/
prefix, enabling the React Router to handle them client-side. This is the
standard SPA pattern.
A
/common
directory at the root of the repository holds code shared between the
client and server. This is particularly valuable for TypeScript type
definitions — since both sides need to agree on the shape of API
request/response objects, having a shared
common/types/
folder prevents duplication and keeps things in sync. This was established
as part of the TypeScript migration effort.
The editor does not use CSS Modules or CSS-in-JS. All styling is written in SCSS (Sass), following two conventions:
BEM (Block Element Modifier) for
naming. Class names follow the pattern
.block__element--modifier, keeping specificity flat and styles predictable.
The 7-1 Pattern for file organization.
The
client/styles/
directory is organized into:
styles/
├── abstracts/ # Variables, mixins, functions, placeholders
├── base/ # Reset, typography, base element styles
├── components/ # Per-component SCSS files
├── layout/ # Layout-level styles (grid, header, sidebar)
├── pages/ # Page-specific styles
├── themes/ # Light and dark theme variables
└── vendors/ # Third-party overrides (CodeMirror themes)
The editor supports light and dark themes, controlled by user preferences. Theme switching is implemented via CSS custom properties or SCSS variable swapping at the root level, and the chosen theme is persisted in the User document.
SCSS uses OOSCSS (Object-Oriented SCSS)
patterns with
@extend
and
%placeholder
selectors to avoid style duplication.
The editor supports multiple languages through
react-i18next, the React binding for
the popular
i18next
library. The configuration lives in
client/i18n.js.
Translation files are JSON files in the
translations/
directory, organized by locale:
translations/
├── en-US.json # English (default)
├── es-419.json # Spanish (Latin America)
├── zh-CN.json # Simplified Chinese
├── pt-BR.json # Brazilian Portuguese
├── hi.json # Hindi
├── bn.json # Bengali
└── de.json # German (partially)
In React components, translations are accessed via the
useTranslation
hook:
const { t } = useTranslation();
return <button>{t('Common.Save')}</button>;
Translation keys use a dot-notation namespace convention (e.g.,
IDE.RunSketch,
Account.Login). Interpolation, pluralization, and context-aware translations are all
supported via i18next's built-in features.
The i18n effort has been an ongoing community contribution — adding new languages and keeping translations current is explicitly called out as a contribution pathway in the contributor documentation.
The client-side build pipeline uses
Webpack for bundling and
Babel for transpilation. Webpack
configuration is split across
webpack/
and the root config files:
webpack/
├── webpack.config.client.development.js
├── webpack.config.client.production.js
└── webpack.config.server.js
Development build features:
Production build features:
Babel is configured in
.babelrc
to transpile modern JavaScript (ES6+) and JSX to ES5. Key Babel plugins
and presets:
@babel/preset-env
— transpiles ES6+ based on a browser target list.
@babel/preset-react
— transpiles JSX.
@babel/plugin-proposal-class-properties
— enables class field syntax.
@babel/plugin-transform-runtime
— avoids duplicating helper functions across modules.
The TypeScript migration added
@babel/preset-typescript
to the Babel pipeline, allowing TypeScript files to be processed by the
same build without a separate
tsc
compilation step (type-checking runs separately via
tsc --noEmit).
The application includes
server-side rendering support. When a
request comes in for a sketch URL like
/:username/sketches/:sketchId, the Express server renders the React application to an HTML string
using
ReactDOMServer.renderToString().
This means:
The SSR flow:
ReactDOMServer.renderToString(<App />)
with a pre-populated store.
window.__PRELOADED_STATE__
script tag), is sent to the browser.
A separate Webpack configuration (webpack.config.server.js) produces the server-side bundle with
target: 'node', excluding Node.js built-ins from bundling.
Testing is an area of acknowledged growth in the codebase. The current setup uses Jest as the test runner with React Testing Library for component tests.
As part of the 2025 TypeScript migration grant, test coverage was specifically expanded. The approach taken was:
user.controller) covering happy paths and edge cases
before making any changes during
migration — a form of property-based regression testing.
The test configuration in
package.json
runs tests with
jest, and the TypeScript migration added a
typecheck
step to the CI pipeline that runs
tsc --noEmit
to catch type errors without emitting compiled output.
For component development and visual testing, the project includes a
Storybook setup (.storybook/) allowing components to be developed and reviewed in isolation.
The CI pipeline is defined in
.travis.yml. The pipeline runs on every push to the
develop
branch and on every pull request:
npm install).
npm run lint
(ESLint). If this fails, the pipeline stops.
tsc --noEmit
(added as part of TypeScript migration).
npm test.
latest.
Steps 5–7 only run on the
develop
branch (not on PRs from forks), protecting production from unreviewed
code.
The project has both a production Dockerfile and development Docker Compose configuration.
Dockerfile
(production) — a multi-stage build:
npm run build.
NODE_ENV=production, exposes the port, runs
npm run start:prod.
docker-compose.yml
(production-like local) — defines two services:
web
— the Express application container.
mongo
— a MongoDB container.
docker-compose-development.yml
— adds volume mounts for live code reloading, disables production
optimizations, and enables HMR.
The Docker Compose setup is the recommended way to get a consistent development environment, especially for contributors who don't want to install MongoDB locally.
Production is hosted on
Google Kubernetes Engine (GKE). The
Kubernetes manifest (kubernetes_app.yml) defines:
Deployment
resource for the Express app container, with a configurable replica
count for horizontal scaling.
Service
resource exposing the deployment on port 80/443.
The
deploy.sh
script uses
kubectl set image
to perform a rolling update of the deployment whenever a new Docker image
is pushed, ensuring zero-downtime deployments.
The infrastructure directory contains Terraform (HCL) configuration for provisioning the GKE cluster and related cloud resources, enabling infrastructure-as-code management.
For contributors or educators who want to run their own instance, the
project includes a one-click Heroku deploy button backed by an
app.json
manifest. This provisions a Heroku app with MongoDB Atlas add-on and the
correct environment variables. A
Procfile
tells Heroku how to start the application (web: node index.js).
As of early 2026, the codebase is ~28% TypeScript and actively migrating. This effort is funded by the p5.js Web Editor pr05 Grant from the Processing Foundation.
The migration strategy is deliberately incremental and broad rather than attempting a complete migration. TypeScript is configured via:
tsconfig.base.json
— shared base configuration.
tsconfig.json
— root-level config extending the base.
client/
and
server/
that add appropriate settings (e.g.,
lib: ["dom"]
for the client,
module: "commonjs"
for the server).
The migration touched every layer of the tech stack:
@babel/preset-typescript
added to process
.ts/.tsx
files.
.ts
and
.tsx
extensions.
ts-jest
or Babel transform to test TypeScript files.
@typescript-eslint/eslint-plugin
added for TypeScript-specific linting rules.
A
common/types/
directory was established for
shared types — particularly important
for API response/request shapes that are used by both the Express
controllers and the Redux action creators.
The migration also introduced a
typecheck
script to CI, making TypeScript errors fail the build rather than just
being warnings. The long-term vision includes adopting
OpenAPI for API documentation, which
would allow shared types to be auto-generated from the API specification.
Accessibility is treated as a core requirement, not an afterthought. The codebase follows WCAG guidelines throughout:
Semantic HTML — proper use of
<nav>,
<main>,
<section>,
<ul>,
<li>,
<button>
vs
<div>
ensures correct screen reader interpretation.
ARIA attributes — ARIA live regions
(aria-live="polite") are used on the console output so screen readers announce new messages.
Modal dialogs use
role="dialog"
and
aria-modal="true". Interactive elements without inherent semantics are annotated with
role
and
aria-label.
Keyboard navigation — all interactive features are reachable by keyboard. The editor's menus, file tree, and toolbar are all keyboard-navigable with logical tab order.
Color contrast — both light and dark themes are designed to meet WCAG AA contrast ratios.
Focus management — when modals open, focus is trapped inside. When they close, focus returns to the triggering element.
p5.js Accessibility interceptor — the project historically used a p5.js interceptor git submodule that provided additional accessibility features for the sketch output (such as announcing canvas changes to screen readers).
The
contributor_docs/
directory includes explicit accessibility guidelines for contributors to
follow when adding new UI features.
The application is configured entirely via environment variables,
following the
12-factor app
methodology. A
.env.example
file in the root documents all supported variables:
| Variable | Purpose |
|---|---|
PORT
|
HTTP port to listen on (default 8000) |
MONGO_URL
|
MongoDB connection string |
SESSION_SECRET
|
Secret for signing session cookies |
AWS_ACCESS_KEY
|
AWS IAM access key for S3 |
AWS_SECRET_KEY
|
AWS IAM secret key for S3 |
AWS_REGION
|
AWS region for the S3 bucket |
S3_BUCKET
|
S3 bucket name for asset storage |
GITHUB_ID
|
GitHub OAuth app client ID |
GITHUB_SECRET
|
GitHub OAuth app client secret |
MAILGUN_KEY
|
Mailgun API key for email sending |
MAILGUN_DOMAIN
|
Mailgun domain |
EMAIL_SENDER
|
From address for transactional emails |
EMAIL_VERIFY_SECRET_TOKEN
|
Secret for email verification tokens |
FORCE_TO_HTTPS
|
Redirect HTTP → HTTPS (for production) |
API_URL
|
Base URL for API calls (default
/api)
|
In development, these are loaded from a
.env
file via
dotenv. In production (Docker/Kubernetes), they are injected as container
environment variables from a Kubernetes
Secret
resource.
1. Sketch execution in the browser (not server-side) Executing sketches entirely in a sandboxed iframe means zero server compute cost per sketch run. The trade-off is that complex sketches with many files need to be assembled client-side, which adds a small latency on initial run. This trade-off is clearly worth it at the scale of a free public tool.
2. Files embedded in Project documents (not a separate collection) Storing file content directly in the MongoDB Project document keeps reads simple and fast — one query fetches everything needed to load a sketch. The trade-off is document size limits (~16MB BSON limit in MongoDB) and potential performance degradation for very large sketches with many files. In practice, sketches rarely approach this limit.
3. MERN stack Using
JavaScript/TypeScript end-to-end simplifies contributor onboarding and
allows code sharing between client and server (the
/common
directory). The trade-off is that Node.js is not the strongest runtime for
CPU-heavy operations, but since sketch execution is client-side, the
server is primarily doing I/O (MongoDB, S3) where Node excels.
4. Incremental TypeScript migration
Migrating incrementally (rather than a big-bang rewrite) keeps the project
deployable and contributors unblocked throughout the migration. The
trade-off is a period of mixed
.js/.ts
codebase that requires contributors to understand both. The tooling
(Babel, Webpack, Jest all configured to handle both) mitigates this.
5. Session-based authentication Using
session cookies (stored in MongoDB via
connect-mongo) rather than stateless JWTs means server state must be maintained. The
trade-off is that scaling horizontally requires a shared session store —
which is solved by using MongoDB as that store rather than in-memory
storage.
6. No CSS Modules or CSS-in-JS The decision to use plain SCSS with BEM keeps the styling approachable for contributors who may not be deeply familiar with React-specific CSS patterns. The trade-off is potential class name collisions in a large codebase, mitigated by strict adherence to BEM conventions.
The p5.js Web Editor is a thoughtfully constructed full-stack application that has evolved over a decade of community development. Its architecture reflects the tension between serving a diverse community of contributors (many of them beginners) and delivering a polished, production-grade product for a global user base.
Several architectural highlights stand out:
If you're looking to contribute — whether it's fixing bugs, adding translations, migrating JavaScript to TypeScript, or building new features — the architecture is designed to be approachable at every level. The contributor documentation is thorough, the module boundaries are clear, and the team is welcoming. The source is at github.com/processing/p5.js-web-editor.
References and further reading: