docs

Ease docs/Concepts/Contacts

Contacts: the contact graph

Who the user knows. The graph that gates DM eligibility and group membership and drives the approval step on adding members. Send a request, accept it, become mutual contacts; from there the rest of the SDK opens up between you and the other party.

What's there

  • list(): established contacts.
  • sendRequest(target, welcomeMessage?): by user id, email, or phone.
  • incomingRequests() / outgoingRequests(): pending state, inbound and outbound.
  • accept(requestId) / decline(requestId) / rescind(requestId): resolve a request.
  • remove(userId): drop an established contact.
  • block(userId) / unblock(userId): the blocking primitive.

The request flow

Alice sends a request to Bob. Bob's SDK receives a contactRequestReceived event. Bob accepts. Alice's SDK receives a contactRequestAccepted event. From that point on, Alice and Bob are mutual contacts. DMs work, group membership works, the rest of the SDK opens up between them.

TypeScript · Web SDK
// Alice's side
await ease.contacts.sendRequest(
  { email: 'bob@example.com' },
  "Hey Bob, let's chat",
);

ease.contacts.onContactRequestAccepted((event) => {
  console.log('Bob accepted; his user id is', event.contactUserId);
});

// Bob's side
ease.contacts.onContactRequestReceived((event) => {
  // Show a UI prompt; on accept:
  ease.contacts.accept(event.requestId);
});

Subscribe to the request events before you open the socket so no inbound request slips past the listener. The received-request handler gives you a request you resolve with accept(requestId); accept returns the new contact's user id once the request settles.

The welcome message

The optional welcomeMessage on sendRequest is encrypted to the recipient before send. The server sees ciphertext; the recipient sees plaintext alongside the request. Keep it short. It is the first impression in your recipient's pending list.

Lookup and filtering

Two reads on the contact graph: strict lookup by handle to translate an email, phone, or username into a user id, and client-side filtering over the listed contacts.

Lookup by handle

You have an email, phone number, or username and you want the userId behind it. Useful when you're about to send a contact request and the form on your side collects an address, not an opaque id.

TypeScript · Web SDK
const { userId } = await ease.contacts.lookup({ email: 'bob@example.com' });
// Resolves the handle to a user id. Also accepts { phone } or { username }.

Strict, single-result. Most apps skip the explicit lookup and let sendRequest({ email }) resolve it inline. The lookup is exposed for cases where you want the userId first.

Filtering the contact list

For finding a contact by name, list() returns the full set and the client filters. The SDK ships deterministic ordering, so substring filters render predictably.

TypeScript · Web SDK
const all = await ease.contacts.list();
const matches = all.filter((c) =>
  c.name?.toLowerCase().includes('alice'),
);

Online presence

Every contact row carries a live presence snapshot. Three fields:

  • online: true while the contact has at least one socket connection open; false while they're away.
  • lastSeenAt: Unix seconds, set when the contact disconnects; null while they're online.
  • statusText: an optional free-form status the contact set on their profile.

Subscribing to changes

The SDK pushes a presence event whenever a contact connects or disconnects:

TypeScript · Web SDK
const unsub = ease.contacts.onPresenceUpdate((evt) => {
  // evt.userId, evt.online, evt.lastSeenAt, evt.statusText
  // Re-render the contact row's online dot, or trigger a notification.
});

// later
unsub();

The initial snapshot

The first time a session opens, the server pushes a single snapshot with every current contact's state. Subscribe to it on launch so you don't have to wait for a contact to change state before you see them online:

TypeScript · Web SDK
ease.contacts.onPresenceSnapshot((rows) => {
  // rows is one entry per contact
});

After the snapshot lands, onPresenceUpdate carries the deltas. The two listeners are independent; a typical UI subscribes to both. An onAvatarUpdated listener rounds out the row: it fires when a contact changes their profile image.

Blocking

block(userId) removes them from your contacts, if they were one, and prevents any future contact request, DM, or group-add from the blocked user. unblock(userId) reverses it, but doesn't re-establish contact; either party still has to send a fresh request.

Why DMs are contact-gated

The default is that you can DM someone who agreed to accept DMs from you. The contact graph is the gate that survives every other check: you can only DM a mutual contact, and group members must be contacts of the creator. Open inboxes are a separate decision per room type; for the E2EE-by-default rooms, contact-of-caller is the constraint that holds.

See also