Auth
Identities
The identities directory is the account-global, device-local cache of every account identity this account has encountered — across spaces, one-to-one chats and inbox invites. It is where you turn an id you hold (a chat message's creator, a mention, a 1-1 peer) into a display name and icon.
Endpoints
Account-scoped — these routes sit outside the /v1/spaces/:spaceId group.
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/identities |
every known identity |
| GET | /v1/identities/:identity |
one identity; 404 identity.not_found when never seen |
| GET | /v1/identities/subscribe |
live directory changes over SSE |
curl -s http://127.0.0.1:7001/v1/identities
{ "identities": [
{ "identity": "A5k…",
"name": "Alice",
"iconCid": "bafy…",
"spaceIds": ["bafyspace1…", "bafyspace2…"] } ] }
| Field | Meaning |
|---|---|
identity |
the account id |
name, description, iconCid |
the last resolved profile — omitted until resolved |
spaceIds |
spaces where the identity is currently seen; pruned when you leave or offload a space |
any identities list # alias: any contacts
any identities get A5k…
any identities subscribe
Profiles are encrypted
A profile is pushed to the network encrypted with an account-derived key. That key reaches a contact only through a channel that is already encrypted — a shared space's ACL metadata, or a one-to-one invite. Until it arrives, the directory row surfaces id-only: name, description and iconCid are empty. Once the key is in, a background fetch resolves the profile and the row updates.
The same applies on a freshly restored device: every contact starts id-only and fills in as keys resolve in the background.
Why it matters. The sync nodes store profile bytes they cannot read. A relay learning "who talks to whom" never learns who those people are; only accounts you have actually shared a space or a direct chat with can decrypt your name.
The synced decryption key behind each row is never exposed over HTTP. The tech-space dataset that stores it is deliberately excluded from the generic space-list query — GET /v1/identities is the one read path.
Tolerate empty names
Render a fallback (a truncated id, a generated avatar) and let the subscribe stream fill the name in. Never block a UI on a resolved name, and do not look for a "fetch raw profile bytes" path — there is none; the directory is the resolution surface.
Live updates
GET /v1/identities/subscribe uses the same envelope as the members and sync-status streams:
event: ready
data: {}
event: identities
data: { "added": [ { …IdentityInfo… } ],
"updated": [ { …IdentityInfo… } ],
"removed": [ "A5k…" ] }
event: lagged
data: { "total": 3 }
event: closed
data: { "reason": "server_shutdown" }
One identities frame per change batch; any of added / updated / removed may be empty or absent. A contact whose key arrives after the first sighting appears first in added with an empty name, then again in updated once resolved. On lagged, re-GET the list to resynchronise.
No rights here
The directory carries no permission field by design. Roles are per-space and live on the members list:
GET /v1/spaces/:id/members # roster + roles for ONE space
GET /v1/identities # global id → profile, across all spaces
To show "Alice is an admin of space X", read space X's members. To show her role in every shared space, iterate her spaceIds and read each members list — there is no cross-space role rollup.
| Question | Surface |
|---|---|
| Who is in this space, with what rights? | GET /v1/spaces/:id/members |
| What does this account id look like? | GET /v1/identities/:identity |
| Keep an avatar cache fresh | GET /v1/identities/subscribe |