Architecture Overview
Awaken is an engine-first, editor-as-a-client system: a set of framework-free engine packages with a clean public API, driven by a React editor that only ever talks to that API - and by the shipped player through the same API. This section maps how the code is laid out, how it builds, and how it is tested.
Engine-first, editor-as-a-client
The core idea is a strict separation between the engine (what draws, simulates, and runs your game) and the editor (the tool you use to author it).
- The engine is four TypeScript packages under
packages/*-@awaken/core,@awaken/render,@awaken/assets, and@awaken/runtime. They have no UI framework in them at all. They expose a public API (ECS, renderer, importers, play loop) and nothing else. - The editor is a React app under
apps/editor. It is a client of the engine: it constructs aWorld, aRenderer, and aRuntime, and drives them through their public methods. It never reaches inside the engine's internals. - The player is a second, much thinner client under
apps/player. It boots a saved scene and runs it, with no editor UI.
Because the editor authors through the same API the engine exposes to a shipped game, edit == play == ship: the object you drag in the Viewport is the object the runtime ticks in Play, which is the object the player runs after export.
The engine does little, on purpose
The shipped runtime stays small because the engine provides mechanisms and keeps the opinions out. An ECS, a renderer, a script host, seams for physics, audio and effects: everything with a taste to it (a look, a gameplay pattern, an importer's knowledge of another engine, a tool) lives outside the kernel as data, content, or an editor concern, and reaches the engine through a named seam.
Measured (2026-08): the complete player runtime minifies to 416 KB (192 KB gzipped) without physics. Rapier adds 2.2 MB and ships only when a scene uses a collider or rigidbody. The editor is a 46 MB application. The two ship separately and are allowed to be different sizes because they have different jobs.
Four consequences worth knowing as a user of the engine:
- Policy lives in data. The sky's bodies, stars and grade are scene data (
SkyEffect); a material's shading is a data asset with WGSL hooks; an absent effect is simply not applied. A constant in engine code that starts describing taste is treated as a bug. ScriptApistays primitive. Gameplay is composed from importable game scripts and wasm modules that ship with the game, not from engine API growth.- Importers translate, they do not interpret. Unity, Godot and Unreal content maps construct-for-construct into one shared IR, one consumer applies it, and the renderer never learns which engine a mesh came from. Where a source concept has no Awaken surface, the surface gets built rather than approximated.
- Absence is free. Physics wasm is stripped from exports with no colliders; shader-graph node functions are emitted per material;
sideEffects: falselets bundlers drop importer code from the player; SSAO, GPU cull and clustered lighting are opt-in settings.
docs/ARCHITECTURE.md in the engine repo is the full statement of this, and it is the document a change is measured against.
The seams
The supported ways to extend the engine, in order of preference. Anything not reachable through a seam is an engine change.
| Seam | Carries | Ships in games |
|---|---|---|
Components + systems (DEFAULT_SYSTEMS, Runtime) | New simulation behaviour | yes |
| Script assets + wasm modules | Gameplay, generated worlds | yes |
| Hook materials / shader assets | Surface looks, vertex motion | yes |
Effect stack (RenderEffects) | Scene look: sky, fog, grade, SSAO | yes, as data |
| Editor plugins (sandboxed, signed) | Tools, panels, importers | no |
Decompressor providers (AWAKEN_DECOMPRESSORS) | External codecs, kept separate for licensing | no |
| Asset store | Content: prefabs, materials, particles | as content |
Two seams are deliberately absent: there is no runtime engine-plugin ABI (third-party code extending the player) and no downloadable render passes. Both would freeze renderer internals as public API and add a security surface to every shipped game, for no current need. Extending the player means contributing upstream or shipping content through the seams above.
The one-way dependency direction
Dependencies flow one way, from the base outward. Nothing in the engine ever depends on anything above it, and nothing anywhere depends on the editor.
@awaken/coresits at the bottom with zero dependencies - pure ECS, math, and serialization.@awaken/renderdepends only oncore;@awaken/assetsdepends oncoreandrender;@awaken/runtimedepends oncoreplus the Rapier physics WASM.- Both apps depend on all four packages, and nothing depends on an app.
This is not just tidiness. The one-way graph is what lets Play mode run the actual shipping runtime rather than an editor-flavoured imitation, and it is what keeps the engine reusable outside the editor.
See Monorepo & Packages for the exact package boundaries and dependency rules.
Why the engine never imports React
The engine packages never import React (or any UI framework). This is a hard rule, and it buys three concrete things:
- The runtime is portable.
@awaken/player- a shipped game - pulls in@awaken/runtimeand gets the play loop, scripts, and physics with zero React in the bundle. If the engine imported React, every exported game would ship a UI framework it never uses. - Play mode is honest. The editor's Play button and the shipped player boot the same
Runtimeclass. If Play depended on editor-only React state, "it worked in the editor" would stop meaning "it works when shipped." - The seam is enforceable. The editor's dev server even reloads the whole page when engine code changes, because the editor store (which owns the
PlayControllerandScriptHost) must be rebuilt from fresh engine code rather than hot-patched - see theawaken-reload-on-engine-changeplugin in Monorepo & Packages.
If you find yourself wanting to import React inside packages/*, that is the signal that a responsibility is on the wrong side of the seam.
One runtime, two front-ends
The same @awaken/runtime powers editor Play and the exported player. The difference is only what wraps it.
- In the editor, Play mode snapshots the world on Play, ticks the runtime, and restores the snapshot exactly on Stop, so play-time changes never persist.
- In a shipped game,
@awaken/playerskips the snapshot and just runs - the scene is the game.
Both call into the same code path, which is why a script or physics behaviour that runs in Play runs identically in the exported game.
What this section covers
- Monorepo & Packages - the pnpm workspace layout, the four
@awaken/*packages and the two apps, dependency rules, the shared TypeScript config (raw-source consumption, the~5.6.3pin), and the editor's custom Vite plugins. - Build & Player Template - the build pipeline, how
build:player-templateproduces the single-file player that exports embed, and the critical rule that a stale template ships stale game code. - Testing - Vitest unit tests, the headless Deno WebGPU shader/pipeline check, the golden-frame pixel baseline, the layering and player-size gates, the Puppeteer smoke tests, and the lint / Husky gates, with the exact command for each.
📸 Screenshot - save as img/architecture-overview.png
The editor open with a scene, and a second browser tab showing the same scene running as an exported game.html - side by side to illustrate "edit == play == ship."