Skip to main content

Your identity and your home node

Two facts, both true, and the gap between them is the thing to understand:

  1. 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.
  2. 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."
}
FieldMeaning
primary_homethe node holding the most state for you — where you should be working
you_are_hometrue when the node you asked is also the node holding your state
homesevery reachable node holding something for you; there can be more than one
totalsmachines, envelopes, coins and pages summed across every node that answered
nodes[]the per-node breakdown, including nodes holding nothing
plans / plans_agreeyour plan on each reachable node, and whether they match
unreachablenodes 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 published publicKeyHex.
  • 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.

  1. 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,json
    d=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'])
    "
  2. Go to primary_home and 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.

  3. If primary_home is null and unreachable is empty, no node holds anything for this ZID. That is a genuinely new identity, and creating a machine is the right move.

  4. If unreachable lists 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.

  5. If plans_agree is false, your paid plan is set on one node and you are looking at a different one. Move to the node in plans that carries the plan, or ask an operator to set it where you actually work.

  6. Expect more than one home. homes is 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.