Apps and handlers
Apps
Apps
A Centraid app is a folder of code, plus the data and history that accumulate as it's used.
The source tree is what you author and upload:
my-app/ app.json # the manifest (required) index.html app.css, app.js, … # static assets — see allowlist queries/<name>.js # dispatched by centraid_read actions/<name>.js # dispatched by centraid_write migrations/<NNNN>_<name>.sql automations/<id>/ # optional — each automation is a subdir automation.json # manifest (triggers, prompt, requires) handler.js # the JS the runtime runs when the trigger firesThe mounted tree is what lives on the gateway — every upload lands in a new versioned snapshot beside the app's persistent state:
<appsDir>/<id>/ data.sqlite # app-owned data, never served as a static file runtime.sqlite # chat sessions, agent run ledger, automation state KV current.json # { activeVersion, history[] } versions/ v_<ts>_<sha>/ # immutable snapshot of an uploaded source treeNo app server framework. No build step required at runtime (you can author in TypeScript and ship compiled .js next to it, but the gateway loads .js only).
What an app is
- A unit of upload. You package the folder as a tar.gz and
POSTit to/centraid/_apps/<id>/upload. The gateway treats each upload as an immutable version. - A unit of data isolation. Every app gets its own
data.sqlite(handler-owned data) andruntime.sqlite(chat sessions, agent run ledger, automation state). Implementations get a scoped DB proxy — there is no path from one app's worker to another app's databases. - A unit of identity. App ids are the gateway's primary key. They appear in URLs (
/centraid/<id>/...), in tool calls ({ app: "<id>", ... }), and in the chat session key (centraid-chat:<id>:w<windowId>). - A tool catalog. An app's
queries[]andactions[]arrays inapp.jsonare the app's tool surface — each entry is a tool with a name, description, input/output schemas, and side-effect annotations. Your UI, an AI agent, and the CLI all reach those tools through the same dispatcher. - A unit of routing. The runtime knows about apps by name and dispatches calls to the active version of each one.
The manifest (app.json)
app.json is the only required file. Everything else is optional, but a useful app has at least one query or action.
{ "manifestVersion": 1, "id": "hydrate", "name": "Hydrate", "description": "Track 8 cups a day.", "version": "0.1.0", "tables": [...], "actions": [ { "name": "set-cups", "description": "Set today's cup count (clamped 0..8).", "confirmation": "none", "input": { "type": "object", "properties": {...}, "required": ["cups"] }, "output": { "type": "object", "properties": {...} }, "writes": ["hydrate_daily"] } ], "queries": [ { "name": "get-today", "input": { "type": "object", "properties": {} }, "output": { "type": "object", "properties": {...} }, "reads": ["hydrate_daily"] } ], "knobs": [...]}Key fields:
id— must match the folder name and the URL slug.actions[]/queries[]— each entry describes a handler. The dispatcher usesinput(JSON Schema draft 2020-12) to validate calls before the handler runs;reads/writesdocument which tables the handler touches.tables[]— declared shape. The actual schema lives inmigrations/;tables[]is for tooling and agents.knobs[]— user-facing settings the desktop shell renders. Each knob has akey,label,type(segmented,swatch, …),default, and per-type options.
See Build → app.json for the full schema.
Versions and current.json
Code is versioned. Data and chat/automation history are not — they persist across every flip.
<appsDir>/<id>/ data.sqlite ← persists across version flips runtime.sqlite ← persists across version flips current.json ← { activeVersion: "v_…", history: [...] } versions/ v_2026-05-08T14-30-00-000Z_a1b2c3/ ← immutable, code-only v_2026-05-09T09-12-44-000Z_d4e5f6/current.json#activeVersionnames the directory underversions/that serves traffic.- Switching versions is a single write to
current.json. No downtime, no extraction, no restart. - Old versions are kept up to
versionRetention(default 5, minimum 2). The active version is always retained.
Why this split:
- Rollback is free. A bad release is a one-line fix.
- Migrations run forward, code can go back. You don't lose yesterday's data when you revert today's handlers (assuming your migrations were additive, which they should be). Chat history and automation runs stick around too.
- Pruning is safe. Retention only deletes inactive
versions/v_…/dirs; it never touchesdata.sqliteorruntime.sqlite.
App ids — what's reserved
Ids beginning with an underscore are reserved for the gateway's own routes (_apps, _tool, _chat, _changes). Trying to register _anything returns an error.
Lifecycle in one sentence
Upload (POST /centraid/_apps/<id>/upload) → gateway extracts to a new versions/v_…/ dir → flips current.json#activeVersion → that version's index.html is what /centraid/<id>/ serves until the next upload or an explicit activate flip.
Where to go next
- Queries and actions — what handlers actually do.
- Change stream — how the UI knows when data moved.
- Automations — triggered work that lives alongside apps (or in headless
auto.*apps). - Build → App anatomy — practical walkthrough.