Finding other nodes
Your node is one person’s operating system. The world is the mesh between nodes — and a mesh only works if people can find each other.
Until now they could not. First contact was out of band: somebody sent you an address
(did:key:z6Mk…) and a URL, you typed both into Peers, and your node did the rest. That works,
it is honest, and it does not scale past people who already have each other’s phone numbers.
So there is a directory now, and Vocabotics runs it.
A directory is a lookup, not an authority
This is the whole design and it is worth reading once before anything else.
Every being and every node in Novaterra is addressed by a public key. That means a document either verifies under the address you asked about, or it does not — and nobody else’s opinion enters into it. A directory therefore cannot do anything except say where to look.
Concretely:
- The endpoint you get back is inside a document your node’s owner signed, not a field the directory holds. If the directory edits it, the signature stops matching and your node refuses it.
- If the directory re-signs the edited document with its own key, your node refuses it for a different reason: the only key entitled to say where a node is, is that node’s own key.
- If the directory hands you somebody else’s perfectly genuine record in answer to your question, your node notices that it is not the address you asked about and refuses that too.
So a directory that has been compromised, seized or is simply lying can withhold, delay and serve stale answers. It cannot forge one. That is the bound, and it is why accepting a central service here is a reasonable trade rather than a quiet surrender.
If you want to see this rather than take it on trust, it is
tests/directory/forgery.test.ts in the repository: eight attacks by a hostile directory, all
refused.
Publishing where your node is
Settings → Node, or from a terminal:
curl -X POST http://localhost:4000/api/directory/register \ -H 'content-type: application/json' -b "$COOKIE" \ -d '{"endpoint":"https://mynode.example","handles":["ash"]}'Your node signs a short document saying “I am this address, I am reachable here, and I answer to these names”, proves it holds the key by answering a random challenge the directory mints, and the directory stores the document verbatim. It cannot register anybody else, and nobody — including Vocabotics — can register on your behalf.
A registration is good for seven days and then stops being served. That is not a policy the directory invented: the expiry is inside the bytes your node signed. Your node renews it; a node that has been switched off for a fortnight quietly disappears from the directory and reappears when it comes back.
DELETE /api/directory/register takes you out again. It needs the same proof registering did.
Handles are a hint, never a name
You can claim ash. So can somebody else, and the directory will let them, and a search for ash
will return both with a note saying so.
That is deliberate. A directory that could decide which of two claimants is the real @ash would be
a name authority — something that can be seized, subpoenaed, sold, or simply mistaken, and which
every node would then have to trust for the one question identity already answers for itself.
The naming that means something in Novaterra is a petname: a name you give an address, on your node, meaningful to you and to nobody else. See Moving your node for how addresses travel; petnames travel with them.
Looking somebody up
curl "http://localhost:4000/api/directory/lookup?q=did:key:z6Mk…" -b "$COOKIE"curl "http://localhost:4000/api/directory/lookup?q=ash" -b "$COOKIE"Every result carries your node’s own verdict, not the directory’s:
{ "nodeDid": "did:key:z6Mk…", "endpoint": "https://theirnode.example", "trusted": true, "reason": "The endpoint is signed by the address it belongs to."}A result that did not hold up is still returned, with trusted: false and a sentence saying
which check failed. A tampered answer must not look like an absent record; those are very different
pieces of news.
Nothing connects automatically. A lookup gives you an address and a URL. Adding a peer is still
POST /api/federation/peers — still your decision, made by you, with the same handshake that
existed before there was a directory. Discovery that befriends on your behalf is discovery that
decides who you talk to.
What a lookup tells Vocabotics
Asking “where is Ash” tells the directory that you want Ash. There is no way around that in a service anybody queries, so here is exactly what is and is not true.
What it stores. A counter and a date on the record that was found — “this address has been looked up 47 times, most recently on 2026-09-07”. That is enough to tell a dead record from a live one and nothing else.
What it cannot store. Anything about you. There is no column for a requester, an IP address, a user agent or a session, in any table in that service, and the tests assert the absence rather than the intention. The date is a date rather than a timestamp on purpose: minute-resolution times are a correlation channel between two records looked at moments apart.
Why the lookup is not authenticated. Requiring your signature would tell the directory precisely who wanted whom, which is strictly worse. The price is that lookups are cheap to scrape — which they would be regardless, because everything a lookup returns is a document its subject chose to publish.
What this does not protect against, plainly. The service sees the query, and whatever proxy sits in front of it sees your IP address. Nothing in the Novaterra codebase controls that layer. An operator determined to build the social graph could log it at the proxy, and no schema decision would stop them. These are structural mitigations, not cryptography.
Making lookups genuinely private is possible and is not built. The cheapest honest answer is bulk
transfer — your node downloads the registry and searches it locally, which is private by
construction and stops scaling somewhere in the tens of thousands of nodes. The options and their
costs are written down in services/directory/README.md so that the next person does not have to
guess which of them was considered.
Credits, and a node that was asleep
Vocabotics is the merchant of record for credit packs: it takes the payment, and it owes your node credit afterwards.
Your node does not have to be reachable for that to work, and this is the part worth understanding. When a payment settles, the directory queues a signed grant addressed to your node. It does not try to deliver it. It has no way to contact your node at all — no callback, no push, no outbound connection to a node anywhere in the service. Your laptop can be shut for a fortnight.
When your node next asks:
curl -X POST http://localhost:4000/api/directory/credits/claim -b "$COOKIE"it verifies each grant, checks it is addressed to this node, and posts it to your node’s own
append-only ledger. Asking twice is harmless: the grant carries an id, the ledger refuses a second
entry under it, and the answer says alreadyHeld rather than failing.
The honest part
Everything else in this system is a node making a claim about itself, which you can check. A credit grant is not. It is Vocabotics saying that money arrived, and your node can verify that the directory it chose signed the statement — not that the payment happened.
That is inherent to somebody else being merchant of record, and it is the one piece that does not
become distributed later even if discovery does. Cross-node value needs a settlement design that
does not exist yet (design/OS-PLAN.md §5.1); until it does, this is a place where Vocabotics is
load-bearing, and saying so is better than letting you find out.
Running without a directory
Leave DIRECTORY_ENDPOINT unset. Everything else works exactly as before: peers added by hand,
addresses exchanged out of band, messages signed and verified with nobody in the middle. The
directory routes answer “no directory is configured on this node” and nothing else changes.
You can also run your own — it is services/directory in the repository, one Fastify process and a
SQLite file. Your node pins whichever directory it first registered with by that directory’s own
address, so a directory that changes its key is refused rather than followed.
Running your own
pnpm --filter @novaterra/directory dev # http://127.0.0.1:4400Then on each node, DIRECTORY_ENDPOINT=http://127.0.0.1:4400.
services/directory/README.md has the environment variables, the route list, and — the part most
worth reading — exactly what would have to change if this were replaced by peer-to-peer gossip. It
is four items long, and none of them is “rewrite trust”. That was the design goal.