Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt

Use this file to discover all available pages before exploring further.

PocketJS began as a single question: what is the minimum structure needed to run a reactive JSX app on a 333 MHz PSP with a 2 MB QuickJS worker? The answer — a no_std Rust core, a narrow synchronous op surface, a JS mirror tree — turned out to be an answer to a much larger question: what is the minimum structure needed to make any domain freely scriptable from JavaScript without paying the cost of a general-purpose engine? This document names that structure, shows how it generalizes, and explains why “many small engines” beats “one engine for everything.”

The thesis

Roblox demonstrated that the most valuable property of a game platform is not engine breadth — it is that the unit of creation is a script, not an engine build. Creators act on a curated world-vocabulary from a safe, embedded language and never touch the native layer. Roblox pays for this with one universal DataModel: every experience, whatever its genre, inherits the same giant ontology, and the engine must be everything to everyone. That is the “Unreal-shaped” cost PocketJS refuses. The PocketJS bet is the dual:
Don’t share a world-ontology. Share the grammar for defining world-ontologies.
Each domain gets its own deliberately small engine — a specialized runtime — that declares its own closed vocabulary: the nouns and verbs of that domain and nothing else. A 2D UI runtime speaks nodes, styles, layout, and focus. An FPS runtime speaks rounds, weapons, hits, and bots. What the platform standardizes is not the vocabulary but the mechanism by which any such vocabulary is implemented natively for performance, exposed to JavaScript wholesale for freedom, and composed with other vocabularies in one program. The PSP UI runtime was always the first instance of this pattern; the architecture below names it and makes it repeatable.

The triple

Every runtime is a triple:
Runtime = ⟨ Cores, Surfaces, Guest ⟩

Core (Rust) — where state and time live

A core is a native simulation that owns its domain state and its clock. pocketjs-core owns the retained UI tree, taffy layout, animation tracks, and the DrawList. An FPS core owns the map, player physics, ballistics, bot navigation, and the 3D scene. Cores are the only place where per-entity, per-frame work happens. Cores never call into the guest. Information flows outward as events in per-tick batches, not as callbacks mid-tick. A core that calls back into JS synchronously would break determinism and make replays impossible. Cores may share native substrate without sharing a vocabulary. pocket3d (wgpu device, forward renderer, BSP collision, skeletal animation) is substrate, not a runtime — it has no guest-facing vocabulary of its own. The 2D UI core renders through that same substrate on desktop (pocket-ui-wgpu), which is what puts 2D and 3D on a single foundation without merging their vocabularies.

Surface — the declared vocabulary

A surface is the entire boundary between a core and the guest, pinned as data in a spec (the spec/spec.ts pattern):
  • Ops — guest → core intent: commands and queries, synchronous, numeric codes, append-only. (ui.createNode, ui.setStyle, … / strike.setPhase, strike.configureWeapon, …)
  • Events — core → guest facts, delivered in per-tick batches. (hit, kill, roundEnded, …)
  • Assets — the binary formats the surface consumes: style tables, font atlases, paks; maps, models.
  • Frame contract — when the guest runs relative to the core’s clock.
A surface is mounted into the guest as one named namespace (globalThis.ui, globalThis.strike). Specs are single-source-of-truth TypeScript data, code-generated into Rust, with a drift guard that byte-compares the generated file in CI. A surface is versioned by append: codes are never renumbered, never reused. Capability = surface. A guest can affect exactly what its mounted surfaces express — nothing else. There is no ambient filesystem, network, or process access. Sandboxing is not a bolted-on policy; it falls out of the ontology. This is what makes third-party mods a tractable idea without a separate permission system.

Guest (QuickJS) — where products live

The guest is one QuickJS realm evaluating one bundled program. Apps, games, and mods are indistinguishable in kind here — they differ only in which surfaces they were given. QuickJS is the choice because it is embeddable everywhere the Pocket stack targets (it already runs on a 333 MHz PSP), deterministic, small, and fast enough when the boundary is designed correctly.

SDK — the idiomatic algebra per domain

Raw surfaces are wire protocols. Each surface ships an SDK that expresses it in the algebra natural to its domain:
  • The ui surface’s natural algebra is a reactive tree → its SDK is JSX (Solid or Vue Vapor through the universal renderer, Tailwind classes, animate()).
  • An FPS surface’s natural algebra is rules and policies over events → its SDK is a mod API: strike.on("kill", …), strike.rules.roundTime = 90, weapon and bot config tables.
Choosing the SDK shape per domain — instead of forcing one paradigm — is the “many small engines” philosophy applied to the API layer.

The three laws

Every runtime in the Pocket stack obeys these laws. They are what keeps “freely scriptable” compatible with “high performance.” All three are generalizations of mechanisms the PSP UI runtime already proved on 333 MHz hardware. Law 1 — State lives in cores; guests hold mirrors. Guest-side reads never cross the boundary in hot paths. The Solid renderer keeps a JS mirror tree so the reconciler reads JS objects, not FFI. A game SDK keeps mirrored snapshots updated from event batches. Ops are one-way writes; queries exist but are for cold paths only. Law 2 — Intent crosses as ops, facts cross as events, both spec-pinned. No shared memory, no callbacks-from-native mid-tick, no stringly-typed side channels. Everything that crosses is enumerable, versioned, and cheap to marshal (numbers, strings, buffers). This is what makes surfaces composable and mods auditable — you can read the spec and know exactly what a guest program can and cannot do. Law 3 — One guest turn per host tick. The host calls the guest exactly once per fixed-step tick (frame(buttons) for UI runtimes; game runtimes add their event pump in the same turn). The guest never owns a timer or a thread. Frame content is a pure function of tick index plus inputs, which is what makes byte-exact PNG goldens, headless acceptance scripts, and deterministic replays possible — the entire PocketJS verification story rests on this law.

The mechanism crates

The grammar is implemented once, as infrastructure every runtime reuses:
CrateRole
pocket-modGuest hosting: QuickJS realm lifecycle, surface mounting (mount("ui", ops)), per-tick pump (frame call + job drain + timers), console, hot reload. The “mod runtime” capability, as a library.
pocket-ui-wgpuThe ui surface, desktop edition: feeds paks to pocketjs-core, exposes the 17 HostOps ops to the guest, renders the DrawList through wgpu into any render target — a window (standalone app host) or an overlay pass over a 3D scene (game HUD).
pocketjs-coreThe 2D UI core: retained node tree, taffy flexbox layout, animation tracks, text/font atlas system, DrawList output. Platform-agnostic no_std + alloc.
pocket3dNative substrate: GPU bootstrap, forward renderer, BSP worlds, collision, skeletal animation, headless capture. Substrate, not a runtime — has no guest-facing vocabulary of its own.
A specialized runtime is a thin composition of these pieces. OpenStrike, the first game runtime built on the stack, looks like this:
openstrike (Rust bin)
  = FPS core (pocket3d substrate + game systems)
  + mounts `strike` surface  (spec: ops/events for rules, weapons, bots, rounds)
  + mounts `ui` surface      (pocket-ui-wgpu, composited as the HUD overlay pass)
  + pocket-mod guest         (one realm running the product bundle)

product bundle (JS, built by the PocketJS two-pass pipeline)
  = gameplay mod  (strike SDK: round rules, scoring, weapon/bot tuning, kill feed)
  + HUD app       (ui SDK: Solid JSX, Tailwind, animations)
Note what composition buys: the HUD is not “game UI code.” It is a full PocketJS app, running unmodified on the same framework that drives PSP hardware, mounted inside a game runtime. Any future runtime gets a production UI layer for free by mounting pocket-ui-wgpu. The PSP UI runtime, in the same notation, is just: a QuickJS guest + the ui surface + the sceGu backend. It was the first instance of the pattern all along.

Discipline for new runtimes

To add a runtime for a new domain, follow these five steps in order:
  1. Write the vocabulary first. Produce a spec.ts for your surface: ops, events, asset formats, enums. Keep it closed and small — if the list of nouns doesn’t fit on one page, the domain is cut too wide. Run codegen and add the drift guard before writing any Rust.
  2. Build the core against the spec, on whatever substrate fits (pocket3d, pocketjs-core, neither). The spec is the contract; the core implements it.
  3. Mount surfaces with pocket-mod, obeying the three laws. Cores never call back into the guest mid-tick; events flow outward in batches at the end of each tick.
  4. Ship the SDK in the algebra natural to the domain, and include a headless verification harness — scripted input, deterministic RNG, screenshot or state assertions. A runtime without a headless story is not done.
  5. Let the base game be the first mod. If the built-in behavior cannot be expressed through the surface, the surface is too weak — fix the surface, not the game. OpenStrike’s round rules, scoring, and weapon tables are JavaScript for exactly this reason.

What stays out of scope permanently

The architecture is defined as much by what it refuses as by what it includes. No universal scene graph. The 2D UI core has its own tree; the FPS core has its own world representation. They share a rendering substrate but not a scene abstraction. Merging them would recreate the “one DataModel for everything” cost the architecture is designed to avoid. No universal editor. Each runtime ships the tools appropriate to its domain. A 2D layout editor and a 3D level editor have nothing to share beyond a file format convention. No cross-runtime portability of game code. Vocabularies are allowed — encouraged — to be incompatible. A <Show> component and a strike.configureWeapon call are not the same kind of thing, and pretending they are would force both surfaces to share a lowest common denominator. The grammar is the platform; the vocabularies are deliberately separate.
The full design rationale for the runtime family is in RUNTIMES.md in the PocketJS repository. The first game runtime built on this architecture is OpenStrike, which demonstrates ui and strike surfaces mounted together in a single pocket-mod guest.

Build docs developers (and LLMs) love