Identity and trust
A peer is an identity
There are no accounts, usernames or peer ids to manage. A peer is a VaultysID, and its address is the identity's DID:
const identity = await VaultysId.generatePerson();
const peer = await createPeer({ identity, transport });
peer.did; // "did:vaultys:01…"
The DID is derived from the identity's public keys, so nobody can claim it without the matching private key.
The SDK always uses VaultysID v1 DIDs. If you pass an identity in the older v0 format, createPeer switches it to v1, and peer.did is the v1 DID. The same key yields a different DID under each version, so mixing them would make peers unreachable.
Keeping your identity
The SDK never stores your identity's secret. Persisting it is your app's decision:
- Software key:
identity.getSecret()returns a string you can restore withVaultysId.fromSecret(). Store it somewhere your platform protects. - Passkey-backed (
VaultysId.createWebauthn()): the private key stays in the authenticator.
Every new session signs a handshake challenge. With a passkey-backed identity, that signature may need the user's approval each time a connection opens. This combination hasn't been validated with v2 yet, so test it for your use case before relying on it.
Proving who you are
When two peers connect, they run a mutual VaultysID handshake (the signed challenge exchange described in the protocol RFC). Each side signs a fresh challenge, so each side proves it holds the private key behind its DID.
Two checks happen on top of that:
- The side that dialed checks that the identity that answered is the DID it dialed. If anyone else answers (say, a misbehaving relay redirected the call), the connection fails with
IDENTITY_MISMATCH. - The side that was dialed learns the caller's DID from the handshake, never from anything the caller claims. Only then does it decide whether to accept.
Nothing else is exchanged before both checks pass. That's why message.from can be trusted: it's the DID the session proved.
Who may reach you
Only after an incoming peer has proven its DID, acceptIncoming decides whether to talk to it. The default accepts your contacts only:
await peer.contacts.add(bobDid, { name: "Bob" }); // Bob can now connect to you
You can replace that policy:
const peer = await createPeer({
identity,
transport,
// Ask the user about strangers; contacts get straight in.
acceptIncoming: async (did) => peer.contacts.has(did) || (await askUser(`${did} wants to connect`)),
});
| Policy | Use it for |
|---|---|
| Contacts only (default) | Messaging between known people |
| Ask the user | Chat apps that accept new contacts |
() => true | Open rooms, servers and bots that serve anyone |
A rejected peer gets REJECTED on its side. On yours, the attempt shows up as an error event, and nothing else happens.
The policy only applies to incoming connections. You can dial and message any DID; it's up to their policy whether they accept you.
Contacts
peer.contacts is a small, persistent address book. Records are plain data, independent of whether the contact is online.
await peer.contacts.add(did, { name: "Bob", data: { avatar: "🦊" } });
peer.contacts.get(did); // { did, name, data, addedAt }
peer.contacts.has(did); // true
peer.contacts.list(); // Contact[]
await peer.contacts.update(did, { name: "Robert" });
await peer.contacts.remove(did);
Contacts are saved in the peer's store. Pass indexedDBStore() or fileStore() to keep them across restarts.