Skip to main content

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.

VaultysID v1 only

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 with VaultysId.fromSecret(). Store it somewhere your platform protects.
  • Passkey-backed (VaultysId.createWebauthn()): the private key stays in the authenticator.
Passkeys and the handshake

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`)),
});
PolicyUse it for
Contacts only (default)Messaging between known people
Ask the userChat apps that accept new contacts
() => trueOpen 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.

Outgoing is always allowed

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.