Deep Dive: The Architecture of the p5.js Web Editor

A technical walkthrough of the open-source, browser-based creative coding IDE for p5.js.

Table of Contents

  1. Introduction
  2. High-Level Architecture Overview
  3. Repository Structure
  4. The Backend: Node.js + Express Server
  5. The Frontend: React + Redux Client
  6. Shared Code: The `/common` Directory
  7. Styling: SCSS with BEM and the 7-1 Pattern
  8. Internationalization (i18n)
  9. Build System: Webpack and Babel
  10. Server-Side Rendering (SSR)
  11. Testing Strategy
  12. CI/CD and Deployment Pipeline
  13. TypeScript Migration
  14. Accessibility-First Design
  15. Environment Configuration
  16. Key Design Decisions and Trade-offs
  17. Conclusion

Introduction

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.


High-Level Architecture Overview

The p5.js Web Editor is a MERN stack application:

At a high level, the application works like this:

Plaintext snippet
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.


Repository Structure

The repository follows a clean monorepo structure. Every major concern has its own top-level directory:

Plaintext snippet
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 Backend: Node.js + Express Server

Entry Point and Server Bootstrap

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 with Passport.js

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.

REST API Routes

All API routes are mounted under the /api path prefix. The routes are organized by resource type inside server/routes/:

Plaintext snippet
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.

Data Models with Mongoose

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:

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:

File Sub-Document Files are embedded documents within a Project, not top-level collections. Each file has:

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 Layer

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.

Email Service (Nodemailer + Mailgun)

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.

File Storage with AWS S3

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:

  1. The client sends a POST request to /api/projects/:id/uploads with the file data as multipart/form-data.
  2. The multer middleware on the server parses the multipart body into a buffer.
  3. The server uploads the buffer to S3 using the AWS SDK, placing the file in a path like /{userId}/{filename}.
  4. The S3 URL is stored in the file's url field in the MongoDB document.
  5. When the sketch runs, the p5.js code references the S3 URL directly.

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 Frontend: React + Redux Client

Client-Side Structure

The client/ directory is a substantial React application structured around feature modules:

Plaintext snippet
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

Module Architecture

Each feature module follows the same internal structure:

Plaintext snippet
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: Heart of the Editor

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 Preview Iframe Sandbox

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:

  1. The Redux store holds the current state of all files in the sketch.
  2. When a "run sketch" action is dispatched, a document assembler function collects all files from the Redux store.
  3. It constructs a full HTML document: the 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.
  4. The assembled HTML string is set as the srcdoc attribute of a sandboxed <iframe>.
  5. The iframe runs in isolation, with access to the DOM, Canvas API, Web Audio API, and everything else a browser offers — but is sandboxed from the parent page for security.

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 Editor: CodeMirror Integration

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:

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

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.

Redux State Management

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:

Javascript snippet
// 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.

Routing with React Router

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.


Shared Code: The `/common` Directory

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.


Styling: SCSS with BEM and the 7-1 Pattern

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:

Plaintext snippet
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.


Internationalization (i18n)

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:

Plaintext snippet
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:

Jsx snippet
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.


Build System: Webpack and Babel

The client-side build pipeline uses Webpack for bundling and Babel for transpilation. Webpack configuration is split across webpack/ and the root config files:

Plaintext snippet
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:

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).


Server-Side Rendering (SSR)

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:

  1. Express receives the request.
  2. It dispatches the necessary Redux actions to pre-populate state (e.g., fetching the sketch from MongoDB).
  3. It calls ReactDOMServer.renderToString(<App />) with a pre-populated store.
  4. The resulting HTML string, along with the serialized Redux state (as a window.__PRELOADED_STATE__ script tag), is sent to the browser.
  5. The client-side React app "hydrates" the server-rendered HTML, attaching event listeners without re-rendering.

A separate Webpack configuration (webpack.config.server.js) produces the server-side bundle with target: 'node', excluding Node.js built-ins from bundling.


Testing Strategy

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:

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.


CI/CD and Deployment Pipeline

Travis CI

The CI pipeline is defined in .travis.yml. The pipeline runs on every push to the develop branch and on every pull request:

  1. Build — installs dependencies (npm install).
  2. Lint — runs npm run lint (ESLint). If this fails, the pipeline stops.
  3. Type Check — runs tsc --noEmit (added as part of TypeScript migration).
  4. Test — runs npm test.
  5. Docker build — builds a production Docker image.
  6. Push to Docker Hub — pushes the image tagged with the commit SHA and latest.
  7. Deploy to Kubernetes — updates the Kubernetes deployment on Google Kubernetes Engine.

Steps 5–7 only run on the develop branch (not on PRs from forks), protecting production from unreviewed code.

Docker and Docker Compose

The project has both a production Dockerfile and development Docker Compose configuration.

Dockerfile (production) — a multi-stage build:

docker-compose.yml (production-like local) — defines two services:

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.

Kubernetes on Google Kubernetes Engine

Production is hosted on Google Kubernetes Engine (GKE). The Kubernetes manifest (kubernetes_app.yml) defines:

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.

Self-Hosting on Heroku

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).


TypeScript Migration

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:

The migration touched every layer of the tech stack:

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-First Design

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.


Environment Configuration

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.


Key Design Decisions and Trade-offs

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.


Conclusion

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: