Guide: Persistence

Status: Active Source: A three-impl response surfaced the persistence-grounding gap; this guide is rewritten to defer to the existing specs rather than redefine.


What this guide covers

How peers persist on disk. The on-disk layout for entity-core peers is already normatively specified by SDK-OPERATIONS.md §15 (Configuration Directory) and extended for identity-aware peers by SDK-IDENTITY-INFRASTRUCTURE.md §8.4 (identity bundle layout). This guide reads those specs back into developer-readable form, fills small alignment gaps (storage filename, env-var name), and elevates the dispatch-index re-registration callout from spec footnote to first-class guidance.

In scope:

Out of scope:


1. The three layers

Persistence sits in three layers, each with a clean responsibility.

1.1 Configuration directory — where stuff lives on disk

~/.entity/ (SDK-OPERATIONS §15 default; env-var/CLI-overridable per §15.3).

~/.entity/
├── identities/{name}/          Keypair material (V7-only) or identity bundle (identity-aware)
└── peers/{name}/               Per-peer state
    ├── keypair                 Runtime peer keypair
    ├── config.toml             Startup config (listen addr, storage backend, extensions)
    ├── identity.toml           OPTIONAL — present iff peer is identity-aware
    └── store.db                The peer's tree (when storage_backend = "sqlite")

The configuration directory is per-user, not per-app. Multiple apps coexisting on a machine share ~/.entity/ the same way they share ~/.ssh/ or ~/.gnupg/ — by convention, by user filesystem permissions, and by per-peer naming inside the directory.

1.2 Per-peer state — one peer, one tree

Peers are indexed by name (a local user-chosen alias) inside ~/.entity/peers/{name}/. The peer-id is derived from peers/{name}/keypair and is not the directory name.

A peer is one tree (one DB), one keypair, one optional identity binding, one config. There is no separate "app peer" vs "network peer" at the persistence layer — there are just peers. Whether a peer's tree happens to host application namespaces (app/workbench-01/..., app/calculator/...) or only system + protocol state is a property of the tree's contents, not a property of the on-disk structure.

If a user runs multiple impls of the same conceptual application (Go workbench, entity-browser-rust workbench, Godot workbench), they spin up peers under different names — peers/workbench-go/, peers/workbench-entity-browser-rust/, peers/workbench-godot/, or whatever the user picks. Different names = different keypairs = different peer-ids = no interference. They can also share, by pointing multiple impls at the same peers/{name}/, but that means running the same peer through different impls — uncommon and not generally recommended (concurrent live runs of the same peer-id is not supported behavior).

1.3 Runtime state lives in the tree, not on disk

Once a peer is up, its operational state — known peers, transports, configuration parameters, peer aliases, active connections — lives in its own tree as entities under system/transport/*, system/config/*, system/peer/*, system/connection/* (per proposals/PROPOSAL-OPERATIONAL-STATE-TYPES.md). A handler that needs the keepalive interval reads system/config/connection. On crash restart, the peer reads its own tree to recover known peers and their transports — the tree is the recovery mechanism.

What's on disk, then, is:

Application state (app/{app-id}/...) is part of the tree, persists with it, and isn't a separate concern at the on-disk layer.


2. Platform mappings for the configuration directory

~/.entity/ is the SDK-OPERATIONS default; deployments often want platform-native data dirs. SDK-OPERATIONS §15.3 MUSTs env-var/CLI override support; the recommended env-var is ENTITY_DATA_DIR.

PlatformTypical resolutionMechanism
Linux native$XDG_DATA_HOME/entity/ (default ~/.local/share/entity/)XDG Base Directory; directories crate / equivalent
macOS native~/Library/Application Support/entity/Apple HIG; directories crate
Windows native%APPDATA%\entity\Known-folder API; directories crate
Godot projectuser://entity/Godot's user:// resolves per-OS
Server / containerized/var/lib/entity/ or env-var-set pathOperator deployment choice
Test / hermetic$TMPDIR/entity-test-{n}/Per-process, cleaned up

The default ~/.entity/ is fine for development; production deployments override.


3. Storage backend selection

BackendWhenCaveats
In-memoryTests, ephemeral workers, library embedding where the host owns persistence.Lost on shutdown.
SQLiteNative desktop apps, server peers, anything user-persistent.Single-process write access; WAL mode handles concurrent readers. Recommended store.db filename.
Future backends(Reserved) — RocksDB, FoundationDB, browser IndexedDB for WASM, custom.The store trait is normative; backends are implementation-defined per SDK-OPERATIONS §16.4.

Per peer, picked at construction time via config.toml storage_backend = "memory" | "sqlite" | ... (current Rust convention) or equivalent.

Decision flow:


4. What survives restart, what's recomputed

StatePersists?Notes
Entity tree (the peer's data)Yes (with persistent backend)The whole point of persistence. App state, system/operational state, identity bindings — all in here.
KeypairYes (file in peers/{name}/keypair)Independent of the tree's storage backend.
Identity bundleYes (files in identities/{name}/...)Per SDK-IDENTITY-INFRASTRUCTURE §8.4.
Bootstrap configYes (peers/{name}/config.toml)What's needed to start the peer; not the same as runtime system/config/* entities.
Runtime operational state (system/transport/*, system/config/*, system/peer/*, system/connection/*)Yes (lives in the tree)Per PROPOSAL-OPERATIONAL-STATE-TYPES; recovered on restart by reading the tree.
Query indexesImplementation-defined per EXTENSION-QUERYSome persist, some always-rebuild.
Subscription stateNosubscribe() is per-process; re-establish at startup.
watch() sessionsNoL0 watch is in-memory pattern matching.
Dispatch index (handler bodies)NoManifest in tree survives; in-memory callable doesn't. Application MUST re-register at startup per SDK-OPERATIONS §11.6.6.
In-flight continuationsIf persisted via EXTENSION-CONTINUATIONWithout that extension, restart loses in-flight work.
TCP listeners / sync enginesNoRe-established by peer construction.

The dispatch-index row is the callout: register your handlers in your startup path, not just at install. The manifest in the tree is the declaration (handler exists, this op, this grant); the language-native callable that actually runs is held in the dispatch index in memory and is gone at restart.


5. Identity-aware vs V7-only modes

Both modes are valid per SDK-OPERATIONS §15.1 / §15.2 / §15.4.

5.1 V7-only mode

No identity extension. Flat keypair files under identities/{name}/{public_key, private_key}; peers/{name}/ with keypair + config.toml + grants.toml. Suitable for development, simple deployments. Absence of peers/{name}/identity.toml signals V7-only.

5.2 Identity-aware mode

Identity extension installed. identities/{name}/ becomes the bundle directory (controllers, agents, quorum constituents, identity.toml). peers/{name}/identity.toml references the bundle. See SDK-IDENTITY-INFRASTRUCTURE §8.4 for the full layout.

5.3 Migration

BootstrapFromExistingKeypair (SDK-IDENTITY-INFRASTRUCTURE §8.1) promotes a V7-only peer to identity-aware without changing the peer-id. Bundle is created in identities/{name}/, peers/{name}/identity.toml is added pointing at the bundle, the peer's keypair is unchanged. No retroactive migration of contacts.


6. Application state on disk — none directly

Worth saying explicitly because it counters a natural misconception:

Application state has no direct on-disk representation. When an application writes to app/{app-id}/workspace/windows/3/state, that's a write into the host peer's tree. The on-disk effect is "the peer's store.db got an updated row." There is no app-peer.db separate from any other peer's DB; there is no per-app filesystem directory. Apps live inside peer trees.

Implications:


7. Application startup sequence

Concretely:

  1. Construct the peer — load keypair, open DB, replay operational state from tree.
  2. Re-register handlers — for each handler the application provides, call register_handler so the in-memory dispatch index is populated. Steps 1 and 2 together produce a peer that's ready to serve requests.
  3. Re-establish subscriptions — the application's panels / watchers re-subscribe to the paths they care about.
  4. Open listeners (if the peer accepts incoming connections).

Steps 1 and 4 are SDK + peer-construction responsibility; steps 2 and 3 are application responsibility. Forgetting step 2 means handlers are silently unreachable until re-registration.


8. Operational notes

8.1 Filesystem permissions on Unix

Keypair files MUST be 0600 (existing convention; user-private). DB files: 0600 recommended (may contain identity-bound private data). Identity bundle files inherit the 0600 standard.

8.2 Backup and restore

Simple form: back up ~/.entity/ (or whatever the configuration directory resolves to). Restore by copying back; preserve the 0600 permissions on keypair files. Don't back up while the peer is live unless the storage backend supports hot backups (SQLite WAL mode does support this with the right flags; consult backend documentation).

Richer backup tooling (incremental backup, encrypted backup, off-machine custody) is operator territory and not specified here.

8.3 Web (entity-browser-rust) persistence

The configuration-directory abstraction maps to IndexedDB / OPFS / LocalStorage on browser WASM rather than a filesystem path. SDK-IDENTITY-INFRASTRUCTURE §8.0 covers the computation/I/O separation for identity bundles; the same pattern applies to the tree's storage backend — pure logic + platform-specific persistence layer. Specific WASM persistence specifics land when an impl commits to an approach.

8.4 Multi-peer per app

Per GUIDE-PEER-CONCERNS-AND-NAMESPACES.md §3 multi-peer-per-app variant: a native application MAY run multiple peers in one process (an internal app peer for UI state plus zero-or-more user-managed network peers for data). All those peers live under peers/{name}/ with different names; nothing special at the on-disk layer.


9. References

External:

Internal (this repo):