docs

Ease docs/Web/Reference

SDK reference

Every surface on one page: the four named surfaces under a connected client (ease.me, ease.auth, ease.passkey, ease.contacts, ease.chat), the top-level lifecycle, the room handles, and the core types. Signatures are shown in TypeScript notation; the API shape is identical on every SDK, only the language syntax differs. Ctrl-F is the index. Methods list the EaseError codes they can raise; the generic infrastructure codes (db_error and the http_ fallbacks) and access_token_invalid, which the SDK retries once with a refreshed token before surfacing, are omitted everywhere. The errors concept page holds the full catalogue with recovery guidance.

Ease (top level)

One Ease instance per application. connect() is tenant bootstrap, not user-scoped. The four named surfaces — me, auth, contacts, chat — hang off the instance, alongside the socket, session, and blob lifecycle methods.

connect(): Promise<TenantInfo>
Resolve tenant configuration from the bootstrap service. Required before any session-bearing call.Throws api_key_missing api_key_malformed api_key_invalid api_key_revoked api_key_env_mismatch
getTenant(): TenantInfo | null
Read the cached tenant info populated by connect(); null before connect() has resolved.
getSession(): Session | null
Read the active session; null before the user is signed in.
getSocketInfo(): SocketInfo | null
Read the current SocketInfo; null when no socket is open.
openSocket(): Promise<SocketInfo>
Open the realtime socket. Idempotent: returns the cached SocketInfo when already open.
closeSocket(): Promise<void>
Close the realtime socket. Idempotent.
sendPing(): Promise<PongInfo>
Send a keepalive ping on the open socket and return the server timestamp plus measured round-trip time.
onSocketClosed(handler: () => void): () => void
Subscribe to socket-close events; returns an unsubscribe function.
refreshSession(): Promise<Session>
Refresh the current session's access and refresh tokens, leaving protocol-store state unchanged.
maintainSession(onRefresh?: (s: Session) => void, signal?: AbortSignal): Promise<void>
Run an auto-refresh loop that refreshes shortly before the access token expires; stop it with an AbortSignal.
uploadBlob(opts: { roomId, data, mimeType, e2ee? }): Promise<BlobRef>
Encrypt bytes client-side and upload them to blob storage, returning a BlobRef the sender embeds in a typed event. e2ee defaults to true.
downloadBlob(ref: BlobRef): Promise<Uint8Array>
Fetch and, for E2EE refs, decrypt a blob; throws blob_decrypt_failed on a failed auth tag.
static get savedSession(): Session | null
The last Session this device persisted, readable before constructing an Ease instance to decide whether to skip the login screen.
static clearSavedSession(): void
Wipe the persisted session manually; signOut already does this.

ease.me

The user-self surface: what the signed-in user is — profile and avatar. Reads and writes delegate to the server.

Device management lives at ease.me.devices.*.
getProfile(): Promise<MyProfile>
Read the authenticated user's profile from the server.
getAvatar(): Promise<Uint8Array | null>
Read the authenticated user's avatar bytes, if any.
setAvatar(input: Blob | File | Uint8Array): Promise<Uint8Array>
Upload a new avatar and return the canonical bytes the server stored.
clearAvatar(): Promise<void>
Remove the user's avatar. Idempotent.
devices.list(): Promise<Device[]>
The linked-devices management surface: every active device with its platform, addedAt, and lastSeenAt. Pair with revoke for a Settings-style 'Linked Devices' screen.
devices.current(): Promise<Device>
The calling device's own row — use its deviceId to mark 'this device' and to hide its revoke control.
devices.revoke(deviceId: string): Promise<void>
Revoke (unlink) another device. The server refuses to revoke the primary or the calling device.
devices.revokeAllOthers(): Promise<void>
Sign out everywhere else — revoke every device except this one.
devices.onLinked(fn: (e: DeviceLinkedEvent) => void): () => void
Subscribe to device-linked events on the user's existing devices; the newly-linked device itself does not receive this. Returns an unsubscribe handle.
devices.onRevoked(fn: (e: DeviceRevokedEvent) => void): () => void
Subscribe to device-revoked events. If the revoked id is the local device, the SDK has already cleared local session and encryption state by the time the listener runs.

ease.auth

How a user enters or leaves the system: sign-up, login, OTP, passkey, sign-out, session restore, and the QR device-link flow.

On web, restoreSession does not rehydrate the libsignal store — the keystore is encrypted with a password-derived key the SDK never persists, so messaging needs a fresh login with password.
sendSignupOtp(email: string): Promise<void>
Send a one-time signup code to email, delivered out-of-band for the caller to pass back to signUp. Email-password tenants only.Throws not_connected email_invalid auth_method_mismatch tenant_email_not_configured otp_rate_limited email_send_failed
verifySignupOtp(email: string, otp: string): Promise<void>
Optional pre-check that an OTP is valid before the rest of the signup form is collected; signUp still re-checks and consumes the code.Throws not_connected email_invalid otp_invalid otp_expired otp_attempts_exceeded
signUp(email, password, otp, options?): Promise<Session>
Create a new user with email, password, and the delivered OTP; options carries username and name. Returns a Session.Throws not_connected email_invalid otp_invalid otp_expired otp_attempts_exceeded password_weak email_taken username_taken username_invalid name_required
signUpByPhone(phone, password, otp, options?): Promise<Session>
Phone variant of signUp; options carries username and name.
loginByPhone(phone: string, password: string): Promise<Session>
Log in with phone and password. Returns a Session.
registerWithPasskey(handle: string, otp: string, options?): Promise<Session>
Register a new user with a WebAuthn passkey instead of a password; handle is an email or phone routed by the tenant auth_method. Requires the PRF extension and throws passkey_prf_unavailable without it.
loginWithPasskey(handle: string): Promise<Session>
Log in an existing user with a WebAuthn passkey; auth succeeds on a valid assertion, while messaging additionally needs the keystore from a prior registration on this browser.
signOut(): Promise<void>
Clear the current session. Idempotent.
getDeviceLinkStatus(claimId: string): Promise<DeviceLinkStatus>
New device, step 3: poll the claim's status, proceeding to redeem once it flips to approved.
onDeviceLinked(handler: (e: DeviceLinkedEvent) => void): () => void
Subscribe to device-link-completed events on the user's existing devices; the newly-linked device itself does not receive this. Returns an unsubscribe handle.

ease.passkey

The WebAuthn PRF extension exposed as a building block, distinct from passkey auth. A passkey can derive stable, audience-scoped secrets for client-side key wrapping.

deriveSecret(audience: string): Promise<Uint8Array>
Derive a deterministic 32-byte secret from the signed-in user's passkey, scoped to audience; triggers a WebAuthn assertion on every call and rejects the reserved keystore audience.
prfAvailable(): Promise<boolean>
Report whether the WebAuthn PRF extension is available in this browser; a false is conservative and never triggers a user prompt.

ease.contacts

The graph of who the user knows. Gates DM eligibility, group membership, and approval flows, and carries the presence and contact-lifecycle event subscriptions.

list(): Promise<Contact[]>
List the user's established contacts.
rescind(requestId: string): Promise<void>
Rescind a contact request the user previously sent.
remove(userId: string): Promise<void>
Remove an established contact.
block(userId: string): Promise<void>
Block a user from contacting the authenticated user.
unblock(userId: string): Promise<void>
Unblock a previously blocked user.
checkHandle(handle: { email } | { phone }): Promise<boolean>
Check whether a handle maps to a discoverable user without surfacing the user ID; true when taken.
checkUsername(username: string): Promise<boolean>
Check username availability for signup; true when the username is available, false when taken.
getProfile(userId: string): Promise<ContactProfile>
Read a specific contact's profile.
getAvatar(userId: string): Promise<Uint8Array | null>
Read a specific contact's avatar bytes, if any.
setLocalName(userId: string, name: string | null): Promise<void>
Set or clear the local-only display name for a contact; pass null to fall back to the contact's own displayName.
onPresenceUpdate(fn: (e: PresenceEvent) => void): () => void
Subscribe to per-contact presence-change deltas.
onPresenceSnapshot(fn: (snapshot: PresenceEvent[]) => void): () => void
Subscribe to full-list presence snapshots, fired on socket open and each reconnect for painting a complete who's-online view.
onAvatarUpdated(fn: (e: AvatarUpdatedEvent) => void): () => void
Subscribe to avatar-updated push frames so peers can re-fetch a swapped avatar without polling.
onContactRequestReceived(fn: (e: ContactRequestReceivedEvent) => void): () => void
Subscribe to incoming contact requests — someone wants to add you.
onContactRequestAccepted(fn: (e: ContactRequestAcceptedEvent) => void): () => void
Subscribe to your outgoing contact requests being accepted.
onContactRequestDeclined(fn: (e: ContactRequestDeclinedEvent) => void): () => void
Subscribe to your outgoing contact requests being declined.
onContactRemoved(fn: (e: ContactRemovedEvent) => void): () => void
Subscribe to events fired when a contact removes you, so the UI can drop them without polling.

ease.chat

The conversational surface: room factories for DMs and groups, cross-room verbs for creating and joining groups, group admin metadata, and the aggregate event subscriptions that fan across every room.

The wider room-type vocabulary — team, community, event, announcement, broadcast, live — is shaped on the API but throws not_yet_implemented today. Build against dm and group.
dm(userId: string): DmRoom
Open a DM room with another user; the room ID is derived from the recipient's user ID.
group(id: string): GroupRoom
Open a group room by its server-issued GRUP_ ID; throws invalid_room_id for a non-group ID.
createGroup(name: string, memberUserIds: string[], options?): Promise<Group>
Create a new group with the caller as owner; a duplicate-roster guard throws duplicate_roster, bypassable with options { force: true }.Throws not_connected not_signed_in name_required name_too_long group_too_small group_full member_set_invalid member_not_in_contacts duplicate_roster
listMyGroups(opts?: { limit? }): Promise<GroupListPage>
List the groups the user is a member of.
joinGroupByToken(token: string): Promise<Group>
Join an existing group by its QR or invite token, bypassing the contact gate and approval toggle; returns the post-join Group snapshot.Throws not_connected not_signed_in invite_token_invalid qr_join_disabled group_full
setGroupRequiresApproval(groupId: string, enabled: boolean): Promise<Group>
Set the group's requires-approval toggle (owner-only); when enabled, regular-member adds route through pending approval.
setGroupQrJoinEnabled(groupId: string, enabled: boolean): Promise<Group>
Owner/admin toggle for joining via the group QR / invite link; disabling hides the token, re-enabling restores the existing link.
setGroupAvatar(groupId: string, jpegBytes: Uint8Array): Promise<void>
Upload a group avatar (owner or admin only); bytes must be a JPEG within the tenant's per-group cap.
getGroupAvatar(groupId: string): Promise<Uint8Array | null>
Fetch a group's avatar bytes; null when no avatar is set.
clearGroupAvatar(groupId: string): Promise<void>
Remove a group's avatar (owner or admin only). Idempotent.
onIncomingMessage(fn: (msg: IncomingMessage) => void): () => void
Subscribe to every incoming decrypted message across all DMs and groups; returns an unsubscribe function.
onIncomingDecryptError(fn: (err: IncomingDecryptError) => void): () => void
Subscribe to the decrypt-error buffer, the silent-drop class an app surfaces as messages that couldn't be read.
onIncomingGroupMessage(fn: (msg: IncomingGroupMessage) => void): () => void
Subscribe to incoming group messages across all groups, carrying group context the per-message stream does not.
onGroupMembershipChange(fn: (ev: GroupMembershipChangeEvent) => void): () => void
Subscribe to group-membership changes across all groups — adds, removes, role transitions, leaves, ownership transfers — with a changeKind to branch on.
onGroupRemoved(fn: (ev: GroupRemovedEvent) => void): () => void
Subscribe to group-removed events when a group the user is in is disbanded.
onGroupJoinRequest(fn: (ev: GroupJoinRequestEvent) => void): () => void
Subscribe to incoming join-request events for groups the user owns or admins, fired when a member nominates someone under the approval gate.
onGroupJoinRequestResolved(fn: (ev: GroupJoinRequestResolvedEvent) => void): () => void
Subscribe to join-request resolution events so the nominator's UI updates when a nomination is approved or rejected.

DmRoom & GroupRoom

The room handle returned by ease.chat.dm and ease.chat.group. Both implement the BaseRoom contract — send, on, off, ready, close — plus capability methods. GroupRoom additionally exposes membership, metadata, approval, and invite-token operations.

on(eventName: string, handler: (event: IncomingEvent) => void): Subscription
Subscribe to a named room event and return a token to cancel with off(); DM rooms surface message and decryptError, group rooms add memberJoined, memberLeft, and metadataUpdated.
off(subscription: Subscription): void
Cancel a subscription returned by on().
ready(): Promise<void>
Resolve once the underlying socket is open; does not wait for backlog drain.
close(): Promise<void>
Release room-local subscriptions; the shared transport stays open for other rooms.
history(limit: number, before?: string): Promise<HistoryEntry[]>
Page back through locally-logged DM messages oldest-first; an app that never built a persisting snapshot sees an empty log.Throws not_connected
members(): Promise<Member[]>
GroupRoom: list the current roster with identity, role, and joinedAt.
addMember(userId: string): Promise<Group>
GroupRoom: nominate a contact for membership; under the approval gate a member's nomination throws pending_approval with the request id, otherwise it resolves with the post-add Group.Throws not_connected group_not_found group_full member_already_present member_not_in_contacts nomination_already_pending pending_approval
leave(): Promise<void>
GroupRoom: self-leave; an owner with remaining members must transferOwnership first or this throws creator_must_transfer_first.
promote(userId: string): Promise<void>
GroupRoom: promote a member to admin (owner-only); a fourth admin throws admin_cap_reached.
demote(userId: string): Promise<void>
GroupRoom: demote an admin back to member (owner-only).
mute(userId: string, until: Date | null): Promise<void>
Owner/admin: mute userId until the given time (null unmutes). A muted member keeps receiving but their sends are rejected with member_muted. The owner is never mutable.Throws not_connected not_signed_in group_not_found forbidden member_not_present cannot_mute_owner
ban(userId: string, reason?: string): Promise<void>
Owner/admin: ban userId — removes them and blocks re-add via a tombstone every join path checks, bumping the group epoch. An optional reason (up to 500 chars) is stored on the ban and the audit event. The owner may ban anyone; an admin may ban plain members only.Throws not_connected not_signed_in group_not_found forbidden cannot_ban_owner cannot_ban_self cannot_ban_admin already_banned reason_too_long
unban(userId: string): Promise<void>
Owner/admin: lift a ban by removing the tombstone. Does not re-add the member — they must rejoin through a normal path.Throws not_connected not_signed_in group_not_found forbidden not_banned
bans(): Promise<GroupBan[]>
Owner/admin: list the group's active bans. Each entry carries the moderator, the timestamp, an optional reason, and the banned user's display fields.Throws not_connected not_signed_in group_not_found forbidden
moderationEvents(opts?: { targetUserId?: string; action?: string; limit?: number; before?: string }): Promise<ModerationEventsPage>
Owner/admin: read the append-only moderation audit log, newest first — every mute, unmute, ban, unban, kick, and role change. Filter by targetUserId and/or action and paginate with limit + before to answer how many times a member has been acted on.Throws not_connected not_signed_in group_not_found forbidden
transferOwnership(newOwnerUserId: string): Promise<void>
GroupRoom: transfer ownership to a current member (owner-only); the caller drops to member.
disband(): Promise<void>
GroupRoom: disband the group entirely (owner-only), fanning group_removed to every member.
rename(name: string): Promise<Group>
GroupRoom: rename the group (owner or admin only).
setAvatar(jpegBytes: Uint8Array): Promise<void>
GroupRoom: set the group avatar (owner or admin only).
getAvatar(): Promise<Uint8Array | null>
GroupRoom: fetch the current group avatar; null if unset.
clearAvatar(): Promise<void>
GroupRoom: clear the group avatar (owner or admin only).
joinRequests(): Promise<GroupJoinRequestRow[]>
GroupRoom: list open join-request nominations awaiting approval (owner or admin only).
pendingJoinRequestCount(): Promise<number>
GroupRoom: count open join-request nominations, for a chat-info badge.
approveJoinRequest(requestId: string): Promise<void>
GroupRoom: approve a pending nomination (owner or admin only), promoting it to a real member.
rejectJoinRequest(requestId: string): Promise<void>
GroupRoom: reject a pending nomination (owner or admin only).
regenerateInviteToken(): Promise<InviteToken>
GroupRoom: rotate the group's invite token (owner-only); the old token stops being accepted.
getInviteToken(): Promise<InviteToken>
GroupRoom: read the current invite token without rotating (owner or admin only).
groupHistory(): Promise<GroupHistoryRow[]>
GroupRoom: fetch interleaved message and system-event history oldest-first, each row tagged with its rowKind.

Core types

The shapes that flow through the surfaces above. Field lists are abbreviated; the full definitions live in sdks/web/src.

Session { userId; email?; phone?; accessToken; accessTokenExpiresAt; refreshToken; refreshTokenExpiresAt }
The signed-in session: user id, the handle signed up with, and the token pair with their expiry timestamps.
MessageEnvelope — type: "ease.text" | "ease.image" | "ease.voice" | "ease.video" | "ease.file" | "ease.location" | "ease.reaction"
The discriminated union a caller hands to room.send; each type carries its own payload (text, blob ref, coordinates, or reaction target).
Contact { userId; name?; username?; email?; phone?; online; lastSeenAt; localName; hasAvatar; ... }
An established contact: identity fields, live presence, an optional private local-name override, and avatar metadata.
Member { identity: Identity; role: MemberRole; joinedAt: Date }
A participant in a room — identity, role (owner / admin / member), and the time they joined.
Group { groupId; name; creatorUserId; members; epoch; createdAt; lastMessageAt; hasAvatar; avatarUpdatedAt }
A group snapshot: identity, the inlined roster, the sender-key epoch, and avatar and last-message metadata.