Skip to content

Presences

Authentication tells you who a user is. Presences tell you whether they are around right now. The Appwrite Presences API records a live status for each signed-in user and broadcasts every change over Realtime, so your app can render online indicators, "viewing this page" cues, typing signals, and collaboration banners without writing any socket plumbing.

A presence is a short-lived record attached to a user. It carries a userId, a status string, an optional metadata JSON object for richer context, and an expiresAt timestamp that controls automatic cleanup. Presences are written by either the user's own session or a server SDK, and read by any client with the right permissions.

Set the user's presence

Once a user is signed in, upsert their presence on the events that should mark them as active, for example on app launch, on a window focus, or on a heartbeat timer. userId is filled in automatically from the session, so you only need to pass the fields that change.

import { Client, Presences, ID, Permission, Role } from "appwrite";

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('<PROJECT_ID>');

const presences = new Presences(client);

const presence = await presences.upsert({
    presenceId: ID.unique(),
    status: 'online',
    metadata: { page: '/dashboard' },
    permissions: [
        Permission.read(Role.users())
    ]
});

Store the returned $id somewhere your client can reach again (for example a context object, a state store, or localStorage) so subsequent updates reuse the same record instead of creating a new one every time. The same call updates the existing presence in place when called with an existing presenceId.

Update on activity changes

Most apps update presence on a few specific signals:

  • Window focus and blur to flip between online and away.
  • Route changes to update the page field in metadata and show "viewing this page".
  • Typing events in a chat or comment box to set status: 'typing' and clear it when the user stops.
  • A heartbeat timer (for example every 30 seconds) to push the expiresAt forward and keep the record alive while the user is active.
Web
async function setStatus(status, metadata = {}) {
    await presences.upsert({
        presenceId,
        status,
        metadata,
        permissions: [
            Permission.read(Role.users())
        ]
    });
}

window.addEventListener('focus', () => setStatus('online'));
window.addEventListener('blur',  () => setStatus('away'));

There is no fixed heartbeat interval enforced by the server, so pick whichever cadence matches your UX. Anything shorter than the expiresAt you choose will keep the presence alive without gaps.

Show other users' presence

List the presences the current user can read to paint the initial "online now" view, a list of viewers on a page, or a typing dot in a chat. The list call honors the same permissions you set on each record, so each client only sees the statuses it is allowed to render.

import { Client, Presences } from "appwrite";

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('<PROJECT_ID>');

const presences = new Presences(client);

const result = await presences.list();

const onlineUsers = new Map(
    result.presences.map(presence => [presence.userId, presence])
);

Then subscribe to the global presences channel to keep that snapshot live. Apply the same patch to the same onlineUsers map on every event, add or replace on upsert or update, remove on delete.

import { Client, Realtime, Channel } from "appwrite";

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('<PROJECT_ID>');

const realtime = new Realtime(client);

await realtime.subscribe(Channel.presences(), response => {
    const presence = response.payload;
    if (response.events.includes('presences.*.delete')) {
        onlineUsers.delete(presence.userId);
    } else {
        onlineUsers.set(presence.userId, presence);
    }
});

Clear presence on sign out

A presence outlives the session that created it by default, so when a user signs out you should delete their presence record explicitly. This emits a delete event on the presence channels, so every subscribed client sees the user go offline immediately instead of waiting for the record to expire.

await presences.delete({ presenceId });
await account.deleteSession({ sessionId: 'current' });

If a user closes the browser tab or loses connection without signing out, the record will still disappear on its own when expiresAt is reached, which is why short heartbeat windows work well for true "live" indicators.

Scoping who can see a presence

Presences use the standard Appwrite permissions system. Set read permissions on each record to match how your app already groups users:

  • Role.users() for any signed-in user, useful for a global "X users online" counter.
  • Role.team('<TEAM_ID>') for collaboration features that should only show statuses to teammates.
  • Role.user('<USER_ID>') for one-to-one features such as DMs, where only the recipient should see the sender's typing state.

Pass a permissions array to upsert() to attach roles to a presence. For example, to share a typing indicator only with the recipient of a DM:

import { Client, Presences, ID, Permission, Role } from "appwrite";

const client = new Client()
    .setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    .setProject('<PROJECT_ID>');

const presences = new Presences(client);

const presence = await presences.upsert({
    presenceId: ID.unique(),
    status: 'typing',
    permissions: [
        Permission.read(Role.user('<RECIPIENT_USER_ID>'))
    ]
});

Presence read and subscribe events both honour these permissions, so a user will never receive a status update for a presence they could not have read with a direct GET.

If you do not pass a permissions array when upserting a presence, Appwrite defaults to giving read access only to the user who created it, so no other client can subscribe to it. To share a presence more broadly, you must set permissions explicitly.

Where to next

  • Realtime: Presences. The full concept reference, including channel patterns, expiry behaviour, and server-side usage.
  • Realtime channels. See how presences fits alongside account, teams, and rows.
  • Permissions. Refresher on how Role.team() and Role.user() work.