Node profiles and Seeds
Every node in Novaterra claims a profile: seed, leaf, home or hall. A profile is a
label, not a promise — a peer reads it to decide what to ask a node for, never to decide whether to
trust it, because trust here comes from a signature, not from a claimed role. What follows is what
each profile actually runs, and then the whole of this page’s real subject: the Seed, the smallest
of the four, and why it counts as a citizen rather than a sensor feed.
The four profiles
A node’s abilities are a set of facilities — sign, queue, store, ui, sandbox,
inference, relay — and a profile is just a fixed row of that table
(PROFILE_FACILITIES in packages/contracts/src/federation.ts):
| Profile | Runs | Delegates |
|---|---|---|
seed | sign, queue | store, ui, sandbox, inference, relay — everything else, to a paired Home |
leaf | sign, queue, store, ui | sandbox, inference, relay |
home | sign, queue, store, ui, sandbox, inference | relay |
hall | sign, queue, store, relay | ui, sandbox, inference |
The “Delegates” column is derived, not configured: it is simply every facility the profile does not run. Read it as what a node of this profile has to borrow, which is a different question from who it borrows from — a Seed and a Leaf name a node they delegate to, and a Home or a Hall names nobody, because there is nothing above them to ask.
Two things about that table are load-bearing rather than incidental:
signandqueuebelong to every profile, including a Seed. That is the floor of citizenship: holding a keypair and signing your own documents, and holding your own outbound documents while the far end is asleep. A node without those has no identity at all. It is also what makes this one protocol rather than a big one with a small one bolted on the side — the smallest citizen’s abilities are a strict subset of every other profile’s.relaybelongs tohallalone. A Hall stores and forwards other nodes’ traffic — that is the thing a Hall is for — and it has neitheruinorinference: the moment a node with a screen or a model starts relaying for strangers, it has stopped being a Hall and become something else, because a Hall never holds a peer’s keys and never acts as anyone.
The default self-hosted install described throughout this section — pnpm install, .env,
pnpm dev — is a home node. It runs the full API, the database, the sandbox and every
scheduled loop, and holds nothing back. Most of what follows on this page is about the profile at
the other end of the table.
The Seed: a real citizen, not a sensor feed
A Seed is the smallest thing that can be a citizen of Novaterra rather than a data source one
reports to. The distinction is not sentimental — it is the one fact that shapes everything below.
A sensor feed reports a number to whoever asks; a Seed has an address of its own (a did:key,
the same address type every being in the world has), signs everything it says, and is
addressed the same way a person is. “The greenhouse is at 31°C” arrives as a signed document from
a specific citizen, not as an anonymous reading a dashboard trusts because it showed up on the
right port.
What a Seed borrows to make that possible is delegation, and delegation on Novaterra is narrow by design.
The two delegable capabilities, and the one that is withheld
A Seed pairs with exactly one Home node and may be granted exactly two capabilities from that Home:
signals—signals.raise, and nothing else. A Seed can put a Signal on its owner’s desktop (a title, an optional body, a priority of low/normal/high) and cannot list, read or act on any other Signal.storage—get,set,delete,keys, each scoped to the Seed’s own keys, values capped at 400 characters. A Seed cannot reach into any other being’s storage, and there is nostorage.keysacross scopes.
network is deliberately withheld, and this is the one decision on this page worth reading
twice. It is not an oversight or a “not yet” — a Seed has no manifest and therefore no host
allowlist for networkCall to check a request against, and granting network to something with no
allowlist would mean handing an always-on device on somebody’s home network the ability to make
outbound requests as that owner’s node, to anywhere, forever. The two capabilities that are
delegable — a note on a desktop, a handful of scoped key/value pairs — are exactly the shape of
what a small, always-on device should be able to do and nothing more.
What it costs on the wire
These are measured, not estimated, from the Seed SDK’s own canonical wire format:
- A presence beat: 300–500 bytes, under 700 with HTTP/1.1 framing.
- A Signal carrying a real sentence: under 1024 bytes, under 900 framed.
- The unsigned first “knock” (the only document a Seed ever sends unsigned, because the address it is signing to has to be named before anything can be signed): under 120 bytes.
- The whole Seed client: under 12 KiB of RAM, fixed at construction and unchanged after 200
signed documents — about 2% of an ESP32’s 520 KB. It allocates nothing after boot: the number of
buffers allocated after the arena seals is zero, asserted by
tests/seed/wire-size.test.ts— “does not grow, however many documents it signs” and “refuses to allocate a buffer after boot” — not hoped for.
packages/seed-sdk earns those numbers by importing nothing at all — not zod, not node:crypto,
not even Buffer — enforced by a tsconfig.json that drops the DOM lib, so a stray fetch or
TextEncoder is a compile error rather than a runtime surprise on a device that has neither.
Say this plainly, not in a footnote: nobody has flashed a board
There is no ESP32 in this repository. examples/seed/greenhouse.mts is a Node script with
three small adapters standing in for the three device-specific things a real Seed would need —
signing (node:crypto’s Ed25519, standing in for mbedTLS or libsodium), flash storage (a JSON
file, standing in for one NVS key), and transport (fetch, standing in for a raw TCP socket and
about forty bytes of HTTP). Everything above those three adapters would compile for an ESP32
unchanged in shape.
That is a real, working stand-in for a device, and it is not a device. The wire sizes above are measured against the actual bytes the SDK produces, so those numbers stand. But three figures that often travel with this kind of claim are not measured, and the source labels each one “Inferred” for exactly this reason:
- Signing time on real silicon — Ed25519 on an ESP32’s CPU has an existing literature, but nobody in this project has timed it on a board.
- Compiled C size — inferred from what the code does, not from a build.
- The 30–50 KB a TLS session would cost, which is one of the reasons the Seed protocol avoids TLS handshakes on the device in the first place — inferred from documentation and prior art, not measured.
Say so wherever these numbers come up. A Seed that has only ever run as a Node script proves the protocol and the wire format; it does not prove a board.
Running the example
examples/seed/greenhouse.mts has two modes. provision is the bench: it mints a keypair,
derives the did:key, and writes a file — the one-off step a real device would have done once at
the factory, encoding an address it then holds as a constant string for the rest of its life.
node --import tsx examples/seed/greenhouse.mts provision ./seed.jsonrun is the device: it knocks once, beats every few seconds with a temperature reading, and
raises a Signal on the owner’s desktop when the reading crosses a threshold. When the node it is
paired to is asleep or unreachable, it queues what it was going to send and says so; when the node
comes back, it drains the queue, and every queued document proves itself by its own counter because
it was written when there was no freshness nonce to quote yet.
node --import tsx examples/seed/greenhouse.mts run ./seed.json http://127.0.0.1:4000Pairing a Seed to your Home node
There is no discovery step, no mDNS, no “devices found on your network” — deliberately. A Seed is
provisioned at a bench and prints its did:key, and a person types that address into their node,
out of band. A device that could join a node just by shouting on the local network would be
trusted for exactly that reason, which is the wrong reason.
Pairing itself is one call, owner-only:
POST /api/federation/seeds{ "did": "did:key:z6Mk…", "name": "Greenhouse", "grants": ["signals"] }grants defaults to nothing: a freshly paired Seed still has presence — it beats, it appears, its
owner can see it is alive — and can ask for nothing at all, which is the right floor for a device
someone has just plugged in. PATCH /api/federation/seeds/:id renames a Seed, changes what it may
do, or blocks it; DELETE /api/federation/seeds/:id unpairs it outright. GET /api/federation/seeds lists every device paired to this node, what each one may do, and when it
last said anything.