Format version 1 · api pdo-v0

Inside a .obj file.

An object file is an ordinary SQLite database. Open it with sqlite3 and you can read all of it. Seven tables whose names start with _obj_ belong to the runtime. Every other table belongs to the object.

The runtime’s half

Object code cannot read or write these. The runtime and the MCP tools can. Migrations cannot touch them either.

  1. _obj_meta (key, value)

    format_version (1), api_version (pdo-v0), name, class, main (src/index.ts), created_at and live_checkin, the id of the code that runs.

  2. _obj_blobs (hash, size, data)

    Every source file version, keyed by its SHA-256. A file that does not change between check-ins is stored once.

  3. _obj_checkins (id, parent, manifest, message, created_at, schema)

    Fossil-style check-ins: a manifest maps each path to a blob hash, parent links the history, and the id is the SHA-256 of the check-in. schema snapshots the data tables the code was released against, which is what rollback checks.

  4. _obj_migrations (seq, name, sql, checkin, applied_at)

    Ordered SQL migrations, each tied to the check-in that brought it. They only move forward; nothing undoes one.

  5. _obj_kv (key, value)

    The key-value half of storage. Values are V8-serialized, so anything structuredClone accepts round-trips.

  6. _obj_alarms (id = 1, scheduled_at, retry_count)

    At most one row: when alarm() is due next. ctx.storage.setAlarm writes it.

  7. _obj_changes (id, at, event, checkin, changeset)

    Every event that writes runs under a SQLite session; its changeset (data tables and _obj_kv) is stored here in the same transaction, with the check-in that produced it.

The object’s half

Data tables are created by migrations and nothing else. Object code reaches them through ctx.storage.sql.exec; agents read them with query_data.

A declared PRIMARY KEY on every table. A migration that creates one without is refused.

No AUTOINCREMENT. Use UUIDv7 text keys, so copies of a file can merge changesets later. The to-do example ships a 14-line uuidv7().

Forward only. Migrations run in order, are recorded with the check-in that brought them, and are never undone.

Code and history

A check-in is a manifest from path to blob hash, a parent, a message and a time. Its id is the SHA-256 of those. _obj_meta.live_checkin names the one that runs.

Releasing writes one squashed check-in, runs its migrations and moves live_checkin, all in one transaction. Rolling back moves live_checkin to an older check-in after comparing the schema it was released against with the tables as they are now.

-- what runs
SELECT value FROM _obj_meta WHERE key = 'live_checkin';

-- its files
SELECT manifest FROM _obj_checkins WHERE id = :live;
-- {"src/index.ts":"9f2c…","src/uuid.ts":"41ab…"}

SELECT data FROM _obj_blobs WHERE hash = '9f2c…';

The preview copy

propose_change copies the live file to <file>.preview, data included, and applies the change there. test_call and query_data with target: "preview" work on that copy. release_change replays the migrations on the live file, and is refused if live moved since the preview was made.

Writes leave a trail

Each event is one SQLite transaction with a session attached. When it writes, its changeset goes into _obj_changes before commit, labelled with the event kind (fetch, rpc, alarm) and the check-in that ran.