Your identity and your home node
Two facts, both true, and the gap between them is the thing to understand:
- Your ZID is the same on every node. It is derived from your account equation — a deterministic function, not a record handed out by a server. Every one of the eighteen nodes computes the same ZID from the same equation, so you can sign in at any of them.
- Your data is on exactly one of them. Machines, envelopes, coins, pages and vault secrets are created on a node and stay there. They are not replicated (what does replicate).
So signing in "works everywhere" and finding your stuff does not. This page is about closing that gap.
Sessions are per-node
Each node issues, signs and verifies its own session. A session established at zeq.me is not a
credential at zeqproof.com — there is no shared session store, and none is planned. The same is
true of machine API keys: a zsm_… key is cryptographically scoped to one state machine on one
node.
Your plan is per-node too, and that surprises people. The read path is
COALESCE(users.plan, zeq_identities.plan, 'free') against the node's own database, so
"Architect on zeq.me, Free on zeqproof.com" is a real, reachable state rather than a bug — and
the directory below reports it explicitly.
The failure this produces is quiet rather than loud: signing in on a node that does not home you auto-provisions an empty identity. You get a working session, a valid ZID, and nothing in it. Nothing is lost; you are simply looking at the wrong node.
GET /api/identity/home/:zid
The directory endpoint. It asks every peer what it holds for your ZID, merges the answers, and names your primary home.
Gated to the owner. requireSessionOrMachineKey then requireParamZidOwner: you must be signed
in as that ZID, present an admin-scope machine key belonging to it, or be an active admin principal.
Anything else is refused:
curl -s https://zeq.me/api/identity/home/ZEQTEST
# {"ok":false,"error":"Authentication required.","code":"UNAUTHORIZED","redirect":"/auth/"}
With an admin-scope machine key:
curl -s https://zeq.me/api/identity/home/$ZID \
-H "Authorization: Bearer $ZEQ_MACHINE_KEY"
A ZID that is not yours returns 403 FORBIDDEN. The response is sent Cache-Control: no-store.
The response
{
"ok": true,
"zid": "ZEQ…",
"asked_from": "https://zeq.me",
"zeqond": 2301712920,
"primary_home": "https://zeqproof.com",
"you_are_home": false,
"homes": ["https://zeqproof.com", "https://zeq.me"],
"totals": { "machines": 3, "envelopes": 12, "coins": 2, "pages": 1 },
"plans": { "https://zeq.me": "free", "https://zeqproof.com": "architect" },
"plans_agree": false,
"nodes": [
{ "origin": "https://zeqproof.com", "reachable": true, "is_self": false,
"has_identity": true, "machines": 2, "envelopes": 11, "coins": 2, "pages": 1,
"plan": "architect", "weight": 16 },
{ "origin": "https://zeq.me", "reachable": true, "is_self": true,
"has_identity": true, "machines": 1, "envelopes": 1, "coins": 0, "pages": 0,
"plan": "free", "weight": 2 },
{ "origin": "https://zeqvm.com", "reachable": false, "is_self": false,
"has_identity": false, "machines": 0, "envelopes": 0, "coins": 0, "pages": 0,
"plan": "free", "weight": 0, "error": "unreachable" }
],
"unreachable": ["https://zeqvm.com"],
"note": "Your ZID is identical on every node; private state … lives on the node that created it."
}
| Field | Meaning |
|---|---|
primary_home | the node holding the most state for you — where you should be working |
you_are_home | true when the node you asked is also the node holding your state |
homes | every reachable node holding something for you; there can be more than one |
totals | machines, envelopes, coins and pages summed across every node that answered |
nodes[] | the per-node breakdown, including nodes holding nothing |
plans / plans_agree | your plan on each reachable node, and whether they match |
unreachable | nodes that did not answer in time — they may hold state you cannot see right now |
Ranking is stable no matter which node you ask. weight = machines + envelopes + coins + pages;
primary_home is the highest weight, ties broken by machine count and then by origin string. Ask
zeq.me or ask hulyasmath.com — the same node comes back first.
An unreachable peer degrades the answer, never the request. A node that is down or slow is
reported as reachable: false with an error, and a node whose own query fails reports empty
counts rather than failing your read.
GET /api/identity/peer/presence/:zid
The node-to-node half. You will not call this directly; it exists so the endpoint above has something to fan out to.
- Signed, not public. It is verified through the
signed HTTP peer protocol:
a framework-owned origin, a zeqond within ~4 minutes of the receiver's, and an Ed25519 signature
over the canonical string
`peer-presence|<ZID>|<zeqond>`checked against the caller's publishedpublicKeyHex. - Counts only, never contents. The query counts identity rows, machines, active envelope tokens, active coin tokens and pages, and reads the plan. No envelope body, no coin, no page, no key material, no vault secret crosses the wire.
curl -s https://zeq.me/api/identity/peer/presence/ZEQTEST
# {"ok":false,"error":"not_a_peer"} ← unsigned, refused
A malformed ZID is rejected before anything is read (400 bad_zid).
What to do when you sign in and your machines aren't there
Do not create them again. A second machine under the same ZID on a second node is a second machine, not a recovered one, and now you have your state split across two homes.
-
Ask the directory. From the node you are signed in to:
curl -s "https://$THIS_NODE/api/identity/home/$ZID" \-H "Authorization: Bearer $ZEQ_MACHINE_KEY" \| python3 -c "import sys,jsond=json.load(sys.stdin)print('primary home :', d['primary_home'])print('you are home :', d['you_are_home'])print('all homes :', ', '.join(d['homes']) or '(none)')print('totals :', d['totals'])if d['unreachable']: print('unreachable :', ', '.join(d['unreachable']))if not d['plans_agree']: print('plans differ :', d['plans'])" -
Go to
primary_homeand work there. That is where your machines, envelopes and coins are. Sign in on that node — same equation, same ZID, a session issued by that node. -
If
primary_homeisnullandunreachableis empty, no node holds anything for this ZID. That is a genuinely new identity, and creating a machine is the right move. -
If
unreachablelists nodes, wait before concluding anything. A node that did not answer may be holding everything you are looking for. There is no read path around a down node, and no amount of retrying from elsewhere will produce its rows. Re-run the directory call when it is back. -
If
plans_agreeis false, your paid plan is set on one node and you are looking at a different one. Move to the node inplansthat carries the plan, or ask an operator to set it where you actually work. -
Expect more than one home.
homesis a list. Signing in on several nodes over time really does scatter small amounts of state, and the directory is how you see it — it does not tidy it up for you.
What this is not
This is a directory, not a merge. It moves no rows, copies no envelopes and changes no data model. It makes the location of your state knowable instead of guessed, which is a strictly smaller claim than making it available.
Replicating private state across nodes — so that a down node stops meaning unreadable data — is an open decision, not a shipped feature. Anything you read that implies your machines follow you between nodes is describing an intention. Until it ships, the honest rule is the one at the top of this page: your ZID is everywhere, your data is in one place, and this endpoint tells you which place.
Read next
- One machine on eighteen nodes — what is shared and what is not.
- The spine and what replicates — the signed peer protocol this endpoint rides on.
- ZeqID and the account equation — why the same equation yields the same ZID on every node.
- Data sovereignty — what you can carry between nodes today.