Graft Objects
Graft repository mode is content-addressed, but it does not store ordinary file blobs. A Git blob usually stores file bytes. A Graft blob currently stores a SQLite database snapshot descriptor. The actual page data and storage logs live in volume storage; repository objects describe how to name, verify, and organize those snapshots.
.graft/ objects/ # repository objects: blob, tree, commit, tag refs/ # branch refs index.toml # staging area store/ # volume storage: pages, logs, storage commitsStart With An Empty Repository
Section titled “Start With An Empty Repository”mkdir /tmp/graft-objects-democd /tmp/graft-objects-demo
graft init app.dbfind .graft/objects -type fRight after initialization, the object database is usually empty. Graft has created the repository structure, but no repository history has been written yet.
Every repository object has an ID derived from its canonical bytes:
ObjectId = BLAKE3(canonical object bytes)If the object content changes, the ID changes. If the content is identical, the ID is identical.
SQLite Snapshot Blobs
Section titled “SQLite Snapshot Blobs”After changing a database and staging it:
graft sql "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"graft sql "INSERT INTO users (name) VALUES ('Ada')"graft add app.dbGraft writes a sqlite-snapshot-v1 blob. Its payload looks like this:
sqlite-snapshot-v1volume <VolumeId>page_count <PageCount>range <LogId> <start_lsn> <end_lsn>commit <lsn> <storage_commit_hash>commit <lsn> <storage_commit_hash>The blob does not contain a full copy of app.db. It records how to find and verify the database state inside volume storage:
volumeidentifies the logical database volume.page_countrecords the snapshot’s page count.rangeidentifies the storage log intervals needed for reconstruction.commitpins each LSN to the expected storage commit hash.
The outer repository object bytes are:
graft-object 1 blob <payload-bytes>\0sqlite-snapshot-v1volume <VolumeId>page_count <PageCount>...The object ID is the BLAKE3 hash of those canonical bytes.
Why Blobs Need Trees
Section titled “Why Blobs Need Trees”A blob answers “what snapshot is this?” It does not answer “where does this snapshot live in the project?”
That is the job of a tree object:
tree-v1160000 <blob-object-id> app.db160000 <blob-object-id> analytics/events.dbEach entry contains a mode, an object ID, and a path. In Graft, mode 160000 means this entry is a SQLite database snapshot entry.
Why Trees Need Commits
Section titled “Why Trees Need Commits”A tree describes one complete project state, but it does not record who created that state, what history it follows, or what message explains it. A commit object adds that information:
tree <tree-object-id>parent <parent-commit-object-id>author-name Graftauthor-email graft@example.invalidauthor-time <timestamp-ms>author-tz +0000committer-name Graftcommitter-email graft@example.invalidcommitter-time <timestamp-ms>committer-tz +0000graft-version <repository-format-version>
create users tableWhen you run:
graft commit -m "create users table"Graft reads staged database states, writes a tree, writes a commit that points at that tree and its parents, then moves the current branch ref to the new commit ID.
refs/heads/main | vcommit | vtree | vsqlite-snapshot blob | vvolume storage logs and pagesHow Objects Are Stored
Section titled “How Objects Are Stored”Loose objects use a fan-out path layout. If an object ID starts with:
9df6d4c4f0f8...Graft stores it at:
.graft/objects/9d/f6d4c4f0f8...Unlike Git loose objects, current Graft loose objects are written as canonical bytes rather than zlib-compressed content:
graft-object <version> <kind> <payload-bytes>\0<payload>When Graft reads an object, it decodes those bytes and recomputes the object ID. If the path ID and computed ID differ, the object is invalid.
Index And Volume Storage
Section titled “Index And Volume Storage”The index is the draft for the next tree. graft add app.db writes a snapshot blob and records a staged entry in .graft/index.toml. graft commit turns staged entries into a tree and clears the index.
The object database and volume storage have different responsibilities:
.graft/objectsstores repository history: blobs, trees, commits, and tags..graft/storestores physical database data: pages, logs, storage commits, and volume indexes.
A commit is therefore not a full database file. It is a verifiable route to one: commit -> tree -> sqlite snapshot blob -> volume storage.
To materialize a revision as a normal SQLite file:
graft export --source HEAD --output app.head.db app.dbsqlite3 app.head.db "SELECT * FROM users;"The exported file is an ordinary SQLite database snapshot. It is no longer the Graft volume itself.