Field guide · TreeDB
The topics a treedb keeps for itself.
Three topics you never declare, and must never write. Each one has a door, and the door is the whole interface.
Open any treedb and it will already contain topics you did not put in the
schema. Their names start with a double underscore, they are marked
system_topic, and delete-topic refuses them by
name. They are the store's own machinery: the ledger of its snapshots, the
drawing of its graph, the index of its bytes.
A system topic is not a topic you write. The system owns it, keeps
it consistent with things that live outside it — a flag inside every
record's metadata, a file on disk, a schema in another treedb — and
opens a door onto it: a command, a Save button, the write path of a
column. Reaching past the door with create-node,
update-node or delete-node on the topic itself is
not using the treedb. It is hacking it, and what breaks is not the row you
edited.
Every system topic is the visible half of something. Write the visible half by hand and the other half does not follow: the tag stays on the records, the bytes stay on disk, and the store is now telling two different stories.
__snaps__ — the ledger of snapshots
A snapshot in a treedb is not a copy of anything. It is a
number stamped into the records themselves — and
__snaps__ is the ledger that says which number means what.
id IS the tag
topic_version 3
| Column | Flags | What it holds |
|---|---|---|
id |
rowid | Assigned by the store, and it is the tag. It rides the
user_flag of every record the snapshot froze — one
uint16_t per record instance, so ids stop at
0xFFFE and 0 means “no snapshot”. |
name | required | What you called it. Shooting a second snapshot under a name already in the ledger is refused. |
date | required | UTC, as a string — 05 Sep 2026 17:04:11 +0000. It is
a label to read, not a field to sort on. |
active | required | The one bit that matters at run time: which snapshot the treedb loads at its next open. At most one row carries it. |
description | Why you shot it. The only part a person writes, and they write it
as an argument to shoot-snap. |
The doors
shoot-snap name=<n>
Adds the row, then stamps its id on the current record of every
topic. Idempotent by name: an existing name is refused, not overwritten.
activate-snap name=<n>
Moves the active bit. It changes what the treedb will
load, so it takes effect on the next open — which is why the agent, whose
own snapshots work the same way, ends its version by restarting what is running.
deactivate-snap
Clears the bit (activate-snap name=__clear__ in the API):
back to the live tree.
snaps
Reads the ledger. The only door that is safe to hold open.
snap-content [topic_name=<t>]
What a given snapshot actually holds, by topic. Takes the snap by
name or by snap_id.
delete-node on a snap row
The one hand-written call the design intends: there is no
delete-snap. Dropping the row is how a snapshot is discarded, and it
is what frees the assets only that snapshot was holding.
active yourself, and never edit an id.
The bit and the tag are two halves of one fact: the ledger says
“tag 7 is the snapshot called pre-upgrade” and thousands
of records carry the 7. Renumber the ledger and every one of those records now
names a snapshot that does not exist.
The mechanism is worth following once, because it explains both the
uint16_t and the reload.
__ are skipped: the machinery
is not part of the picture it takes.
user_flag, so a second snapshot
cannot share it: a clone is appended with the new tag and the original keeps the
old one. This is the only case where a snapshot costs storage — and it is why
the older snapshot still loads.
active bit moves; nothing else happens yet.
The tag is a filter applied when the treedb loads its topics
(user_flag == tag, newest instance per key). A running treedb has
already loaded.
__graphs__ — where the drawing is kept
Every treedb is a graph: topics are nodes and hook/fkey
columns are edges. That much is derived from the schema and needs no storage.
What cannot be derived is where a person decided each card should sit —
and that is all __graphs__ holds.
| Column | Flags | What it holds |
|---|---|---|
id | rowid | In practice the name of the topic being drawn: the writer sends it, so a second Save updates the same row instead of adding one. |
topic |
requiredwritable | Which topic this layout belongs to. What the reader indexes by. |
active | writable | Whether the layout is applied. A row that is not active is kept and ignored — which is how a saved arrangement survives a spell of automatic layout. |
time |
timewritable | When it was saved. |
properties blob |
writable | The drawing itself: nodes keyed by node id with their
x, y and size, edges with their
styles, and __origin__ — the node_uuid of the
machine whose user arranged it. |
The doors
manual reads these rows back; dagre ignores
them and re-arranges on every load. Choosing manual is what makes a Save
mean anything.
update-node on __graphs__, with create
set. It can afford to, because it is the one with nothing on the other side to keep
in step — no tag in the records, no bytes on disk. Writing it by hand still gains
you nothing, and can lose you something: __graphs__ is deliberately
excluded from the topics a graph draws, so a row for a topic that is not there is a
card of nothing.
It is the only system topic with no other half. Delete every row and you lose an arrangement, not a fact — the graph comes back laid out automatically, and somebody drags it into shape again.
/* what one Save writes -- one row, all of a topic's cards */ { "id": "devices", "topic": "devices", "active": true, "properties": { "nodes": { "dev-0417": {"x": 240, "y": 96, "size": [180, 60]}, … }, "edges": { … }, "__origin__": "a4f1…" // the node_uuid that arranged it } }
__assets__ — the index of the bytes
The third one, added in 7.18.0, is the index of the files a
file column points at: one row per content, keyed by the
sha256 of the bytes, with the bytes themselves in .blobs/ beside
the treedb. Its hooks are not declared either — they are derived from the
file columns of your own topics.
It has the strictest door of the three, because it is the one with real bytes on the other side: you never write it at all. You write your topic, handing the file over in the same message, and the write path does the rest. There is a whole page on that one: the life of a file in a treedb.
.blobs/
topic_version 1
The doors
create-node / update-node
On your topic, with the file beside the record. The write path
stores the bytes, writes the row and links the column itself.
import-assets
A whole directory the node already has. Not one byte crosses the
network.
get-asset
Reads one back: a signed URL where the service can sign, the bytes
inline where it cannot.
gc-assets
Takes what no live node and no snapshot holds — and the bytes no
row names. Run it with dry_run=1 first.
__assets__ row and its blob are one object in two places, and
only the write path keeps them together. A row written by hand names bytes that are
not there; a row deleted by hand can take bytes another treedb of the same tranger is
still using — which is exactly why delete-node on an asset runs the
collector's own guards, and why force overrides none of them.
Three rules that hold for all of them
-
You never declare them and you never version them. They are created by
treedb_open_db()on every open, with their owntopic_versionthat belongs to the framework. Yourschema_versionhas nothing to say about them. -
delete-topicrefuses them. They carrysystem_topic: true, persisted with the topic and tested before anything is removed. That is a guard, not a suggestion. -
The
__prefix is load-bearing. It is whatshoot-snaptests to skip a topic, so the machinery is never part of the picture it takes. Do not name a topic of your own that way — and do not confuse these with__system__, which is not a topic at all but a separate treedb, the one that holds every treedb's schema as data.
Source: kernel/c/timeranger2/src/tr_treedb.c —
treedb_open_db() creates both topics inline,
treedb_shoot_snap() and treedb_activate_snap() are the
snapshot mechanism, and treedb_delete_topic() holds the
system_topic guard. The only writer of __graphs__ is
kernel/js/gobj-ui/src/c_g6_nodes_tree.js.
Companion page: the life of a file in a treedb.