From 8fcd080dd651c9cc146eece2e7d32100f2d731fe Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Tue, 19 May 2026 19:55:33 +0530 Subject: [PATCH 01/12] Add Presence API announcement, docs, and changelog --- .../announcing-presence-api/+page.markdoc | 166 ++++++++++ .../changelog/(entries)/2026-05-19-2.markdoc | 14 + .../docs/apis/realtime/channels/+page.markdoc | 8 + .../docs/apis/realtime/presence/+page.markdoc | 283 ++++++++++++++++++ src/routes/docs/products/auth/+page.markdoc | 3 + .../docs/products/auth/presence/+page.markdoc | 249 +++++++++++++++ 6 files changed, 723 insertions(+) create mode 100644 src/routes/blog/post/announcing-presence-api/+page.markdoc create mode 100644 src/routes/changelog/(entries)/2026-05-19-2.markdoc create mode 100644 src/routes/docs/apis/realtime/presence/+page.markdoc create mode 100644 src/routes/docs/products/auth/presence/+page.markdoc diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc new file mode 100644 index 00000000000..7e2008b6d88 --- /dev/null +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -0,0 +1,166 @@ +--- +layout: post +title: "Announcing the Presence API: Track who is online, typing, and active in realtime" +description: A new Appwrite API for short-lived user statuses, with built-in Realtime channels, automatic expiry, and permission-aware subscriptions. +date: 2026-05-19 +cover: /images/blog/announcing-presence-api/cover.png +timeToRead: 5 +author: aditya-oberai +category: announcement +featured: false +callToAction: true +faqs: + - question: "What is the Appwrite Presence API?" + answer: "Presence is a new Appwrite API for tracking short-lived user statuses, like online, away, editing, or typing. Each presence is a small record attached to a user, with optional status text and metadata, and it broadcasts every change over a dedicated Realtime channel. It is built for online indicators, collaboration cursors, typing dots, and live attendee lists, the kind of UI signals that should disappear automatically when a user goes offline." + - question: "How is Presence different from storing status in a database row?" + answer: "A database row stays around until you delete it. Presence records expire automatically based on an expiresAt timestamp you control, so a stale online indicator never gets stuck after a user closes the tab or loses connection. Presence also ships with its own Realtime channels, so you do not need to write subscription logic or maintenance jobs on top of a regular table to get a live who-is-here view." + - question: "How long does a presence record live?" + answer: "Every presence carries an expiresAt timestamp, up to 30 days in the future. Once that timestamp passes, Appwrite deletes the record and emits a delete event on the presence channels. The typical pattern is to upsert the same presence on a heartbeat (every few seconds, or on focus and route change events) so the expiry keeps sliding forward while the user is active." + - question: "Who can read a presence record?" + answer: "Presences use the same permissions system as the rest of Appwrite. Set Role.users() for any signed-in user, Role.team('TEAM_ID') for a single team, or Role.user('USER_ID') for a one-to-one channel. Realtime subscriptions honor these rules, so a client only receives updates for presences it could have fetched with a direct GET." + - question: "Which SDKs support the Presence API?" + answer: "Presence is exposed through every Appwrite SDK as a Presences service, alongside Account, TablesDB, Storage, and the rest. Client SDKs (Web, Flutter, Apple, Android, React Native) can upsert and subscribe to presence directly from the user's session, and server SDKs can manage presence with an API key that holds the presences.read and presences.write scopes." + - question: "Do I need to run my own cleanup job for stale presences?" + answer: "No. Appwrite runs a background worker that removes expired presences automatically and emits delete events, so stale online indicators disappear without any extra code on your side. You only need to call delete explicitly when you want a user to go offline immediately, for example on sign out." +--- + +Realtime apps almost always need to answer one question that has nothing to do with the data they store: **who is here right now?** Whether it is the green dot next to a teammate's avatar, the cursor on a shared document, or the "typing..." indicator under a chat input, that signal is short-lived, frequently updated, and supposed to disappear the moment a user closes the tab. + +Building that with a database row works until it doesn't. A stale "online" flag survives a network drop. A presence table needs a cleanup job. A subscription has to know which row is whose. The shape of the data, with sub-second writes, second-scale TTLs, and permission-aware broadcasts, is just different from a row you mean to keep. + +Today, we are announcing the **Appwrite Presence API**. + +# What this gives you + +Presence is a first-class Appwrite resource for short-lived user statuses, with the same SDK shape and permissions model as the rest of the platform. + +- **Upsert-first writes** so you can call the same method on every focus, route change, or heartbeat without worrying about duplicates. +- **Automatic expiry** controlled by an `expiresAt` timestamp (up to 30 days). Stale records disappear on their own, no cleanup cron required. +- **Dedicated Realtime channels** (`presences` and `presences.`) that emit `create`, `update`, and `delete` events for every record a subscriber has permission to read. +- **Free-form status and metadata** so a presence can mean "online", "typing in #general", or "viewing document `abc123`", whichever vocabulary fits your app. +- **Permission-aware subscriptions** that reuse `Role.users()`, `Role.team()`, and `Role.user()`, so collaboration features only leak status to the right people. +- **A `Presences` service in every SDK**, with the matching scopes (`presences.read`, `presences.write`) on the server side. + +# Setting a presence + +Once a user signs in, upsert their presence. The first call creates the record, every subsequent call updates it in place. + +{% multicode %} +```client-web +import { Client, Presences, ID } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const presences = new Presences(client); + +const presence = await presences.upsert({ + presenceId: ID.unique(), + status: 'online', + metadata: { page: '/dashboard' } +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final presences = Presences(client); + +final presence = await presences.upsert( + presenceId: ID.unique(), + status: 'online', + metadata: { 'page': '/dashboard' }, +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let presence = try await presences.upsert( + presenceId: ID.unique(), + status: "online", + metadata: ["page": "/dashboard"] +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.services.Presences + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +val presence = presences.upsert( + presenceId = ID.unique(), + status = "online", + metadata = mapOf("page" to "/dashboard") +) +``` +{% /multicode %} + +`userId` is filled in automatically from the session on client SDKs. On server SDKs (API key, JWT, Admin), pass `userId` explicitly. `presenceId` and `status` are both required; `permissions`, `expiresAt`, and `metadata` are optional, so the smallest possible call is just `{ presenceId, status }` on a fresh ID. + +# Subscribing to presence updates + +Subscribe to the global presences channel to drive an "online now" list, or to a specific presence to follow one user. The Realtime payload is identical in shape to every other Appwrite event, so the same handler patterns you already use for rows or files work here. + +```client-web +import { Client, Realtime, Channel } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const realtime = new Realtime(client); + +const onlineUsers = new Map(); + +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); + } +}); +``` + +The `delete` event fires both when you remove a presence explicitly and when it expires automatically, so a single handler can drive the "user just went offline" branch of your UI either way. + +# When to reach for Presence + +Presence is the right primitive for any UI cue that should appear when a user is around and disappear when they are not: + +- Online indicators in a team directory, contacts list, or member sidebar. +- Collaboration cursors that show which document or row a teammate is viewing. +- Typing indicators in chat, comment threads, or live forms. +- Live attendee lists for streams, classrooms, or shared dashboards. +- "Someone else is editing this" banners and soft locks on shared records. + +For anything that should outlive the session, like a user's profile, preferences, or saved settings, stick with [account preferences](/docs/products/auth/preferences) or a row in your database. Presence is intentionally short-lived and self-cleaning, and it is at its best when you treat it that way. + +# Get started with Presence + +The Presence API is **available on Appwrite Cloud today**. You can start using it from the existing client SDKs with no extra setup; server-side use requires an API key with the `presences.read` and `presences.write` scopes. + +# More resources + +- [Realtime: Presence](/docs/apis/realtime/presence) +- [Authentication: Presence](/docs/products/auth/presence) +- [Realtime channels reference](/docs/apis/realtime/channels) +- [Permissions](/docs/advanced/platform/permissions) diff --git a/src/routes/changelog/(entries)/2026-05-19-2.markdoc b/src/routes/changelog/(entries)/2026-05-19-2.markdoc new file mode 100644 index 00000000000..bcb478dcbd5 --- /dev/null +++ b/src/routes/changelog/(entries)/2026-05-19-2.markdoc @@ -0,0 +1,14 @@ +--- +layout: changelog +title: "Track who is online with the new Presence API" +date: 2026-05-19 +cover: /images/blog/announcing-presence-api/cover.avif +--- + +Appwrite now ships a first-class **Presence API** for short-lived user statuses like online, away, editing, or typing. Each presence is a small record attached to a user, with an `expiresAt` timestamp (up to 30 days), optional `status` and `metadata`, and the same [permissions](/docs/advanced/platform/permissions) model as the rest of the platform. + +Presences broadcast every change over dedicated Realtime channels (`presences` and `presences.`), so an "online now" list, a typing indicator, or a "viewing this page" cue is a single `Channel.presences()` subscription away. Stale records expire and emit `delete` events automatically, no cleanup job required. + +{% arrow_link href="/blog/post/announcing-presence-api" %} +Read the announcement +{% /arrow_link %} diff --git a/src/routes/docs/apis/realtime/channels/+page.markdoc b/src/routes/docs/apis/realtime/channels/+page.markdoc index bc0e1cba7c2..ddfb0de7a61 100644 --- a/src/routes/docs/apis/realtime/channels/+page.markdoc +++ b/src/routes/docs/apis/realtime/channels/+page.markdoc @@ -209,6 +209,14 @@ A list of all channels available you can subscribe to. When using `Channel` help * `functions.` * `Channel.function('')` * Any execution event to a given function +--- +* `presences` +* `Channel.presences()` +* Any create, update, or delete event on any [presence](/docs/apis/realtime/presence) the subscriber can read. +--- +* `presences.` +* `Channel.presence('')` +* Any update or delete event on a given presence record. {% /table %} diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc new file mode 100644 index 00000000000..f224f026191 --- /dev/null +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -0,0 +1,283 @@ +--- +layout: article +title: Presence +description: Use the Appwrite Presence API to track which users are currently active, broadcast their status, and subscribe to live presence updates over Realtime. +--- + +The Appwrite **Presence API** tracks which users are currently active in your app and lets every connected client see those statuses in realtime. You can use it to render online indicators next to teammates, show who is viewing a document, broadcast a "typing" status in a chat, or surface "looking at the same page" cues during collaboration. + +A presence is a short-lived record tied to a user. Each record carries a `userId`, an optional `status` string (for example `online`, `away`, `editing`), an optional `metadata` JSON object for richer context (a cursor position, the document the user is viewing, the device they are on), and an `expiresAt` timestamp that controls when the record is automatically cleaned up. + +Presences are exposed as both a regular HTTP resource and a [Realtime](/docs/apis/realtime) channel, so the same record can be written by any client or server SDK and read live by every subscriber that has permission. + +# How it works {% #how-it-works %} + +A presence has two sides that are always in sync. + +**It is durable.** When you write a presence, it sticks around until it expires or you delete it. That means you can `list()` presences at any time to see who is online right now, including from a server-side function, without having to keep a Realtime connection open. + +**It is live.** Every change to a presence fires an event on the `presences` and `presences.` [Realtime](/docs/apis/realtime) channels. Subscribers get `create`, `upsert`, `update`, and `delete` events in milliseconds, over the same Realtime connection they are already using for rows and files. + +A typical "online dot" loop looks like this: + +1. Client A signs in and calls `presences.upsert({...})`. An `upsert` event fires on the presence channels. +2. Client B, subscribed to `Channel.presences()`, receives the event and shows A as online. +3. Client A keeps the record alive by upserting again on focus, route change, or a periodic timer, which slides `expiresAt` forward. +4. When `expiresAt` passes, the record is removed and a `delete` event fires. B drops A from its list. +5. If A signs out cleanly, they call `presences.delete(...)` and the `delete` event fires immediately, no waiting on expiry. + +This gives you two ways to keep a presence alive, and you pick whichever fits your UI: + +- **Heartbeat.** Upsert on focus, route change, or a periodic timer to push `expiresAt` forward. Best when presence should persist briefly across short disconnects (a quick network blip, a tab switch) or when you write presence from server code that has no live socket. +- **While connected.** If you keep a Realtime connection open, presence written over that connection is cleaned up automatically when the connection closes. Best for "online while the tab is open" UIs where you do not want to manage a heartbeat yourself. + +# Set a presence {% #set-a-presence %} + +A presence is created with an **upsert** on the user's `presenceId`. Calling the same endpoint again updates the existing record, so you can safely call it on every page navigation, focus change, or heartbeat without worrying about duplicates. + +{% multicode %} +```client-web +import { Client, Presences, ID } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const presences = new Presences(client); + +const presence = await presences.upsert({ + presenceId: ID.unique(), + status: 'online', + metadata: { page: '/dashboard' } +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final presences = Presences(client); + +final presence = await presences.upsert( + presenceId: ID.unique(), + status: 'online', + metadata: { 'page': '/dashboard' }, +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let presence = try await presences.upsert( + presenceId: ID.unique(), + status: "online", + metadata: ["page": "/dashboard"] +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.services.Presences + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +val presence = presences.upsert( + presenceId = ID.unique(), + status = "online", + metadata = mapOf("page" to "/dashboard") +) +``` +{% /multicode %} + +A few notes on the parameters: + +- `presenceId` (**required**) is the unique ID of the presence record. Use `ID.unique()` on first creation and persist it for subsequent updates so the same record is reused for the same user across sessions. +- `status` (**required**) is a free-form string up to 256 characters. There are no reserved values, so pick whatever vocabulary fits your app (`online`, `away`, `busy`, `editing`, `typing`). +- `userId` is set automatically from the authenticated session on client SDKs. Server SDKs (API key, JWT, Admin) must pass `userId` explicitly because there is no session to read it from. +- `metadata` is an arbitrary JSON object. Use it to carry any context that subscribers should see together with the status. +- `expiresAt` is optional. Without it, Appwrite applies a default TTL (see [Expiry and cleanup](#expiry-and-cleanup) below). +- `permissions` controls who can read or modify the presence record, the same way it works on rows and files. Without permissions, only the owner and project keys can see it. + +Call `presences.update(...)` with the same `presenceId` to patch any subset of these fields without re-sending the whole record; every field on `update` is optional. + +# Subscribe to presence updates {% #subscribe-to-presence-updates %} + +Presence is most useful when other clients can react to it live. Use the `Channel.presences()` helper to subscribe to the global presences channel, or `Channel.presence('')` to follow a single record. All Realtime subscriptions are gated by the [permissions system](/docs/advanced/platform/permissions), so a client will only receive updates for presences it has permission to read. + +{% multicode %} +```client-web +import { Client, Realtime, Channel } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const realtime = new Realtime(client); + +const subscription = await realtime.subscribe(Channel.presences(), response => { + if (response.events.includes('presences.*.update')) { + console.log('Presence updated', response.payload); + } + if (response.events.includes('presences.*.delete')) { + console.log('Presence expired or removed', response.payload); + } +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final realtime = Realtime(client); + +final subscription = realtime.subscribe([Channel.presences()]); + +subscription.stream.listen((response) { + if (response.events.contains('presences.*.update')) { + print('Presence updated: ${response.payload}'); + } + if (response.events.contains('presences.*.delete')) { + print('Presence expired or removed: ${response.payload}'); + } +}); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let realtime = Realtime(client) + +let subscription = realtime.subscribe(channels: [Channel.presences()]) { response in + if (response.events?.contains("presences.*.update") == true) { + print("Presence updated: \(String(describing: response.payload))") + } + if (response.events?.contains("presences.*.delete") == true) { + print("Presence expired or removed: \(String(describing: response.payload))") + } +} +``` + +```client-android-kotlin +import io.appwrite.Channel +import io.appwrite.Client +import io.appwrite.services.Realtime + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val realtime = Realtime(client) + +val subscription = realtime.subscribe(Channel.presences()) { + if (it.events.contains("presences.*.update")) { + println("Presence updated: ${it.payload}") + } + if (it.events.contains("presences.*.delete")) { + println("Presence expired or removed: ${it.payload}") + } +} +``` +{% /multicode %} + +The `events` array follows the same pattern as every other Appwrite resource: + +- `presences.*.create` and `presences..create` for new records. +- `presences.*.upsert` and `presences..upsert` for the unified create-or-update path that fires on every `upsert()` call. +- `presences.*.update` and `presences..update` for status, metadata, or expiry changes. +- `presences.*.delete` and `presences..delete` for records that were deleted explicitly or expired automatically. + +This gives you a clean signal for "user just came online", "user changed status", and "user went offline", without writing any custom socket logic. + +# Presence channels {% #presence-channels %} + +{% table %} +* Channel +* Channel Helper +* Description +--- +* `presences` +* `Channel.presences()` +* Any create, update, or delete event on any presence the subscriber can read. +--- +* `presences.` +* `Channel.presence('')` +* Any update or delete event on a specific presence record. +{% /table %} + +You can also append `.create()`, `.upsert()`, `.update()`, or `.delete()` to `Channel.presence('')` to narrow the stream to a single event type, identical to how channel filters work on every other resource. + +# Expiry and cleanup {% #expiry-and-cleanup %} + +Every presence carries an `expiresAt` timestamp. Once that time passes, Appwrite removes the record automatically and emits a `delete` event on the presence channels, so subscribers can react to "user went offline" without any explicit signal from the client that owned the presence. + +You can pass an explicit `expiresAt` up to **30 days in the future**. If you omit it, Appwrite applies a sensible default that fits the typical heartbeat pattern: keep upserting the presence every few seconds while the user is active, and let it expire naturally a short time after the last heartbeat. + +To remove a presence immediately, for example on sign out or when the user closes a document, send a delete: + +{% multicode %} +```client-web +await presences.delete({ presenceId: '' }); +``` + +```client-flutter +await presences.delete(presenceId: ''); +``` + +```client-apple +try await presences.delete(presenceId: "") +``` + +```client-android-kotlin +presences.delete(presenceId = "") +``` +{% /multicode %} + +# Permissions and scopes {% #permissions-and-scopes %} + +Presences use the standard Appwrite [permissions system](/docs/advanced/platform/permissions). Set read permissions on a presence to control who can subscribe to it: + +- `Role.any()` makes the presence visible to anyone, including unauthenticated visitors. +- `Role.users()` restricts visibility to signed-in users. +- `Role.team('')` shares the presence with a specific team, which is the right choice for collaboration features where only teammates should see each other's status. + +Server SDKs need an API key with the `presences.read` scope to list or read presences, and `presences.write` to create, update, or delete them. Client sessions can always update their own presence without an extra scope. + +# Use cases {% #use-cases %} + +The Presence API is a good fit any time you need to render "who is here right now" rather than "what has been written to storage": + +- **Online indicators** in a directory or contacts list +- **Collaboration cursors** that show which document or section each teammate is viewing +- **Typing indicators** in chat or comment threads +- **Live attendee lists** for live streams, classrooms, or shared dashboards +- **Locking signals** that warn a teammate when someone else is already editing a row + +For longer-lived state, like a user's profile or settings, use [account preferences](/docs/products/auth/preferences) or a row in your database instead. Presence is intentionally short-lived and self-cleaning. + +# Related {% #related %} + +- [Realtime overview](/docs/apis/realtime) +- [Realtime channels reference](/docs/apis/realtime/channels) +- [Realtime payload structure](/docs/apis/realtime/payload) +- [Authentication: Presence](/docs/products/auth/presence) diff --git a/src/routes/docs/products/auth/+page.markdoc b/src/routes/docs/products/auth/+page.markdoc index b0f1a5110e3..4fde59d9409 100644 --- a/src/routes/docs/products/auth/+page.markdoc +++ b/src/routes/docs/products/auth/+page.markdoc @@ -47,6 +47,9 @@ Implement custom authentication methods like biometric and passkey login by gene {% cards_item href="/docs/products/auth/mfa" title="Multifactor authentication (MFA)" %} Implementing MFA to add extra layers of security to your app. {% /cards_item %} +{% cards_item href="/docs/products/auth/presence" title="Presence" %} +Track which signed-in users are active right now and broadcast online, typing, and viewing status in realtime. +{% /cards_item %} {% /cards %} # Flexible permissions {% #flexible-permissions %} diff --git a/src/routes/docs/products/auth/presence/+page.markdoc b/src/routes/docs/products/auth/presence/+page.markdoc new file mode 100644 index 00000000000..80193618ab7 --- /dev/null +++ b/src/routes/docs/products/auth/presence/+page.markdoc @@ -0,0 +1,249 @@ +--- +layout: article +title: Presence +description: Track which signed-in users are active right now and broadcast their status in realtime with the Appwrite Presence API. +--- + +Authentication tells you **who a user is**. Presence tells you **whether they are around right now**. The Appwrite **Presence API** records a live status for each signed-in user and broadcasts every change over [Realtime](/docs/apis/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`, an optional `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](/docs/advanced/platform/permissions). + +# Set the user's presence {% #set-the-users-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. + +{% multicode %} +```client-web +import { Client, Presences, ID } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const presences = new Presences(client); + +const presence = await presences.upsert({ + presenceId: ID.unique(), + status: 'online', + metadata: { page: '/dashboard' } +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final presences = Presences(client); + +final presence = await presences.upsert( + presenceId: ID.unique(), + status: 'online', + metadata: { 'page': '/dashboard' }, +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let presence = try await presences.upsert( + presenceId: ID.unique(), + status: "online", + metadata: ["page": "/dashboard"] +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.services.Presences + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +val presence = presences.upsert( + presenceId = ID.unique(), + status = "online", + metadata = mapOf("page" to "/dashboard") +) +``` +{% /multicode %} + +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 {% #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. + +```client-web +async function setStatus(status, metadata = {}) { + await presences.update({ + presenceId, + status, + metadata + }); +} + +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 {% #show-other-users-presence %} + +Subscribe to the global `presences` channel (or a specific presence) to drive an "online now" indicator, a list of viewers on a page, or a typing dot in a chat. The subscription only emits records the current user has permission to read, so your access rules from sign in carry over without any extra work. + +{% multicode %} +```client-web +import { Client, Realtime, Channel } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const realtime = new Realtime(client); + +const onlineUsers = new Map(); + +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); + } +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final realtime = Realtime(client); + +final subscription = realtime.subscribe([Channel.presences()]); + +final onlineUsers = {}; + +subscription.stream.listen((response) { + final presence = response.payload; + if (response.events.contains('presences.*.delete')) { + onlineUsers.remove(presence['userId']); + } else { + onlineUsers[presence['userId']] = presence; + } +}); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let realtime = Realtime(client) + +var onlineUsers: [String: Any] = [:] + +let subscription = realtime.subscribe(channels: [Channel.presences()]) { response in + guard let payload = response.payload as? [String: Any], + let userId = payload["userId"] as? String else { return } + + if (response.events?.contains("presences.*.delete") == true) { + onlineUsers.removeValue(forKey: userId) + } else { + onlineUsers[userId] = payload + } +} +``` + +```client-android-kotlin +import io.appwrite.Channel +import io.appwrite.Client +import io.appwrite.services.Realtime + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val realtime = Realtime(client) + +val onlineUsers = mutableMapOf() + +realtime.subscribe(Channel.presences()) { response -> + val payload = response.payload as? Map ?: return@subscribe + val userId = payload["userId"] as? String ?: return@subscribe + + if (response.events.contains("presences.*.delete")) { + onlineUsers.remove(userId) + } else { + onlineUsers[userId] = payload + } +} +``` +{% /multicode %} + +# Clear presence on sign out {% #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. + +{% multicode %} +```client-web +await presences.delete({ presenceId }); +await account.deleteSession({ sessionId: 'current' }); +``` + +```client-flutter +await presences.delete(presenceId: presenceId); +await account.deleteSession(sessionId: 'current'); +``` + +```client-apple +try await presences.delete(presenceId: presenceId) +try await account.deleteSession(sessionId: "current") +``` + +```client-android-kotlin +presences.delete(presenceId = presenceId) +account.deleteSession(sessionId = "current") +``` +{% /multicode %} + +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 {% #scoping-who-can-see-a-presence %} + +Presences use the standard Appwrite [permissions system](/docs/advanced/platform/permissions). 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('')` for collaboration features that should only show statuses to teammates. +- `Role.user('')` for one-to-one features such as DMs, where only the recipient should see the sender's typing state. + +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. + +# Where to next {% #where-to-next %} + +- [Realtime: Presence](/docs/apis/realtime/presence). The full concept reference, including channel patterns, expiry behaviour, and server-side usage. +- [Realtime channels](/docs/apis/realtime/channels). See how `presences` fits alongside `account`, `teams`, and `rows`. +- [Permissions](/docs/advanced/platform/permissions). Refresher on how `Role.team()` and `Role.user()` work. From 8e021a0749ba51b822efb0d956d95b2a1a349afd Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Tue, 19 May 2026 21:24:32 +0530 Subject: [PATCH 02/12] content fixes --- .../announcing-presence-api/+page.markdoc | 2 +- .../changelog/(entries)/2026-05-19-2.markdoc | 2 +- .../docs/apis/realtime/presence/+page.markdoc | 1393 ++++++++++++++++- .../docs/products/auth/presence/+page.markdoc | 2 +- 4 files changed, 1354 insertions(+), 45 deletions(-) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 7e2008b6d88..0fffa99d828 100644 --- a/src/routes/blog/post/announcing-presence-api/+page.markdoc +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -11,7 +11,7 @@ featured: false callToAction: true faqs: - question: "What is the Appwrite Presence API?" - answer: "Presence is a new Appwrite API for tracking short-lived user statuses, like online, away, editing, or typing. Each presence is a small record attached to a user, with optional status text and metadata, and it broadcasts every change over a dedicated Realtime channel. It is built for online indicators, collaboration cursors, typing dots, and live attendee lists, the kind of UI signals that should disappear automatically when a user goes offline." + answer: "Presence is a new Appwrite API for tracking short-lived user statuses, like online, away, editing, or typing. Each presence is a small record attached to a user, with a status string and optional metadata, and it broadcasts every change over a dedicated Realtime channel. It is built for online indicators, collaboration cursors, typing dots, and live attendee lists, the kind of UI signals that should disappear automatically when a user goes offline." - question: "How is Presence different from storing status in a database row?" answer: "A database row stays around until you delete it. Presence records expire automatically based on an expiresAt timestamp you control, so a stale online indicator never gets stuck after a user closes the tab or loses connection. Presence also ships with its own Realtime channels, so you do not need to write subscription logic or maintenance jobs on top of a regular table to get a live who-is-here view." - question: "How long does a presence record live?" diff --git a/src/routes/changelog/(entries)/2026-05-19-2.markdoc b/src/routes/changelog/(entries)/2026-05-19-2.markdoc index bcb478dcbd5..06a9bbf07f1 100644 --- a/src/routes/changelog/(entries)/2026-05-19-2.markdoc +++ b/src/routes/changelog/(entries)/2026-05-19-2.markdoc @@ -5,7 +5,7 @@ date: 2026-05-19 cover: /images/blog/announcing-presence-api/cover.avif --- -Appwrite now ships a first-class **Presence API** for short-lived user statuses like online, away, editing, or typing. Each presence is a small record attached to a user, with an `expiresAt` timestamp (up to 30 days), optional `status` and `metadata`, and the same [permissions](/docs/advanced/platform/permissions) model as the rest of the platform. +Appwrite now ships a first-class **Presence API** for short-lived user statuses like online, away, editing, or typing. Each presence is a small record attached to a user, with a `status` string, optional `metadata`, an `expiresAt` timestamp (up to 30 days), and the same [permissions](/docs/advanced/platform/permissions) model as the rest of the platform. Presences broadcast every change over dedicated Realtime channels (`presences` and `presences.`), so an "online now" list, a typing indicator, or a "viewing this page" cue is a single `Channel.presences()` subscription away. Stale records expire and emit `delete` events automatically, no cleanup job required. diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index f224f026191..e7ddf10bdc8 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -6,7 +6,7 @@ description: Use the Appwrite Presence API to track which users are currently ac The Appwrite **Presence API** tracks which users are currently active in your app and lets every connected client see those statuses in realtime. You can use it to render online indicators next to teammates, show who is viewing a document, broadcast a "typing" status in a chat, or surface "looking at the same page" cues during collaboration. -A presence is a short-lived record tied to a user. Each record carries a `userId`, an optional `status` string (for example `online`, `away`, `editing`), an optional `metadata` JSON object for richer context (a cursor position, the document the user is viewing, the device they are on), and an `expiresAt` timestamp that controls when the record is automatically cleaned up. +A presence is a short-lived record tied to a user. Each record carries a `userId`, a `status` string (for example `online`, `away`, `editing`), an optional `metadata` JSON object for richer context (a cursor position, the document the user is viewing, the device they are on), and an `expiresAt` timestamp that controls when the record is automatically cleaned up. Presences are exposed as both a regular HTTP resource and a [Realtime](/docs/apis/realtime) channel, so the same record can be written by any client or server SDK and read live by every subscriber that has permission. @@ -31,9 +31,9 @@ This gives you two ways to keep a presence alive, and you pick whichever fits yo - **Heartbeat.** Upsert on focus, route change, or a periodic timer to push `expiresAt` forward. Best when presence should persist briefly across short disconnects (a quick network blip, a tab switch) or when you write presence from server code that has no live socket. - **While connected.** If you keep a Realtime connection open, presence written over that connection is cleaned up automatically when the connection closes. Best for "online while the tab is open" UIs where you do not want to manage a heartbeat yourself. -# Set a presence {% #set-a-presence %} +# Upsert a presence {% #upsert-a-presence %} -A presence is created with an **upsert** on the user's `presenceId`. Calling the same endpoint again updates the existing record, so you can safely call it on every page navigation, focus change, or heartbeat without worrying about duplicates. +`upsert` creates a presence or updates the existing record with the same `presenceId`. Call it on every page navigation, focus change, or heartbeat without worrying about duplicates. From a client session, `userId` is inferred from the signed-in user; from a server SDK with an API key, pass `userId` explicitly. Server SDKs need an [API key](/docs/advanced/platform/api-keys) with the `presences.write` scope. {% multicode %} ```client-web @@ -101,18 +101,1345 @@ val presence = presences.upsert( metadata = mapOf("page" to "/dashboard") ) ``` + +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const presences = new sdk.Presences(client); + +const presence = await presences.upsert({ + presenceId: '', + userId: '', + status: 'online' +}); +``` + +```server-python +from appwrite.client import Client +from appwrite.services.presences import Presences + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +presences = Presences(client) + +presence = presences.upsert( + presence_id = '', + user_id = '', + status = 'online' +) +``` + +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$presences = new Presences($client); + +$presence = $presences->upsert( + presenceId: '', + userId: '', + status: 'online' +); +``` + +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +presences = Presences.new(client) + +presence = presences.upsert( + presence_id: '', + user_id: '', + status: 'online' +) +``` + +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Presences presences = Presences(client); + +Presence presence = await presences.upsert( + presenceId: '', + userId: '', + status: 'online', +); +``` + +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Presences + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val presences = Presences(client) + +val presence = presences.upsert( + presenceId = "", + userId = "", + status = "online" +) +``` + +```server-java +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Presences; + +Client client = new Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey(""); + +Presences presences = new Presences(client); + +presences.upsert( + "", // presenceId + "", // userId + "online", // status + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + System.out.println(result); + }) +); +``` + +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let presences = Presences(client) + +let presence = try await presences.upsert( + presenceId: "", + userId: "", + status: "online" +) +``` + +```server-dotnet +using Appwrite; +using Appwrite.Models; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Presences presences = new Presences(client); + +Presence presence = await presences.Upsert( + presenceId: "", + userId: "", + status: "online" +); +``` + +```server-go +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/presences" +) + +func main() { + cli := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(""), + client.WithKey(""), + ) + + service := presences.New(cli) + + presence, err := service.Upsert( + "", + "", + "online", + ) + if err != nil { + panic(err) + } + fmt.Println(presence) +} +``` + +```server-rust +use appwrite::Client; +use appwrite::services::Presences; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let presences = Presences::new(&client); + + let presence = presences.upsert( + "", + "", + "online", + None, + None, + None, + ).await?; + + println!("{:?}", presence); + + Ok(()) +} +``` +{% /multicode %} + +A few notes on the parameters: + +- `presenceId` (**required**) is the unique ID of the presence record. Use `ID.unique()` on first creation and persist it for subsequent updates so the same record is reused for the same user across sessions. +- `status` (**required**) is a free-form string up to 256 characters. There are no reserved values, so pick whatever vocabulary fits your app (`online`, `away`, `busy`, `editing`, `typing`). +- `userId` is set automatically from the authenticated session on client SDKs and is required on server SDKs. +- `metadata` is an arbitrary JSON object. Use it to carry any context that subscribers should see together with the status. +- `expiresAt` is optional. Without it, Appwrite applies a default TTL (see [Expiry and cleanup](#expiry-and-cleanup) below). +- `permissions` controls who can read or modify the presence record, the same way it works on rows and files. Without permissions, only the owner and project keys can see it. + +# Get a presence {% #get-a-presence %} + +Fetch a single presence by its `presenceId`. Records whose `expiresAt` is in the past are treated as not found. + +{% multicode %} +```client-web +import { Client, Presences } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const presences = new Presences(client); + +const presence = await presences.get({ + presenceId: '' +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final presences = Presences(client); + +final presence = await presences.get( + presenceId: '', +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let presence = try await presences.get( + presenceId: "" +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Presences + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +val presence = presences.get( + presenceId = "" +) +``` + +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const presences = new sdk.Presences(client); + +const presence = await presences.get({ + presenceId: '' +}); +``` + +```server-python +from appwrite.client import Client +from appwrite.services.presences import Presences + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +presences = Presences(client) + +presence = presences.get( + presence_id = '' +) +``` + +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$presences = new Presences($client); + +$presence = $presences->get( + presenceId: '' +); +``` + +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +presences = Presences.new(client) + +presence = presences.get( + presence_id: '' +) +``` + +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Presences presences = Presences(client); + +Presence presence = await presences.get( + presenceId: '', +); +``` + +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Presences + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val presences = Presences(client) + +val presence = presences.get( + presenceId = "" +) +``` + +```server-java +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Presences; + +Client client = new Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey(""); + +Presences presences = new Presences(client); + +presences.get( + "", // presenceId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + System.out.println(result); + }) +); +``` + +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let presences = Presences(client) + +let presence = try await presences.get( + presenceId: "" +) +``` + +```server-dotnet +using Appwrite; +using Appwrite.Models; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Presences presences = new Presences(client); + +Presence presence = await presences.Get( + presenceId: "" +); +``` + +```server-go +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/presences" +) + +func main() { + cli := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(""), + client.WithKey(""), + ) + + service := presences.New(cli) + + presence, err := service.Get("") + if err != nil { + panic(err) + } + fmt.Println(presence) +} +``` + +```server-rust +use appwrite::Client; +use appwrite::services::Presences; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let presences = Presences::new(&client); + + let presence = presences.get("").await?; + + println!("{:?}", presence); + + Ok(()) +} +``` +{% /multicode %} + +# List presences {% #list-presences %} + +`list` returns the active set. Expired records are filtered out automatically, so the response is always "who is here right now". Pass [Queries](/docs/products/databases/queries) to filter by `status`, `userId`, or any indexed field, and pass `ttl` to cache the response server-side for a configurable number of seconds. + +{% multicode %} +```client-web +import { Client, Presences, Query } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const presences = new Presences(client); + +const result = await presences.list({ + queries: [Query.equal('status', ['online'])] +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final presences = Presences(client); + +final result = await presences.list( + queries: [Query.equal('status', ['online'])], +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let result = try await presences.list( + queries: [Query.equal("status", value: ["online"])] +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.Query +import io.appwrite.services.Presences + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +val result = presences.list( + queries = listOf(Query.equal("status", listOf("online"))) +) +``` + +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const presences = new sdk.Presences(client); + +const result = await presences.list({ + queries: [sdk.Query.equal('status', ['online'])] +}); +``` + +```server-python +from appwrite.client import Client +from appwrite.services.presences import Presences +from appwrite.query import Query + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +presences = Presences(client) + +result = presences.list( + queries = [Query.equal('status', ['online'])] +) +``` + +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$presences = new Presences($client); + +$result = $presences->list( + queries: [Query::equal('status', ['online'])] +); +``` + +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +presences = Presences.new(client) + +result = presences.list( + queries: [Query.equal('status', ['online'])] +) +``` + +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Presences presences = Presences(client); + +PresenceList result = await presences.list( + queries: [Query.equal('status', ['online'])], +); +``` + +```server-kotlin +import io.appwrite.Client +import io.appwrite.Query +import io.appwrite.services.Presences + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val presences = Presences(client) + +val response = presences.list( + queries = listOf(Query.equal("status", listOf("online"))) +) +``` + +```server-java +import io.appwrite.Client; +import io.appwrite.Query; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Presences; + +import java.util.List; + +Client client = new Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey(""); + +Presences presences = new Presences(client); + +presences.list( + List.of(Query.equal("status", List.of("online"))), // queries + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + System.out.println(result); + }) +); +``` + +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let presences = Presences(client) + +let response = try await presences.list( + queries: [Query.equal("status", value: ["online"])] +) +``` + +```server-dotnet +using Appwrite; +using Appwrite.Models; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Presences presences = new Presences(client); + +PresenceList result = await presences.List( + queries: new List { Query.Equal("status", new List { "online" }) } +); +``` + +```server-go +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/presences" + "github.com/appwrite/sdk-for-go/query" +) + +func main() { + cli := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(""), + client.WithKey(""), + ) + + service := presences.New(cli) + + result, err := service.List( + service.WithListQueries([]string{ + query.Equal("status", "online"), + }), + ) + if err != nil { + panic(err) + } + fmt.Println(result) +} +``` + +```server-rust +use appwrite::Client; +use appwrite::services::Presences; +use appwrite::query::Query; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let presences = Presences::new(&client); + + let result = presences.list( + Some(vec![Query::equal("status", vec!["online".into()])]), + None, + None, + ).await?; + + println!("{:?}", result); + + Ok(()) +} +``` +{% /multicode %} + +# Update a presence {% #update-a-presence %} + +`update` patches a subset of fields on an existing record without re-sending the whole payload. Every field except `presenceId` is optional, so a "go away" handler only needs to send `status`. **One naming difference to watch for:** the method is named `update` on client SDKs and `updatePresence` (with each language's case convention) on server SDKs, where it also requires `userId`. This is the only point at which the client and server surfaces diverge. + +{% multicode %} +```client-web +import { Client, Presences } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const presences = new Presences(client); + +const presence = await presences.update({ + presenceId: '', + status: 'away' +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final presences = Presences(client); + +final presence = await presences.update( + presenceId: '', + status: 'away', +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let presence = try await presences.update( + presenceId: "", + status: "away" +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Presences + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +val presence = presences.update( + presenceId = "", + status = "away" +) +``` + +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const presences = new sdk.Presences(client); + +const presence = await presences.updatePresence({ + presenceId: '', + userId: '', + status: 'away' +}); +``` + +```server-python +from appwrite.client import Client +from appwrite.services.presences import Presences + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +presences = Presences(client) + +presence = presences.update_presence( + presence_id = '', + user_id = '', + status = 'away' +) +``` + +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$presences = new Presences($client); + +$presence = $presences->updatePresence( + presenceId: '', + userId: '', + status: 'away' +); +``` + +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +presences = Presences.new(client) + +presence = presences.update_presence( + presence_id: '', + user_id: '', + status: 'away' +) +``` + +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Presences presences = Presences(client); + +Presence presence = await presences.updatePresence( + presenceId: '', + userId: '', + status: 'away', +); +``` + +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Presences + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val presences = Presences(client) + +val presence = presences.updatePresence( + presenceId = "", + userId = "", + status = "away" +) +``` + +```server-java +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Presences; + +Client client = new Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey(""); + +Presences presences = new Presences(client); + +presences.updatePresence( + "", // presenceId + "", // userId + "away", // status + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + System.out.println(result); + }) +); +``` + +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let presences = Presences(client) + +let presence = try await presences.updatePresence( + presenceId: "", + userId: "", + status: "away" +) +``` + +```server-dotnet +using Appwrite; +using Appwrite.Models; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Presences presences = new Presences(client); + +Presence presence = await presences.UpdatePresence( + presenceId: "", + userId: "", + status: "away" +); +``` + +```server-go +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/presences" +) + +func main() { + cli := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(""), + client.WithKey(""), + ) + + service := presences.New(cli) + + presence, err := service.UpdatePresence( + "", + "", + service.WithUpdatePresenceStatus("away"), + ) + if err != nil { + panic(err) + } + fmt.Println(presence) +} +``` + +```server-rust +use appwrite::Client; +use appwrite::services::Presences; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let presences = Presences::new(&client); + + let presence = presences.update_presence( + "", + "", + Some("away"), + None, + None, + None, + None, + ).await?; + + println!("{:?}", presence); + + Ok(()) +} +``` {% /multicode %} -A few notes on the parameters: +# Delete a presence {% #delete-a-presence %} -- `presenceId` (**required**) is the unique ID of the presence record. Use `ID.unique()` on first creation and persist it for subsequent updates so the same record is reused for the same user across sessions. -- `status` (**required**) is a free-form string up to 256 characters. There are no reserved values, so pick whatever vocabulary fits your app (`online`, `away`, `busy`, `editing`, `typing`). -- `userId` is set automatically from the authenticated session on client SDKs. Server SDKs (API key, JWT, Admin) must pass `userId` explicitly because there is no session to read it from. -- `metadata` is an arbitrary JSON object. Use it to carry any context that subscribers should see together with the status. -- `expiresAt` is optional. Without it, Appwrite applies a default TTL (see [Expiry and cleanup](#expiry-and-cleanup) below). -- `permissions` controls who can read or modify the presence record, the same way it works on rows and files. Without permissions, only the owner and project keys can see it. +`delete` removes a record immediately and fires a `delete` event on the presence channels. Use it when you want a user to go offline without waiting for `expiresAt` to elapse, for example on sign out or admin force-offline. + +{% multicode %} +```client-web +import { Client, Presences } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const presences = new Presences(client); + +await presences.delete({ + presenceId: '' +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final presences = Presences(client); + +await presences.delete( + presenceId: '', +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +_ = try await presences.delete( + presenceId: "" +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Presences + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +presences.delete( + presenceId = "" +) +``` + +```server-nodejs +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +const presences = new sdk.Presences(client); + +await presences.delete({ + presenceId: '' +}); +``` + +```server-python +from appwrite.client import Client +from appwrite.services.presences import Presences + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') +client.set_project('') +client.set_key('') + +presences = Presences(client) + +presences.delete( + presence_id = '' +) +``` + +```server-php +setEndpoint('https://.cloud.appwrite.io/v1') + ->setProject('') + ->setKey(''); + +$presences = new Presences($client); + +$presences->delete( + presenceId: '' +); +``` + +```server-ruby +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') + .set_project('') + .set_key('') + +presences = Presences.new(client) + +presences.delete( + presence_id: '' +) +``` + +```server-dart +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setKey(''); + +Presences presences = Presences(client); -Call `presences.update(...)` with the same `presenceId` to patch any subset of these fields without re-sending the whole record; every field on `update` is optional. +await presences.delete( + presenceId: '', +); +``` + +```server-kotlin +import io.appwrite.Client +import io.appwrite.services.Presences + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +val presences = Presences(client) + +presences.delete( + presenceId = "" +) +``` + +```server-java +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Presences; + +Client client = new Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey(""); + +Presences presences = new Presences(client); + +presences.delete( + "", // presenceId + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + System.out.println(result); + }) +); +``` + +```server-swift +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setKey("") + +let presences = Presences(client) + +_ = try await presences.delete( + presenceId: "" +) +``` + +```server-dotnet +using Appwrite; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") + .SetProject("") + .SetKey(""); + +Presences presences = new Presences(client); + +await presences.Delete( + presenceId: "" +); +``` + +```server-go +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/presences" +) + +func main() { + cli := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1"), + client.WithProject(""), + client.WithKey(""), + ) + + service := presences.New(cli) + + _, err := service.Delete("") + if err != nil { + panic(err) + } + fmt.Println("Presence deleted") +} +``` + +```server-rust +use appwrite::Client; +use appwrite::services::Presences; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new() + .set_endpoint("https://.cloud.appwrite.io/v1") + .set_project("") + .set_key(""); + + let presences = Presences::new(&client); + + presences.delete("").await?; + + Ok(()) +} +``` +{% /multicode %} # Subscribe to presence updates {% #subscribe-to-presence-updates %} @@ -129,10 +1456,10 @@ const client = new Client() const realtime = new Realtime(client); const subscription = await realtime.subscribe(Channel.presences(), response => { - if (response.events.includes('presences.*.update')) { + if (response.events.includes('presences.update')) { console.log('Presence updated', response.payload); } - if (response.events.includes('presences.*.delete')) { + if (response.events.includes('presences.delete')) { console.log('Presence expired or removed', response.payload); } }); @@ -150,10 +1477,10 @@ final realtime = Realtime(client); final subscription = realtime.subscribe([Channel.presences()]); subscription.stream.listen((response) { - if (response.events.contains('presences.*.update')) { + if (response.events.contains('presences.update')) { print('Presence updated: ${response.payload}'); } - if (response.events.contains('presences.*.delete')) { + if (response.events.contains('presences.delete')) { print('Presence expired or removed: ${response.payload}'); } }); @@ -169,10 +1496,10 @@ let client = Client() let realtime = Realtime(client) let subscription = realtime.subscribe(channels: [Channel.presences()]) { response in - if (response.events?.contains("presences.*.update") == true) { + if (response.events?.contains("presences.update") == true) { print("Presence updated: \(String(describing: response.payload))") } - if (response.events?.contains("presences.*.delete") == true) { + if (response.events?.contains("presences.delete") == true) { print("Presence expired or removed: \(String(describing: response.payload))") } } @@ -190,10 +1517,10 @@ val client = Client(context) val realtime = Realtime(client) val subscription = realtime.subscribe(Channel.presences()) { - if (it.events.contains("presences.*.update")) { + if (it.events.contains("presences.update")) { println("Presence updated: ${it.payload}") } - if (it.events.contains("presences.*.delete")) { + if (it.events.contains("presences.delete")) { println("Presence expired or removed: ${it.payload}") } } @@ -202,10 +1529,10 @@ val subscription = realtime.subscribe(Channel.presences()) { The `events` array follows the same pattern as every other Appwrite resource: -- `presences.*.create` and `presences..create` for new records. -- `presences.*.upsert` and `presences..upsert` for the unified create-or-update path that fires on every `upsert()` call. -- `presences.*.update` and `presences..update` for status, metadata, or expiry changes. -- `presences.*.delete` and `presences..delete` for records that were deleted explicitly or expired automatically. +- `presences.create` and `presences..create` for new records. +- `presences.upsert` and `presences..upsert` for the unified create-or-update path that fires on every `upsert()` call. +- `presences.update` and `presences..update` for status, metadata, or expiry changes. +- `presences.delete` and `presences..delete` for records that were deleted explicitly or expired automatically. This gives you a clean signal for "user just came online", "user changed status", and "user went offline", without writing any custom socket logic. @@ -233,25 +1560,7 @@ Every presence carries an `expiresAt` timestamp. Once that time passes, Appwrite You can pass an explicit `expiresAt` up to **30 days in the future**. If you omit it, Appwrite applies a sensible default that fits the typical heartbeat pattern: keep upserting the presence every few seconds while the user is active, and let it expire naturally a short time after the last heartbeat. -To remove a presence immediately, for example on sign out or when the user closes a document, send a delete: - -{% multicode %} -```client-web -await presences.delete({ presenceId: '' }); -``` - -```client-flutter -await presences.delete(presenceId: ''); -``` - -```client-apple -try await presences.delete(presenceId: "") -``` - -```client-android-kotlin -presences.delete(presenceId = "") -``` -{% /multicode %} +To remove a presence immediately, for example on sign out or when the user closes a document, use the [Delete a presence](#delete-a-presence) operation above. # Permissions and scopes {% #permissions-and-scopes %} diff --git a/src/routes/docs/products/auth/presence/+page.markdoc b/src/routes/docs/products/auth/presence/+page.markdoc index 80193618ab7..13f1f13c0ff 100644 --- a/src/routes/docs/products/auth/presence/+page.markdoc +++ b/src/routes/docs/products/auth/presence/+page.markdoc @@ -6,7 +6,7 @@ description: Track which signed-in users are active right now and broadcast thei Authentication tells you **who a user is**. Presence tells you **whether they are around right now**. The Appwrite **Presence API** records a live status for each signed-in user and broadcasts every change over [Realtime](/docs/apis/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`, an optional `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](/docs/advanced/platform/permissions). +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](/docs/advanced/platform/permissions). # Set the user's presence {% #set-the-users-presence %} From 87ef8ea017cc13173cfb6bf1218b12b9aa0d8251 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Tue, 19 May 2026 21:27:55 +0530 Subject: [PATCH 03/12] add note on upsert being keyed by userid --- src/routes/docs/apis/realtime/presence/+page.markdoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index e7ddf10bdc8..52c80293145 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -341,6 +341,10 @@ A few notes on the parameters: - `expiresAt` is optional. Without it, Appwrite applies a default TTL (see [Expiry and cleanup](#expiry-and-cleanup) below). - `permissions` controls who can read or modify the presence record, the same way it works on rows and files. Without permissions, only the owner and project keys can see it. +{% info title="Upsert is keyed by userId" %} +A user can have at most one active presence at a time. `upsert` looks up an existing record by `userId` first and updates it in place if one is found, so the `presenceId` you pass is only used as the new record's `$id` on the very first create. For `get`, `update`, and `delete`, the actual `$id` returned by upsert is still the addressing key. +{% /info %} + # Get a presence {% #get-a-presence %} Fetch a single presence by its `presenceId`. Records whose `expiresAt` is in the past are treated as not found. From 780787bea3d2ef44533890e457220116cc894276 Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Tue, 19 May 2026 21:45:01 +0530 Subject: [PATCH 04/12] Update src/routes/blog/post/announcing-presence-api/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/blog/post/announcing-presence-api/+page.markdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 0fffa99d828..d788d51ad07 100644 --- a/src/routes/blog/post/announcing-presence-api/+page.markdoc +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -36,7 +36,7 @@ Presence is a first-class Appwrite resource for short-lived user statuses, with - **Upsert-first writes** so you can call the same method on every focus, route change, or heartbeat without worrying about duplicates. - **Automatic expiry** controlled by an `expiresAt` timestamp (up to 30 days). Stale records disappear on their own, no cleanup cron required. -- **Dedicated Realtime channels** (`presences` and `presences.`) that emit `create`, `update`, and `delete` events for every record a subscriber has permission to read. +- **Dedicated Realtime channels** (`presences` and `presences.`) that emit `create`, `upsert`, `update`, and `delete` events for every record a subscriber has permission to read. - **Free-form status and metadata** so a presence can mean "online", "typing in #general", or "viewing document `abc123`", whichever vocabulary fits your app. - **Permission-aware subscriptions** that reuse `Role.users()`, `Role.team()`, and `Role.user()`, so collaboration features only leak status to the right people. - **A `Presences` service in every SDK**, with the matching scopes (`presences.read`, `presences.write`) on the server side. From 457bc52c6a94975ac2d450d4ac7417c5a932e0f9 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Tue, 19 May 2026 22:21:47 +0530 Subject: [PATCH 05/12] Include Realtime queries mention --- .../announcing-presence-api/+page.markdoc | 29 ++++++++++++++++++- .../changelog/(entries)/2026-05-19-2.markdoc | 2 ++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 0fffa99d828..7e5e5f22132 100644 --- a/src/routes/blog/post/announcing-presence-api/+page.markdoc +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -142,9 +142,36 @@ await realtime.subscribe(Channel.presences(), response => { The `delete` event fires both when you remove a presence explicitly and when it expires automatically, so a single handler can drive the "user just went offline" branch of your UI either way. +# Pair it with Realtime queries + +Presence gets sharper when you combine it with [Realtime queries](/blog/post/announcing-realtime-queries), which let you pass SDK queries to `realtime.subscribe(...)` so events are filtered server-side. Instead of receiving every presence event on `Channel.presences()` and discarding the ones you do not care about in your callback, you subscribe with a query and only see the events that match. + +```client-web +import { Client, Realtime, Channel, Query } from "appwrite"; + +const realtime = new Realtime(client); + +// Only receive online players, filtered server-side. +await realtime.subscribe( + Channel.presences(), + response => { + console.log(response.payload); + }, + [Query.equal('status', ['online'])] +); +``` + +This is what makes the API a fit for the more demanding use cases on top of online indicators: + +- **Multiplayer games.** Set a `status` per zone or party and subscribe with a matching query, so a client only receives presence updates for the players it actually needs to render on screen. +- **Live movement tracking.** In a collaborative editor or shared map, subscribe with a query keyed to the document or tile the user is on, so cursor positions from people elsewhere never reach the client. +- **Filtered "who is online" lists.** Subscribe with `Query.equal('status', ['online'])` and `away` records never trigger your handler. + +**Realtime + Presence + Queries** together give you a low-bandwidth, server-filtered "who is here right now, doing what" stream that scales without per-client filtering logic. + # When to reach for Presence -Presence is the right primitive for any UI cue that should appear when a user is around and disappear when they are not: +Beyond the demanding scenarios above, Presence is the right primitive for any UI cue that should appear when a user is around and disappear when they are not: - Online indicators in a team directory, contacts list, or member sidebar. - Collaboration cursors that show which document or row a teammate is viewing. diff --git a/src/routes/changelog/(entries)/2026-05-19-2.markdoc b/src/routes/changelog/(entries)/2026-05-19-2.markdoc index 06a9bbf07f1..b6ef1153eb3 100644 --- a/src/routes/changelog/(entries)/2026-05-19-2.markdoc +++ b/src/routes/changelog/(entries)/2026-05-19-2.markdoc @@ -9,6 +9,8 @@ Appwrite now ships a first-class **Presence API** for short-lived user statuses Presences broadcast every change over dedicated Realtime channels (`presences` and `presences.`), so an "online now" list, a typing indicator, or a "viewing this page" cue is a single `Channel.presences()` subscription away. Stale records expire and emit `delete` events automatically, no cleanup job required. +Combine it with [Realtime queries](/blog/post/announcing-realtime-queries) and a client only receives the presence events its UI actually needs to render, which makes the API a fit for multiplayer games and live movement tracking as much as for online indicators. + {% arrow_link href="/blog/post/announcing-presence-api" %} Read the announcement {% /arrow_link %} From c5758a5534d7b329c179ef783762e0ff0f8cde4c Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Tue, 19 May 2026 22:23:51 +0530 Subject: [PATCH 06/12] update date to may 20 --- .gitignore | 5 ++++- src/routes/blog/post/announcing-presence-api/+page.markdoc | 2 +- .../(entries)/{2026-05-19-2.markdoc => 2026-05-20.markdoc} | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) rename src/routes/changelog/(entries)/{2026-05-19-2.markdoc => 2026-05-20.markdoc} (98%) diff --git a/.gitignore b/.gitignore index c819cc91e90..d78e97fedc9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,7 @@ terraform/**/**/*.tfstate* # Sentry Config File .env.sentry-build-plugin -.playwright-mcp \ No newline at end of file +.playwright-mcp + +SKILL.md +AGENTS.md \ No newline at end of file diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 27088bb6358..742f3dc8fc1 100644 --- a/src/routes/blog/post/announcing-presence-api/+page.markdoc +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -2,7 +2,7 @@ layout: post title: "Announcing the Presence API: Track who is online, typing, and active in realtime" description: A new Appwrite API for short-lived user statuses, with built-in Realtime channels, automatic expiry, and permission-aware subscriptions. -date: 2026-05-19 +date: 2026-05-20 cover: /images/blog/announcing-presence-api/cover.png timeToRead: 5 author: aditya-oberai diff --git a/src/routes/changelog/(entries)/2026-05-19-2.markdoc b/src/routes/changelog/(entries)/2026-05-20.markdoc similarity index 98% rename from src/routes/changelog/(entries)/2026-05-19-2.markdoc rename to src/routes/changelog/(entries)/2026-05-20.markdoc index b6ef1153eb3..81e9946576f 100644 --- a/src/routes/changelog/(entries)/2026-05-19-2.markdoc +++ b/src/routes/changelog/(entries)/2026-05-20.markdoc @@ -1,7 +1,7 @@ --- layout: changelog title: "Track who is online with the new Presence API" -date: 2026-05-19 +date: 2026-05-20 cover: /images/blog/announcing-presence-api/cover.avif --- From 2ff7a1df7d4d815c9be8322cc7c087258a4edcd7 Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Tue, 19 May 2026 22:37:27 +0530 Subject: [PATCH 07/12] Update src/routes/docs/apis/realtime/presence/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/docs/apis/realtime/presence/+page.markdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index 52c80293145..c08ea8027f2 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -1553,7 +1553,7 @@ This gives you a clean signal for "user just came online", "user changed status" --- * `presences.` * `Channel.presence('')` -* Any update or delete event on a specific presence record. +* Any create, upsert, update, or delete event on a specific presence record. {% /table %} You can also append `.create()`, `.upsert()`, `.update()`, or `.delete()` to `Channel.presence('')` to narrow the stream to a single event type, identical to how channel filters work on every other resource. From 5fec97e4ef3526da11e792366c85f22fb591d428 Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Tue, 19 May 2026 22:37:42 +0530 Subject: [PATCH 08/12] Update src/routes/docs/apis/realtime/channels/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/docs/apis/realtime/channels/+page.markdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/docs/apis/realtime/channels/+page.markdoc b/src/routes/docs/apis/realtime/channels/+page.markdoc index ddfb0de7a61..a7271295336 100644 --- a/src/routes/docs/apis/realtime/channels/+page.markdoc +++ b/src/routes/docs/apis/realtime/channels/+page.markdoc @@ -212,11 +212,11 @@ A list of all channels available you can subscribe to. When using `Channel` help --- * `presences` * `Channel.presences()` -* Any create, update, or delete event on any [presence](/docs/apis/realtime/presence) the subscriber can read. +* Any create, upsert, update, or delete event on any [presence](/docs/apis/realtime/presence) the subscriber can read. --- * `presences.` * `Channel.presence('')` -* Any update or delete event on a given presence record. +* Any create, upsert, update, or delete event on a given presence record. {% /table %} From 9f547587ca2e09a3c1386d0517399d35773efbdb Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Tue, 19 May 2026 22:47:17 +0530 Subject: [PATCH 09/12] Update src/routes/docs/apis/realtime/presence/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/docs/apis/realtime/presence/+page.markdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index c08ea8027f2..bf4149b0437 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -1549,7 +1549,7 @@ This gives you a clean signal for "user just came online", "user changed status" --- * `presences` * `Channel.presences()` -* Any create, update, or delete event on any presence the subscriber can read. +* Any create, upsert, update, or delete event on any presence the subscriber can read. --- * `presences.` * `Channel.presence('')` From 57da5374b21331d939b0f0c1e2aa61b4314cc244 Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Wed, 20 May 2026 01:45:44 +0530 Subject: [PATCH 10/12] Update src/routes/docs/apis/realtime/presence/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/docs/apis/realtime/presence/+page.markdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index bf4149b0437..31abae4c480 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -1460,10 +1460,10 @@ const client = new Client() const realtime = new Realtime(client); const subscription = await realtime.subscribe(Channel.presences(), response => { - if (response.events.includes('presences.update')) { + if (response.events.includes('presences.*.update')) { console.log('Presence updated', response.payload); } - if (response.events.includes('presences.delete')) { + if (response.events.includes('presences.*.delete')) { console.log('Presence expired or removed', response.payload); } }); From cef477ad8b7cb78a56b93ec91c3a693f82bed5cd Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Wed, 20 May 2026 17:48:37 +0530 Subject: [PATCH 11/12] Update src/routes/docs/apis/realtime/presence/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/docs/apis/realtime/presence/+page.markdoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index 31abae4c480..c684a5b58db 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -1533,10 +1533,10 @@ val subscription = realtime.subscribe(Channel.presences()) { The `events` array follows the same pattern as every other Appwrite resource: -- `presences.create` and `presences..create` for new records. -- `presences.upsert` and `presences..upsert` for the unified create-or-update path that fires on every `upsert()` call. -- `presences.update` and `presences..update` for status, metadata, or expiry changes. -- `presences.delete` and `presences..delete` for records that were deleted explicitly or expired automatically. +- `presences.*.create` and `presences..create` for new records. +- `presences.*.upsert` and `presences..upsert` for the unified create-or-update path that fires on every `upsert()` call. +- `presences.*.update` and `presences..update` for status, metadata, or expiry changes. +- `presences.*.delete` and `presences..delete` for records that were deleted explicitly or expired automatically. This gives you a clean signal for "user just came online", "user changed status", and "user went offline", without writing any custom socket logic. From 00a8400c24a57e514780a454346f834c9a945fc2 Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Wed, 20 May 2026 18:04:08 +0530 Subject: [PATCH 12/12] Update src/routes/docs/apis/realtime/presence/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/docs/apis/realtime/presence/+page.markdoc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index c684a5b58db..0ba5ab1629a 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -1460,11 +1460,10 @@ const client = new Client() const realtime = new Realtime(client); const subscription = await realtime.subscribe(Channel.presences(), response => { - if (response.events.includes('presences.*.update')) { - console.log('Presence updated', response.payload); - } if (response.events.includes('presences.*.delete')) { console.log('Presence expired or removed', response.payload); + } else if (response.events.includes('presences.*.upsert') || response.events.includes('presences.*.update')) { + console.log('Presence created or updated', response.payload); } }); ```