Deploy
SQLite layout on disk
SQLite layout on disk
The gateway's state is files. Each app has its own SQLite pair (data.sqlite for handler-owned data, runtime.sqlite for chat + agent run history). Two small gateway-level SQLites sit next to the apps directory: centraid-gateway.sqlite (users + prefs) and centraid-analytics.sqlite (cross-app run rollup). The rest of the gateway-level metadata is plain JSON.
Top-level layout
<gatewayRoot>/├── centraid-gateway.sqlite ← users, user_prefs├── centraid-analytics.sqlite ← cross-app run_summary rollup└── <appsDir>/ ├── _registry.json └── <id>/ ← one per registered app ├── data.sqlite ← app-owned data (persistent across versions) ├── runtime.sqlite ← chat sessions, agent run ledger, automation state (persistent across versions) ├── current.json ← { activeVersion, history } ├── versions/ │ ├── v_<UTC ts>_<sha[:6]>/ │ └── v_…/ └── _chat/ ├── w<windowId>.jsonl ← per-window chat transcript └── index.json ← chat mode + adapter session idsWhere <gatewayRoot> is path.dirname(appsDir), and <appsDir> is:
- Local desktop:
~/.centraid/apps/(default) — gateway-level dbs at~/.centraid/. - Remote (OpenClaw plugin):
$OPENCLAW_STATE_DIR/centraid/apps/— typically~/.openclaw/centraid/apps/; gateway-level dbs at~/.openclaw/centraid/.
TODO(#120) — confirm the local desktop's default. README of openclaw-plugin documents the remote default; the local desktop's is in
apps/desktop/src/main/.
_registry.json
The registry of currently-registered app ids. Schema:
{ "apps": [ { "id": "hydrate", "registeredAt": "2026-05-08T14:30:00.000Z" }, { "id": "todos", "registeredAt": "2026-05-09T09:12:44.000Z" } ]}TODO(#120) — confirm the exact schema. The README documents the existence of
_registry.jsonbut not its full shape.
The gateway writes this atomically on register/deregister.
<id>/data.sqlite
The app's handler-owned database. Lives at the app root — not inside a version directory. Code is versioned; data isn't.
- Persists across
activateflips. - Persists across version pruning.
- The only way to "reset" an app's data is to delete this file (and let migrations re-run on next boot).
The reserved-name guards mean data.sqlite is never served as a static file, never uploadable in a tarball, never readable from any HTTP path except through query/action handlers.
<id>/runtime.sqlite
The app's history-of-use database. Also lives at the app root, also persists across version flips. Separate from data.sqlite so handler-owned data and runtime-owned history can evolve independently. Holds:
chat_sessions— conversation containers (user, title, mode, adapter session id, turn count, timestamps).runs/run_nodes— unified agent run ledger across kinds (chat turns, automation fires, sub-runs).parent_run_idlinks sub-runs across apps.automation_state— per-automation KV ((automation_id, key) → JSON).
Centraid never serves this file; only the runtime reads/writes it.
Gateway-level SQLites (<gatewayRoot>/centraid-*.sqlite)
Two small files sit beside <appsDir>/, owned by the runtime, never per-app:
centraid-gateway.sqlite—usersanduser_prefstables. Schema is multi-user-ready; on the desktop it's single-user today. On the OpenClaw plugin, identity comes from OpenClaw — see below.centraid-analytics.sqlite— a denormalizedrun_summarytable with one row per run (every kind, every app). Each insert into a per-appruntime.sqlite#runsmirrors a summary row here so cross-app audit / activity queries don't have to fan out across every app's runtime db.
<id>/current.json
The atomic pointer to the active version.
{ "activeVersion": "v_2026-05-09T09-12-44-000Z_d4e5f6", "history": [ { "versionId": "v_2026-05-08T14-30-00-000Z_a1b2c3", "activatedAt": "2026-05-08T14:30:01.000Z" }, { "versionId": "v_2026-05-09T09-12-44-000Z_d4e5f6", "activatedAt": "2026-05-09T09:12:45.000Z" } ]}TODO(#120) — confirm whether
history[]is a flat list of activations (an audit log) or a different shape. The README mentions{ activeVersion, history }; the inner shape needs verification.
A POST /centraid/_apps/<id>/activate call rewrites this file. The gateway uses temp-file + rename for atomicity.
<id>/versions/v_<ts>_<sha>/
Each upload extracts to a new directory under versions/. The directory name is v_ + UTC ISO timestamp + first 6 chars of the content sha256.
- Immutable. The gateway never modifies a version directory after extraction.
- Code-only. No
data.sqlite, noruntime.sqlite, no_chat, nothing app-state. Just what was in the uploaded tarball. - Pruned. Old versions above
versionRetention(default 5; min 2) are deleted. The active version is always retained.
Identical re-uploads collapse: if the sha matches an existing version, no new directory is created — history[] records the latest timestamp.
<id>/_chat/
Per-window chat state.
_chat/├── w<windowId>.jsonl├── w<otherWindow>.jsonl└── index.jsonw<windowId>.jsonl— append-only transcript, one event per line.index.json— per-window metadata: chat mode (openclaw/codex/claude), adapter session id, so the next turn can resume the conversation.
TODO(#120) — document the .jsonl event shape. From the README: "SSE stream of normalized events" describes the wire format; the .jsonl transcript almost certainly stores the same events. Confirm against
runtime-core/src/chat/.
What's NOT here
- No per-app user table. Identity lives in the gateway-level
centraid-gateway.sqlite#users. On the OpenClaw plugin, the gateway can also import identity from OpenClaw. - No cross-app handler database. Each app's
data.sqliteandruntime.sqliteare isolated; no handler ever opens another app's files. The gateway-levelcentraid-analytics.sqliteis a rollup (one summary row per run) — handlers can't write to it. - No log files. Handler logs go through the gateway's logger; persistence is the gateway host's concern.
Backing up
The whole <gatewayRoot>/ directory is the backup surface — both gateway-level SQLites plus the apps directory. versions/ can be re-uploaded and _chat/ can be cleared, but the SQLite files and current.json are the only state you can't reconstruct.
tar czf centraid-backup.tar.gz ~/.centraidTODO(#120) — capture official backup guidance once the runtime stabilizes. For now this is the obvious move.
Resetting an app
# Stop the gateway (close desktop, or stop the OpenClaw process)rm <appsDir>/<id>/data.sqlite # clear handler-owned datarm <appsDir>/<id>/runtime.sqlite # clear chat history + run ledger# Start the gateway — migrations re-run on next bootWhere to go next
- Gateway — the conceptual model the layout implements.
- Local deploy and Remote deploy — where each layout lives.
- Apps — the per-app shape.