Self-hosted service
yoohoo360/open-pencil-cloud avatar
yoohoo360/open-pencil-cloud

OpenPencil under Yoohoo: the upstream engine with a React shell and a cloud backend

AI-native design editor, open-source Figma alternative with Spring Boot and React.

549 stars4 forksTypeScriptMIT

At a glance

What is it?
Yoohoo's fork of OpenPencil keeps the upstream design engine, the .fig format and the CanvasKit renderer, then wraps them in a React shell, a Java API and a Compose deployment for teams that want their own document backend. The rendering core is upstream's, and everything you have to operate is new.
Who is it for?
Adopt this fork when your team already runs Postgres, Redis and a Compose host and wants .fig documents in storage it controls, since the Bun plus rspack path opens the editor in a browser with no desktop build step, while every AI capability on offer comes from upstream. Skip it for local only work, where upstream OpenPencil needs no account and no backend at all.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A product fork that leaves the editor engine alone

OpenPencil is an open source design editor that reads and writes `.fig` files through a CanvasKit renderer driven by a SceneGraph, and this repository is the Yoohoo product fork of it. Nothing in the engine was rebuilt. The `.fig` and `.pen` formats, the Yoga layout pass, components and instances, and the programmable `@open-pencil/core` package all arrive from upstream unchanged, so a document opened here is the same document the upstream tooling expects.

What changed is everything wrapped around that engine. Upstream ships a Vue 3 app under `src/`; the fork names `packages/react` as its primary shell and adds a Java cloud API under `server/`, object storage, and a Compose deployment path. This is aimed at a company that wants a browser design editor with its documents on infrastructure it hosts itself, rather than a local-first desktop tool that asks for no account.

`*.override.ts` remaps product diffs at build time

The rule for diverging from upstream is file naming rather than patching. Where product behaviour has to differ, the change lands in a file whose name ends `*.override.ts`, and rspack remaps those overrides onto the upstream module while building. Upstream sources therefore stay mergeable, which is what makes tracking a fast moving upstream project workable in the first place.

The package layout shows where product code actually lives: the graph model in `packages/scene-graph`, parsing and instance handling for `.fig` files in `packages/fig`, renderer and editor work in `packages/core`. One performance change is named outright, a lazy page load for `.fig` files, so a large document pays to open the pages on screen rather than parsing everything up front. What the docs never explain is what happens to an override when an upstream module is renamed or dropped. That gap will surface the first time you touch the engine.

The Vue shell is still wired into the workspace list

The workspace declarations say more than the comparison table does. Both `packages/react` and `packages/vue` appear among the twenty-two workspaces declared in the root `package.json`. The repository root still carries an `src/` directory, which the table assigns to the upstream Vue 3 app, plus a `desktop/` directory and a `tauri` script. So the fork did not remove the Vue shell: two shells and the Tauri desktop target remain inside the install and build graph while only `packages/react` is called the primary app. The bill for that is a heavier install than a React-only consumer needs, and a split between bundlers, because the root build chains a package build, a lint pass and `vite build` while the React shell starts an rspack server.

Bun, rspack and the port the editor opens on

Requirements are Bun, latest stable recommended, with Docker optional and only needed when you want the API on the same machine. One install wires every workspace in the repository, and then the shell starts:

sh
bun install
cd packages/react
bun run start

Behind that script sits an rspack server, and the comment kept beside it names the environment it sets, `NODE_ENV=development` and `APP_ENV=local`. The `APP_ENV` value chooses the config file, so the editor talks to http://localhost:8080 for the API by default and the browser opens http://localhost:8000. Two further targets exist for the same shell:

sh
cd packages/react
bun run build:test   # APP_ENV=test (dev preview build)
bun run build:prod   # APP_ENV=prod

The mapping is short enough to state outright. `local` selects `config.local.ts`, `test` selects `config.dev.ts` for the dev preview site, and `prod` selects `config.prod.ts`. What a host serves is the `dist` output of that package, and `.github/workflows/react-ui-deploy.yml` is offered as one way to produce and publish it, with nginx in front.

The server env file is where configuration actually lives

Everything the Java service reads comes from the `.env` file under `server/`, where the connection string puts Postgres on port 5432 and two token lifetimes deserve a closer look than a skim:

bash
DATABASE_URL=jdbc:postgresql://xxx:5432/open-pencil
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:1420,http://localhost:8000
JWT_EXPIRATION=8640000000
JWT_REFRESH_EXPIRATION=604800000
STORAGE_TYPE=local
OSS_PRESIGN_EXPIRATION_SECONDS=900

`JWT_EXPIRATION` is set to eight thousand six hundred and forty million milliseconds, which is a hundred days, while `JWT_REFRESH_EXPIRATION` is seven days. An access token that outlives its own refresh window by a factor of fourteen is a default to change before this runs anywhere but a laptop. The presign lifetime of 900 seconds keeps upload links short, which is sensible.

The allowed origins list is the other thing to check. Two of its three entries, the ports 3000 and 1420, are never explained anywhere in the docs, so whoever owns the deployment has to work out for itself which service answers on them. Mail has a shortcut worth knowing: leaving `MAIL_HOST` empty makes the API print verification codes to its own console, convenient locally and wrong on a shared host. Storage switches between `STORAGE_TYPE=local` and `STORAGE_TYPE=oss` with the matching `OSS_` keys.

Compose declares a storage path the env file never lists

The deployment recipe exposes a gap between the two documents a reader is handed. Compose passes two environment values:

yaml
services:
  open-pencil-server:
    image: registry.cn-hangzhou.aliyuncs.com/jongwong/open-pencil-server:0.0.1-beta.28
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - LOCAL_STORAGE_PATH=/app/storage

`LOCAL_STORAGE_PATH` appears in neither `.env` sample, so the only place its value is written down is the Compose file, and an operator who edits the env file alone will find that setting untouched. `SPRING_PROFILES_ACTIVE=dev` is what the section titled cloud deployment ships, while the production value comes with a condition attached, use it only if that profile is configured in `server/`. Nobody has confirmed that it is. The sample also expects an external Compose network to exist already, plus a host directory for the env file and a persistent storage directory:

sh
# Ensure app-network exists, then:
docker compose pull open-pencil-server
docker compose up -d open-pencil-server

Restart policy is `unless-stopped`, so a mistyped tag retries on every boot instead of leaving a quiet outage. Compose publishes port 8080 for the API, which is the same port the React shell expects by default.

Two version lines that nothing maps to each other

Engine packages and the server image move on unrelated version lines. The root `package.json` carries the name `open-pencil-app` and the number `0.15.1` with `private` set, while the image tag in the deployment sample is `0.0.1-beta.28`. Nothing in the repository connects the two. The last push was on 2026-09-30, and the repository publishes no GitHub releases, so there is no tag history to read before bumping the image and no way to tell from outside whether a published image was built from a given commit. Upgrading the backend means choosing a tag yourself, while the TypeScript side at least has a documented gate:

sh
bun run check        # full gate
bun run test:unit    # unit tests
bun run format       # format

Those three scripts run against the workspace. No equivalent is given for the Java service, and no migration or rollback story is documented for the database.

Where the fork is the wrong tool, and what upstream gives instead

One alternative deserves a straight comparison, and it is the project this one forks. Upstream OpenPencil is local-first, needs no account, ships official web and desktop builds, and carries the Vue app, a CLI package, MCP, desktop AI agents, P2P collaboration and token analysis. For a single user opening local files with no backend to operate, upstream deletes the whole Java, Postgres, Redis and OSS surface the fork asks you to run.

The reverse case is just as clear. When documents have to be shared, accounts and OAuth sign-in through GitHub and Google matter, and the storage has to be yours, then `server/` and the Compose path are the only part of this repository that answers, and upstream does not. Where the evaluation can mislead is the AI framing. The repository calls itself an AI-native design editor, yet every AI capability listed, the CLI, MCP, agents and token analysis, is credited to upstream, and the project states that its own day-to-day path is the React shell plus the Compose API. The engine is not modified, so judging this on AI features means judging upstream's engine with another shell attached. The engine stays under the upstream MIT terms and the fork adds its own config and deploy files on top, so check which of those files your organisation needs to own before forking further.

Editorial conclusion

Adopt this fork when your team already runs Postgres, Redis and a Compose host and wants .fig documents in storage it controls, since the Bun plus rspack path opens the editor in a browser with no desktop build step, while every AI capability on offer comes from upstream. Skip it for local only work, where upstream OpenPencil needs no account and no backend at all. Verify three things first: that SPRING_PROFILES_ACTIVE=prod exists in server/, where LOCAL_STORAGE_PATH is read from, and how far a shared document travels on a token that lives a hundred days.

Frequently asked questions

Is yoohoo360/open-pencil-cloud a free replacement for Figma?

The repository describes itself as an open source Figma alternative, and the fork keeps upstream's `.fig` format, SceneGraph and CanvasKit renderer unchanged while adding a React shell and a Java API for cloud documents. What you get is a self-hosted editor with accounts, not a drop-in replacement for every part of a commercial design suite.

What does it take to run yoohoo360/open-pencil-cloud locally?

Install Bun, run `bun install` from the repository root, then `bun run start` inside `packages/react`, which starts an rspack server and serves the editor at `http://localhost:8000` with the API defaulting to `http://localhost:8080`. Docker is optional and only needed for the API, and the editor still opens local `.fig` files without it.

Which services does the yoohoo360/open-pencil-cloud server need?

The Java service reads Postgres, Redis, JWT, OAuth, mail and storage settings from a `.env` file under `server/`, and the deployment runs it under Compose with an external network, a persistent storage directory and port 8080 published. Redis and Postgres are configured explicitly, so both are expected rather than optional.

Is there a version of yoohoo360/open-pencil-cloud I can pin?

The repository publishes no GitHub releases, so there is no tag history to pin against. The backend runs from a container image whose tag in the sample is `0.0.1-beta.28`, and that line is separate from the app version `0.15.1` recorded in the root `package.json`.

Does yoohoo360/open-pencil-cloud include the AI features listed for OpenPencil?

The AI capabilities named, including MCP, desktop AI agents and token analysis, are attributed to upstream OpenPencil, and the project states that its own day to day path is the React shell and the Compose API. What the fork changes is the shell, cloud document handling, storage and performance overrides, not the engine.

How do I change editor behaviour without patching upstream code?

Product differences go into files ending `*.override.ts`, which rspack remaps at build time so upstream files stay mergeable. Named examples are the lazy `.fig` page load under `packages/core`, and the layout notes that `packages/scene-graph` and `packages/fig` may also carry overrides.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. yoohoo360/open-pencil-cloud on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yoohoo360-open-pencil-cloud.svg)](https://hysenlabs.com/projects/yoohoo360-open-pencil-cloud)

Was this useful?