dox docs

Architecture

dox is four layers stacked so that each one can be replaced without touching the others:

app/        features, one folder each
event/      a graph of named events between apps
icanvas/    the canvas, the windows, the keyboard
system/     one process that mounts everything

Each layer only knows the one beneath it. An app does not know the canvas; the canvas does not know what an app does; the server does not know what an app renders.

The four layers

Layer Owns Does not own
app/ One feature: its front end, its optional router, its manifest.json, its dependencies. Other apps, the window chrome, routing between apps.
event/ The edge list (graph.json) and a 40-line SDK with call and on. What the payloads mean.
icanvas/ Camera, windows as iframes, selection, keyboard, saved scenes. Anything inside a window.
system/ Directory scan, manifest parsing, mounting routers and static files, one origin. Layout, interaction, feature code.

Why apps stand alone

Every app has its own package.json, its own build and its own dependencies. No bundler config is shared; no package is hoisted across apps. Editor can be React, Wallpaper can be a single HTML file, and neither knows.

This costs disk space and duplicates a little config. It buys one property: an app can leave. Copy app/editor/ into another project, run npm install && npm run build, and it still works. Delete it here and the rest of the workspace loads exactly as before.

That property is why there is no "plugin API": the manifest is the whole contract, and the filesystem is the registry.

Why events instead of imports

An app that wants to open a file does not import the editor. It emits an event:

event.call('fileClick', { path });

event/graph.json decides what happens:

{ "from": { "app": "explorer", "event": "fileClick" },
  "to":   { "app": "editor",   "call": "openFile" } }

Three consequences:

  • Explorer has no idea the editor exists. It can be used with the editor removed.
  • Rewiring behaviour is a JSON edit, not a code edit.
  • The emitter cannot be broken by a listener: unbound events are silent no-ops.

Why one HTML file for the canvas

icanvas/index.html is a single file with no dependencies, about 1,700 lines. It draws windows, pans, zooms in steps from 50% to 200%, handles selection and the keyboard, and saves layouts.

Windows are <iframe> elements. That choice is doing real work:

  • Each app runs in its own document with its own globals, so two apps can both define React or fetch wrappers without colliding.
  • An app's CSS cannot leak into the canvas or into another app.
  • Same origin, so postMessage is cheap and an app's fetch('api/x') hits its own backend without CORS.

The cost is that focus and keyboard input have to be passed across the iframe boundary, which is why some shortcuts are handled by each app and some by the container.

Why one process

Every app's backend is a router mounted into one Node process, not a separate server. That is why the workspace costs what it costs: about 95 MB for the server and roughly 140 MB including a browser tab, versus 1.6 GB for an Electron editor.

The trade-offs are deliberate:

  • One origin. No CORS, no per-app ports, no proxy table.
  • One crash domain. A backend that throws at import time is caught, logged and skipped; that app falls back to static. An app that throws at runtime can still take the process down — the same risk a monolith has.
  • No per-app resource limits. There is no sandbox between apps.

If you need hard isolation between two apps, run a second dox on another port; nothing in the data model prevents it.

Data is files

Thing Where Format
Conversation dox-data/agent/sessions/*.sss JSON: title, turns, tool calls, timestamps
Layout *.dox JSON: camera, plus one entry per window
Settings dox-data/agent/config.json default model, default cwd
Models dox-data/agent/models/*.env endpoint, key and model name, one file per model

Where the data directory goes

Three requirements pick the location, in this order:

  1. On the persistent volume. In a container or on a cluster, everything outside the mounted volume disappears on restart — the root filesystem and $HOME included.
  2. Outside the code directory. Replacing the code tree, or syncing it with rsync --delete, must not touch the data.
  3. Derivable from the code path. If the location needs an environment variable or a remembered absolute path, every deployment has to be told, and "copy the folder and run" stops being true.

A sibling directory satisfies all three with no configuration: dirname(REPO_ROOT) + "/dox-data".

Every alternative fails one of them:

Location Fails
~/.local/share/dox 1 — home is often ephemeral on clusters
<code>/.data 2 — replaced or deleted with the code
/var/lib/dox 3 — needs DOX_DATA_DIR on every deployment

The dox- prefix earns its keep twice: it never collides with a sibling project's data/, and it stays correct when the code directory is renamed — /srv/dox-2026-10/ still reads /srv/dox-data/.

DOX_DATA_DIR remains the escape hatch for the layout that breaks rule 1 by design: code baked into an image, data volume mounted somewhere else.

No database, no migrations. A conversation is a document the agent app knows how to render — the same relationship the editor has with source files and the image app has with bitmaps. That is what "AI as a file type" means here: the agent is not a panel bolted to the side, it is a renderer registered for an extension.

What is deliberately absent

  • LSP, debuggers, an extension marketplace. The editor is a text editor. If you need those, run them beside dox, not inside it.
  • Authentication. The server binds 127.0.0.1 and assumes a trusted local user. Put a reverse proxy with auth in front (see Deploy).
  • A build pipeline for the server. The server imports .ts directly through Node's type stripping. This is why Node 22.6 is the floor.