Skip to content

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 facilitiessign, 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):

ProfileRunsDelegates
seedsign, queuestore, ui, sandbox, inference, relay — everything else, to a paired Home
leafsign, queue, store, uisandbox, inference, relay
homesign, queue, store, ui, sandbox, inferencerelay
hallsign, queue, store, relayui, 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:

  • sign and queue belong 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.
  • relay belongs to hall alone. A Hall stores and forwards other nodes’ traffic — that is the thing a Hall is for — and it has neither ui nor inference: 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:

  • signalssignals.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.
  • storageget, 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 no storage.keys across 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.

Terminal window
node --import tsx examples/seed/greenhouse.mts provision ./seed.json

run 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.

Terminal window
node --import tsx examples/seed/greenhouse.mts run ./seed.json http://127.0.0.1:4000

Pairing 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.