From 5339271fe7fa14a0ea9a3d42ec09bab7d96479fa Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 31 Mar 2026 21:22:59 +0530 Subject: [PATCH 01/49] Add agent skills auto-install note to init project docs --- src/routes/docs/tooling/command-line/installation/+page.markdoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/docs/tooling/command-line/installation/+page.markdoc b/src/routes/docs/tooling/command-line/installation/+page.markdoc index a63d163f47f..53bd80ee712 100644 --- a/src/routes/docs/tooling/command-line/installation/+page.markdoc +++ b/src/routes/docs/tooling/command-line/installation/+page.markdoc @@ -147,6 +147,8 @@ This will create your `appwrite.config.json` file, where you will configure your } ``` +The CLI will also auto-detect your project configuration and automatically install relevant [Appwrite agent skills](/docs/tooling/agent-skills). + You can run your first CLI command after logging in. Try fetching information about your Appwrite project. ```sh From 0717909ed42345120a2b003029cc1ac246213881 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 31 Mar 2026 21:30:52 +0530 Subject: [PATCH 02/49] Update src/routes/docs/tooling/command-line/installation/+page.markdoc Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/routes/docs/tooling/command-line/installation/+page.markdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/docs/tooling/command-line/installation/+page.markdoc b/src/routes/docs/tooling/command-line/installation/+page.markdoc index 53bd80ee712..c282c6ab2ca 100644 --- a/src/routes/docs/tooling/command-line/installation/+page.markdoc +++ b/src/routes/docs/tooling/command-line/installation/+page.markdoc @@ -147,7 +147,7 @@ This will create your `appwrite.config.json` file, where you will configure your } ``` -The CLI will also auto-detect your project configuration and automatically install relevant [Appwrite agent skills](/docs/tooling/agent-skills). +The CLI will also auto-detect your project configuration and automatically install relevant [Appwrite agent skills](/docs/tooling/ai/skills). You can run your first CLI command after logging in. Try fetching information about your Appwrite project. From 351e4e02b97ab90d803a004240307ad95a308c1e Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Wed, 1 Apr 2026 01:47:45 +0530 Subject: [PATCH 03/49] add notes to ai docs skill section --- src/routes/docs/tooling/ai/skills/+page.markdoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/docs/tooling/ai/skills/+page.markdoc b/src/routes/docs/tooling/ai/skills/+page.markdoc index 0570fa7e408..63d950887aa 100644 --- a/src/routes/docs/tooling/ai/skills/+page.markdoc +++ b/src/routes/docs/tooling/ai/skills/+page.markdoc @@ -25,6 +25,10 @@ Skills work across all major AI dev tools that support them. They are installed # Install skills {% #install-skills %} +{% info title="Automatic installation" %} +When you run `appwrite init project`, the Appwrite CLI auto-detects your project configuration and installs relevant skills automatically. You can also install skills manually using the steps below. +{% /info %} + {% section #step-1 step=1 title="Run the install command" %} Run the following command in your project directory: From dd9023a4cf86558985fee477efc44810d17d29dc Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Mon, 18 May 2026 11:18:14 +0530 Subject: [PATCH 04/49] Restructure AI docs: split ai-dev-tools into agentic-coding and vibe-coding, drop legacy /docs/tooling/mcp section --- src/partials/mcp-add-ides-tools.md | 24 +-- src/redirects.json | 64 +++++++- src/routes/(marketing)/(components)/ai.svelte | 12 +- .../(marketing)/(components)/platforms.svelte | 4 +- src/routes/docs/+page.svelte | 8 +- src/routes/docs/tooling/ai/+layout.svelte | 22 +-- src/routes/docs/tooling/ai/+page.markdoc | 44 ++--- .../antigravity/+page.markdoc | 0 .../claude-code/+page.markdoc | 0 .../codex/+page.markdoc | 0 .../cursor/+page.markdoc | 0 .../opencode/+page.markdoc | 0 .../vscode/+page.markdoc | 0 .../windsurf/+page.markdoc | 0 .../tooling/ai/responsible-ai/+page.markdoc | 2 +- .../bolt/+page.markdoc | 0 .../claude-desktop/+page.markdoc | 0 .../emergent/+page.markdoc | 0 .../lovable/+page.markdoc | 0 .../zenflow/+page.markdoc | 0 src/routes/docs/tooling/mcp/+layout.svelte | 78 --------- src/routes/docs/tooling/mcp/+page.markdoc | 41 ----- .../tooling/mcp/antigravity/+page.markdoc | 116 -------------- src/routes/docs/tooling/mcp/api/+page.markdoc | 118 -------------- .../tooling/mcp/claude-code/+page.markdoc | 100 ------------ .../tooling/mcp/claude-desktop/+page.markdoc | 144 ----------------- .../docs/tooling/mcp/cursor/+page.markdoc | 150 ------------------ .../docs/tooling/mcp/docs/+page.markdoc | 72 --------- .../docs/tooling/mcp/opencode/+page.markdoc | 118 -------------- .../docs/tooling/mcp/vscode/+page.markdoc | 121 -------------- .../docs/tooling/mcp/windsurf/+page.markdoc | 123 -------------- .../docs/tooling/mcp/zenflow/+page.markdoc | 120 -------------- 32 files changed, 114 insertions(+), 1367 deletions(-) rename src/routes/docs/tooling/ai/{ai-dev-tools => agentic-coding}/antigravity/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => agentic-coding}/claude-code/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => agentic-coding}/codex/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => agentic-coding}/cursor/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => agentic-coding}/opencode/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => agentic-coding}/vscode/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => agentic-coding}/windsurf/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => vibe-coding}/bolt/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => vibe-coding}/claude-desktop/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => vibe-coding}/emergent/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => vibe-coding}/lovable/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{ai-dev-tools => vibe-coding}/zenflow/+page.markdoc (100%) delete mode 100644 src/routes/docs/tooling/mcp/+layout.svelte delete mode 100644 src/routes/docs/tooling/mcp/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/antigravity/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/api/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/claude-code/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/claude-desktop/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/cursor/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/docs/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/opencode/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/vscode/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/windsurf/+page.markdoc delete mode 100644 src/routes/docs/tooling/mcp/zenflow/+page.markdoc diff --git a/src/partials/mcp-add-ides-tools.md b/src/partials/mcp-add-ides-tools.md index 48ed4d86dae..28d2f5c6067 100644 --- a/src/partials/mcp-add-ides-tools.md +++ b/src/partials/mcp-add-ides-tools.md @@ -3,28 +3,28 @@ You can add the MCP server to various AI tools and code editors: {% only_light %} {% cards %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/mcp/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/claude.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/claude.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/mcp/zenflow" title="Zenflow" image="/images/docs/mcp/logos/zenflow.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} {% /cards_item %} {% /cards %} @@ -33,28 +33,28 @@ You can add the MCP server to various AI tools and code editors: {% only_dark %} {% cards %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/mcp/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/dark/claude.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/dark/claude.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/mcp/zenflow" title="Zenflow" image="/images/docs/mcp/logos/dark/zenflow.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} {% /cards_item %} {% /cards %} diff --git a/src/redirects.json b/src/redirects.json index 85d37658dec..d4438f1f1e3 100644 --- a/src/redirects.json +++ b/src/redirects.json @@ -782,31 +782,31 @@ }, { "link": "/docs/tooling/mcp/cursor", - "redirect": "/docs/tooling/ai/ai-dev-tools/cursor" + "redirect": "/docs/tooling/ai/agentic-coding/cursor" }, { "link": "/docs/tooling/mcp/vscode", - "redirect": "/docs/tooling/ai/ai-dev-tools/vscode" + "redirect": "/docs/tooling/ai/agentic-coding/vscode" }, { "link": "/docs/tooling/mcp/windsurf", - "redirect": "/docs/tooling/ai/ai-dev-tools/windsurf" + "redirect": "/docs/tooling/ai/agentic-coding/windsurf" }, { "link": "/docs/tooling/mcp/opencode", - "redirect": "/docs/tooling/ai/ai-dev-tools/opencode" + "redirect": "/docs/tooling/ai/agentic-coding/opencode" }, { "link": "/docs/tooling/mcp/antigravity", - "redirect": "/docs/tooling/ai/ai-dev-tools/antigravity" + "redirect": "/docs/tooling/ai/agentic-coding/antigravity" }, { "link": "/docs/tooling/mcp/claude-code", - "redirect": "/docs/tooling/ai/ai-dev-tools/claude-code" + "redirect": "/docs/tooling/ai/agentic-coding/claude-code" }, { "link": "/docs/tooling/mcp/claude-desktop", - "redirect": "/docs/tooling/ai/ai-dev-tools/claude-desktop" + "redirect": "/docs/tooling/ai/vibe-coding/claude-desktop" }, { "link": "/docs/tooling/mcp/api", @@ -830,12 +830,60 @@ }, { "link": "/docs/tooling/mcp/zenflow", - "redirect": "/docs/tooling/ai/ai-dev-tools/zenflow" + "redirect": "/docs/tooling/ai/vibe-coding/zenflow" }, { "link": "/docs/tooling/ai/ai-dev-tools", "redirect": "/docs/tooling/ai/" }, + { + "link": "/docs/tooling/ai/ai-dev-tools/claude-code", + "redirect": "/docs/tooling/ai/agentic-coding/claude-code" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/codex", + "redirect": "/docs/tooling/ai/agentic-coding/codex" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/cursor", + "redirect": "/docs/tooling/ai/agentic-coding/cursor" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/vscode", + "redirect": "/docs/tooling/ai/agentic-coding/vscode" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/opencode", + "redirect": "/docs/tooling/ai/agentic-coding/opencode" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/antigravity", + "redirect": "/docs/tooling/ai/agentic-coding/antigravity" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/windsurf", + "redirect": "/docs/tooling/ai/agentic-coding/windsurf" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/claude-desktop", + "redirect": "/docs/tooling/ai/vibe-coding/claude-desktop" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/lovable", + "redirect": "/docs/tooling/ai/vibe-coding/lovable" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/emergent", + "redirect": "/docs/tooling/ai/vibe-coding/emergent" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/bolt", + "redirect": "/docs/tooling/ai/vibe-coding/bolt" + }, + { + "link": "/docs/tooling/ai/ai-dev-tools/zenflow", + "redirect": "/docs/tooling/ai/vibe-coding/zenflow" + }, { "link": "/install/compose", "redirect": "/docs/advanced/self-hosting/installation#manual-installation" diff --git a/src/routes/(marketing)/(components)/ai.svelte b/src/routes/(marketing)/(components)/ai.svelte index c522d00d76c..ac4a91b8eea 100644 --- a/src/routes/(marketing)/(components)/ai.svelte +++ b/src/routes/(marketing)/(components)/ai.svelte @@ -19,7 +19,7 @@ const tools: AiStripTool[] = [ { name: 'Claude Code', - href: '/docs/tooling/ai/ai-dev-tools/claude-code', + href: '/docs/tooling/ai/agentic-coding/claude-code', dark: '/images/docs/mcp/logos/dark/claude.svg', light: '/images/docs/mcp/logos/claude.svg', primary: '#D97659', @@ -27,7 +27,7 @@ }, { name: 'Codex', - href: '/docs/tooling/ai/ai-dev-tools/codex', + href: '/docs/tooling/ai/agentic-coding/codex', dark: '/images/docs/mcp/logos/dark/openai.svg', light: '/images/docs/mcp/logos/openai.svg', primary: '#10A37F', @@ -35,7 +35,7 @@ }, { name: 'Cursor', - href: '/docs/tooling/ai/ai-dev-tools/cursor', + href: '/docs/tooling/ai/agentic-coding/cursor', dark: '/images/docs/mcp/logos/dark/cursor-ai.svg', light: '/images/docs/mcp/logos/cursor-ai.svg', primary: '#141414', @@ -43,7 +43,7 @@ }, { name: 'VS Code', - href: '/docs/tooling/ai/ai-dev-tools/vscode', + href: '/docs/tooling/ai/agentic-coding/vscode', dark: '/images/docs/mcp/logos/dark/vscode.svg', light: '/images/docs/mcp/logos/vscode.svg', primary: '#0078D7', @@ -51,7 +51,7 @@ }, { name: 'OpenCode', - href: '/docs/tooling/ai/ai-dev-tools/opencode', + href: '/docs/tooling/ai/agentic-coding/opencode', dark: '/images/docs/mcp/logos/dark/opencode.svg', light: '/images/docs/mcp/logos/opencode.svg', primary: '#FFFFFF', @@ -59,7 +59,7 @@ }, { name: 'Google Antigravity', - href: '/docs/tooling/ai/ai-dev-tools/antigravity', + href: '/docs/tooling/ai/agentic-coding/antigravity', dark: '/images/docs/mcp/logos/dark/google-antigravity.svg', light: '/images/docs/mcp/logos/google-antigravity.svg', primary: '#4285F4', diff --git a/src/routes/(marketing)/(components)/platforms.svelte b/src/routes/(marketing)/(components)/platforms.svelte index 2330287f062..c820514ba12 100644 --- a/src/routes/(marketing)/(components)/platforms.svelte +++ b/src/routes/(marketing)/(components)/platforms.svelte @@ -37,7 +37,7 @@ name: 'Codex', dark: '/images/docs/mcp/logos/dark/openai.svg', light: '/images/docs/mcp/logos/openai.svg', - href: '/docs/tooling/ai/ai-dev-tools/codex', + href: '/docs/tooling/ai/agentic-coding/codex', primary: '#10A37F', secondary: '#064E3B' }, @@ -53,7 +53,7 @@ name: 'Lovable', dark: '/images/docs/mcp/logos/dark/lovable.svg', light: '/images/docs/mcp/logos/lovable.svg', - href: '/docs/tooling/ai/ai-dev-tools/lovable', + href: '/docs/tooling/ai/vibe-coding/lovable', primary: '#FF6355', secondary: '#C43D32' }, diff --git a/src/routes/docs/+page.svelte b/src/routes/docs/+page.svelte index 91eb9340456..0597c09e277 100644 --- a/src/routes/docs/+page.svelte +++ b/src/routes/docs/+page.svelte @@ -38,7 +38,7 @@ event: 'docs-ai-ide_claude-code-click' }, { - href: '/docs/tooling/ai/ai-dev-tools/codex', + href: '/docs/tooling/ai/agentic-coding/codex', title: 'Codex', logoDark: '/images/docs/mcp/logos/dark/openai.svg', logoLight: '/images/docs/mcp/logos/openai.svg', @@ -83,21 +83,21 @@ event: 'docs-ai-vibe_claude-desktop-click' }, { - href: '/docs/tooling/ai/ai-dev-tools/lovable', + href: '/docs/tooling/ai/vibe-coding/lovable', title: 'Lovable', logoDark: '/images/docs/mcp/logos/dark/lovable.svg', logoLight: '/images/docs/mcp/logos/lovable.svg', event: 'docs-ai-vibe_lovable-click' }, { - href: '/docs/tooling/ai/ai-dev-tools/emergent', + href: '/docs/tooling/ai/vibe-coding/emergent', title: 'Emergent', logoDark: '/images/docs/mcp/logos/dark/emergent.svg', logoLight: '/images/docs/mcp/logos/emergent.svg', event: 'docs-ai-vibe_emergent-click' }, { - href: '/docs/tooling/ai/ai-dev-tools/bolt', + href: '/docs/tooling/ai/vibe-coding/bolt', title: 'Bolt', logoDark: '/images/docs/mcp/logos/dark/bolt.svg', logoLight: '/images/docs/mcp/logos/bolt.svg', diff --git a/src/routes/docs/tooling/ai/+layout.svelte b/src/routes/docs/tooling/ai/+layout.svelte index ef43e155d55..57d8b0eeee6 100644 --- a/src/routes/docs/tooling/ai/+layout.svelte +++ b/src/routes/docs/tooling/ai/+layout.svelte @@ -51,27 +51,27 @@ items: [ { label: 'Claude Code', - href: '/docs/tooling/ai/ai-dev-tools/claude-code' + href: '/docs/tooling/ai/agentic-coding/claude-code' }, { label: 'Codex', - href: '/docs/tooling/ai/ai-dev-tools/codex' + href: '/docs/tooling/ai/agentic-coding/codex' }, { label: 'Cursor', - href: '/docs/tooling/ai/ai-dev-tools/cursor' + href: '/docs/tooling/ai/agentic-coding/cursor' }, { label: 'VS Code', - href: '/docs/tooling/ai/ai-dev-tools/vscode' + href: '/docs/tooling/ai/agentic-coding/vscode' }, { label: 'OpenCode', - href: '/docs/tooling/ai/ai-dev-tools/opencode' + href: '/docs/tooling/ai/agentic-coding/opencode' }, { label: 'Google Antigravity', - href: '/docs/tooling/ai/ai-dev-tools/antigravity' + href: '/docs/tooling/ai/agentic-coding/antigravity' } ] }, @@ -80,23 +80,23 @@ items: [ { label: 'Claude Desktop', - href: '/docs/tooling/ai/ai-dev-tools/claude-desktop' + href: '/docs/tooling/ai/vibe-coding/claude-desktop' }, { label: 'Lovable', - href: '/docs/tooling/ai/ai-dev-tools/lovable' + href: '/docs/tooling/ai/vibe-coding/lovable' }, { label: 'Emergent', - href: '/docs/tooling/ai/ai-dev-tools/emergent' + href: '/docs/tooling/ai/vibe-coding/emergent' }, { label: 'Bolt', - href: '/docs/tooling/ai/ai-dev-tools/bolt' + href: '/docs/tooling/ai/vibe-coding/bolt' }, { label: 'Zenflow', - href: '/docs/tooling/ai/ai-dev-tools/zenflow' + href: '/docs/tooling/ai/vibe-coding/zenflow' } ] }, diff --git a/src/routes/docs/tooling/ai/+page.markdoc b/src/routes/docs/tooling/ai/+page.markdoc index 00747cf732e..0376a9b180b 100644 --- a/src/routes/docs/tooling/ai/+page.markdoc +++ b/src/routes/docs/tooling/ai/+page.markdoc @@ -13,22 +13,22 @@ AI-powered IDEs and code editors provide intelligent code completion and context {% only_light %} {% cards %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/antigravity" title="Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} {% /cards_item %} {% /cards %} @@ -37,22 +37,22 @@ AI-powered IDEs and code editors provide intelligent code completion and context {% only_dark %} {% cards %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/antigravity" title="Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} {% /cards_item %} {% /cards %} @@ -65,19 +65,19 @@ Vibe coding platforms let you build applications through natural language. Descr {% only_light %} {% cards %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/claude.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/lovable" title="Lovable" image="/images/docs/mcp/logos/lovable.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/lovable" title="Lovable" image="/images/docs/mcp/logos/lovable.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/emergent" title="Emergent" image="/images/docs/mcp/logos/emergent.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/emergent" title="Emergent" image="/images/docs/mcp/logos/emergent.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/bolt" title="Bolt" image="/images/docs/mcp/logos/bolt.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/bolt" title="Bolt" image="/images/docs/mcp/logos/bolt.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/zenflow" title="Zenflow" image="/images/docs/mcp/logos/zenflow.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/zenflow" title="Zenflow" image="/images/docs/mcp/logos/zenflow.svg" %} {% /cards_item %} {% /cards %} @@ -86,19 +86,19 @@ Vibe coding platforms let you build applications through natural language. Descr {% only_dark %} {% cards %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/dark/claude.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/dark/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/lovable" title="Lovable" image="/images/docs/mcp/logos/dark/lovable.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/lovable" title="Lovable" image="/images/docs/mcp/logos/dark/lovable.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/emergent" title="Emergent" image="/images/docs/mcp/logos/dark/emergent.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/emergent" title="Emergent" image="/images/docs/mcp/logos/dark/emergent.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/bolt" title="Bolt" image="/images/docs/mcp/logos/dark/bolt.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/bolt" title="Bolt" image="/images/docs/mcp/logos/dark/bolt.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/ai-dev-tools/zenflow" title="Zenflow" image="/images/docs/mcp/logos/dark/zenflow.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/zenflow" title="Zenflow" image="/images/docs/mcp/logos/dark/zenflow.svg" %} {% /cards_item %} {% /cards %} diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/antigravity/+page.markdoc b/src/routes/docs/tooling/ai/agentic-coding/antigravity/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/antigravity/+page.markdoc rename to src/routes/docs/tooling/ai/agentic-coding/antigravity/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/claude-code/+page.markdoc b/src/routes/docs/tooling/ai/agentic-coding/claude-code/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/claude-code/+page.markdoc rename to src/routes/docs/tooling/ai/agentic-coding/claude-code/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/codex/+page.markdoc b/src/routes/docs/tooling/ai/agentic-coding/codex/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/codex/+page.markdoc rename to src/routes/docs/tooling/ai/agentic-coding/codex/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/cursor/+page.markdoc b/src/routes/docs/tooling/ai/agentic-coding/cursor/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/cursor/+page.markdoc rename to src/routes/docs/tooling/ai/agentic-coding/cursor/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/opencode/+page.markdoc b/src/routes/docs/tooling/ai/agentic-coding/opencode/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/opencode/+page.markdoc rename to src/routes/docs/tooling/ai/agentic-coding/opencode/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/vscode/+page.markdoc b/src/routes/docs/tooling/ai/agentic-coding/vscode/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/vscode/+page.markdoc rename to src/routes/docs/tooling/ai/agentic-coding/vscode/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/windsurf/+page.markdoc b/src/routes/docs/tooling/ai/agentic-coding/windsurf/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/windsurf/+page.markdoc rename to src/routes/docs/tooling/ai/agentic-coding/windsurf/+page.markdoc diff --git a/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc b/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc index cef7217eb07..641dad336fd 100644 --- a/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc +++ b/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc @@ -42,7 +42,7 @@ Users should understand when they are interacting with AI-generated content or A # AI-assisted development {% #ai-assisted-development %} -When using AI development tools like [Cursor, VS Code, or Claude Code](/docs/tooling/ai/ai-dev-tools) to build with Appwrite, keep the following in mind. +When using AI development tools like [Cursor, VS Code, or Claude Code](/docs/tooling/ai/agentic-coding/cursor) to build with Appwrite, keep the following in mind. - **Review generated code** before committing. AI-generated code may contain security vulnerabilities, incorrect API usage, or outdated patterns. - **Keep API keys out of prompts** when chatting with AI assistants. Avoid pasting secrets, credentials, or sensitive configuration into AI chat interfaces. diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/bolt/+page.markdoc b/src/routes/docs/tooling/ai/vibe-coding/bolt/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/bolt/+page.markdoc rename to src/routes/docs/tooling/ai/vibe-coding/bolt/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/claude-desktop/+page.markdoc b/src/routes/docs/tooling/ai/vibe-coding/claude-desktop/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/claude-desktop/+page.markdoc rename to src/routes/docs/tooling/ai/vibe-coding/claude-desktop/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/emergent/+page.markdoc b/src/routes/docs/tooling/ai/vibe-coding/emergent/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/emergent/+page.markdoc rename to src/routes/docs/tooling/ai/vibe-coding/emergent/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/lovable/+page.markdoc b/src/routes/docs/tooling/ai/vibe-coding/lovable/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/lovable/+page.markdoc rename to src/routes/docs/tooling/ai/vibe-coding/lovable/+page.markdoc diff --git a/src/routes/docs/tooling/ai/ai-dev-tools/zenflow/+page.markdoc b/src/routes/docs/tooling/ai/vibe-coding/zenflow/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/ai-dev-tools/zenflow/+page.markdoc rename to src/routes/docs/tooling/ai/vibe-coding/zenflow/+page.markdoc diff --git a/src/routes/docs/tooling/mcp/+layout.svelte b/src/routes/docs/tooling/mcp/+layout.svelte deleted file mode 100644 index cbba3af773b..00000000000 --- a/src/routes/docs/tooling/mcp/+layout.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - diff --git a/src/routes/docs/tooling/mcp/+page.markdoc b/src/routes/docs/tooling/mcp/+page.markdoc deleted file mode 100644 index e477e69188b..00000000000 --- a/src/routes/docs/tooling/mcp/+page.markdoc +++ /dev/null @@ -1,41 +0,0 @@ ---- -layout: article -title: Model Context Protocol -description: Enable LLMs and code-generation tools to interact with your Appwrite project ---- - -Appwrite offers [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers that allow LLMs to directly interact with Appwrite's API and docs. Using MCP servers, you can use applications such as Claude Desktop, Cursor, Windsurf Editor, etc. to operate on your Appwrite project as well as gain context about the latest updates to Appwrite's SDKs, APIs, and CLI. - -# What is MCP? - -The Model Context Protocol (MCP) is an open standard that enables Large Language Models (LLMs) and AI code-generation tools to interact with APIs and documentation in a structured manner. MCP servers provide a bridge between LLMs and external services, allowing them to perform actions such as querying databases, managing users, and accessing files. - -The key benefits of using MCP servers include: - -- **Enhanced capabilities**: LLMs can perform complex tasks by interacting with APIs, going beyond simple text generation. -- **Improved context**: By accessing up-to-date documentation and API definitions, LLMs can provide more accurate and relevant responses. -- **Seamless integration**: MCP servers can be easily integrated with popular AI tools and code editors, enhancing their functionality. - -# Available MCP servers - -Appwrite currently offers the following MCP servers: - -{% cards %} - -{% cards_item href="/docs/tooling/mcp/api" title="MCP server for Appwrite API" icon="icon-globe-alt"%} -{% /cards_item %} - -{% cards_item href="/docs/tooling/mcp/docs" title="MCP server for Appwrite docs" icon="icon-document-text" %} -{% /cards_item %} - -{% /cards %} - -## Why use Appwrite's MCP servers? - -Some **popular use cases** for Appwrite's MCP servers include: - -- **Code generation**: Automatically generate code snippets or entire files based on user input and context. -- **Documentation lookup**: Quickly find relevant documentation for specific API endpoints or SDK features. -- **Project management**: Create, update, or delete resources in your Appwrite project using natural language commands. -- **Debugging assistance**: Get help with debugging issues by providing context about your project and recent changes. -- **Learning and exploration**: Explore Appwrite's features and capabilities through interactive conversations with LLMs. \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/antigravity/+page.markdoc b/src/routes/docs/tooling/mcp/antigravity/+page.markdoc deleted file mode 100644 index 0727f12d31f..00000000000 --- a/src/routes/docs/tooling/mcp/antigravity/+page.markdoc +++ /dev/null @@ -1,116 +0,0 @@ ---- -layout: article -title: Appwrite MCP and Google Antigravity -description: Learn how to add the Appwrite MCP servers to Agent Manager in Google Antigravity to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to Agent Manager in Google Antigravity to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **pre-requisites** installed on your system: - -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -To add the Appwrite MCP server, open Antigravity and go to the drop-down (...) menu in the Agent window . From there, navigate to Manage MCP Servers in the MCP Store, and then click View raw config in the main panel to add your custom MCP server. - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -Update the `mcp_config.json` file to include the API server: - -```json -{ - "mcpServers": { - "appwrite-api": { - "command": "uvx", - "args": [ - "mcp-server-appwrite", - "--users" - ], - "env": { - "APPWRITE_PROJECT_ID": "your-project-id", - "APPWRITE_API_KEY": "your-api-key", - "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" - } - } - } -} -``` - -**Configuration:** -- Replace `your-project-id` with your actual Appwrite project ID -- Replace `your-api-key` with your Appwrite API key -- Replace `` with your Appwrite Cloud region (e.g., `nyc`, `fra`) - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -Update the `mcp_config.json` file to include the docs server: - -```json -{ - "mcpServers": { - "appwrite-docs": { - "command": "npx", - "args": [ - "mcp-remote", - "https://mcp-for-docs.appwrite.io" - ] - } - } -} -``` - -{% /tabsitem %} -{% /tabs %} - -Head back to the Managed MCP Server page and click refresh. - -{% /section %} - -{% section #step-2 step=2 title="Test the integration" %} - -Open **Agent Manager** in Antigravity to test your MCP integrations. You can try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![Search for portfolio site in Appwrite project](/images/docs/mcp/antigravity/agent-chat.avif) - -{% /section %} \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/api/+page.markdoc b/src/routes/docs/tooling/mcp/api/+page.markdoc deleted file mode 100644 index dd28306d9ca..00000000000 --- a/src/routes/docs/tooling/mcp/api/+page.markdoc +++ /dev/null @@ -1,118 +0,0 @@ ---- -layout: article -title: MCP server for Appwrite API -description: Enable LLMs and code-generation tools to interact with the Appwrite API ---- - -The MCP server for Appwrite API allows LLMs and code-generation tools to interact with the Appwrite platform and perform various operations on your Appwrite resources, such as creating users, managing databases, and more, using natural language commands. - -Here are some of the key benefits of using the MCP server: - -- **Direct API interaction**: Enables LLMs to perform actions directly on your Appwrite project -- **Real-time data access**: Allows LLMs to fetch and manipulate live data from your Appwrite instance -- **Simplified workflows**: Facilitates complex operations through simple natural language prompts -- **Customizable tools**: Offers a range of tools for different Appwrite services, which can be enabled as needed - -# Pre-requisites {% #pre-requisites %} - -## Appwrite API key - -Before launching the MCP server, you must [set up an Appwrite project](https://cloud.appwrite.io) and create an **API key** with the necessary scopes enabled. - -{% only_light %} -![Appwrite API key](/images/docs/mcp/appwrite/appwrite-api-secret.avif) -{% /only_light %} -{% only_dark %} -![Appwrite API key](/images/docs/mcp/appwrite/dark/appwrite-api-secret.avif) -{% /only_dark %} - -Ensure you save the **API key** along with the **project ID**, **region** and **endpoint URL** from the Settings page of your project as you'll need them later. - -## Install uv - -Install [uv](https://docs.astral.sh/uv/getting-started/installation/) on your system with: - -{% tabs %} -{% tabsitem #uv-linux-macos title="Linux and MacOS" %} - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - -{% /tabsitem %} - -{% tabsitem #uv-windows title="Windows" %} - -```powershell -powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" -``` - -{% /tabsitem %} -{% /tabs %} - -You can verify the installation by running the following command in your terminal: - -```bash -uv -``` - -# Installation {% #installation %} - -{% partial file="mcp-add-ides-tools.md" /%} - -## Command-line arguments - -Database tools are enabled by default. In addition you can pass arguments to `uvx mcp-server-appwrite [args]` to enable other MCP tools for various Appwrite APIs. - -| Argument | Description | -| --- | --- | -| `--tablesdb` | Enables the TablesDB API | -| `--users` | Enables the Users API | -| `--teams` | Enables the Teams API | -| `--storage` | Enables the Storage API | -| `--functions` | Enables the Functions API | -| `--messaging` | Enables the Messaging API | -| `--locale` | Enables the Locale API | -| `--avatars` | Enables the Avatars API | -| `--databases` | Enables the legacy Databases API | -| `--all` | Enables all Appwrite APIs | - -{% info title="Enable only necessary MCP tools" %} -When an MCP tool is enabled, the tool's definition is passed to the LLM, using up tokens from the model's available context window. As a result, the effective context window is reduced. Some IDEs may return errors if too many tools are enabled for the same reason. - -The default Appwrite MCP server ships with only the Databases tools (our most commonly used API) enabled to stay within these limits. Additional tools can be enabled using the flags above. -{% /info %} - -# Usage {% #usage %} - -Once configured, your AI assistant will have access to your Appwrite project. You can ask questions like: - -## Example 1: List users - -Run the following prompt in your preferred code editor/LLM after enabling the MCP server: - -``` -List users in my Appwrite project -``` - -![List users in Appwrite project](/images/docs/mcp/claude-desktop/claude-list-users.avif) - -## Example 2: Search a site - -Run the following prompt in your preferred code editor/LLM after enabling the MCP server: - -``` -Get the details of my portfolio site from Appwrite -``` - -![Search for portfolio site in Appwrite project](/images/docs/mcp/vscode/copilot-chat.avif) - -## Example 3: Create a user - -Run the following prompt in your preferred code editor/LLM after enabling the MCP server: - -``` -Add a user john.doe@example.com to the Appwrite project -``` - -![Create user in Appwrite project](/images/docs/mcp/cursor/cursor-create-user.avif) diff --git a/src/routes/docs/tooling/mcp/claude-code/+page.markdoc b/src/routes/docs/tooling/mcp/claude-code/+page.markdoc deleted file mode 100644 index c8b240b180c..00000000000 --- a/src/routes/docs/tooling/mcp/claude-code/+page.markdoc +++ /dev/null @@ -1,100 +0,0 @@ ---- -layout: article -title: Appwrite MCP and Claude Code -description: Learn how to add the Appwrite MCP servers to Claude= Code to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to Claude Code to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **pre-requisites** installed on your system: - -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -Run the following commands in your terminal to add the MCP servers: - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -```bash -claude mcp add-json appwrite-api '{"command":"uvx","args":["mcp-server-appwrite","--users"],"env":{"APPWRITE_PROJECT_ID": "your-project-id", "APPWRITE_API_KEY": "your-api-key", "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1"}}' -``` - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -```bash -claude mcp add appwrite-docs https://mcp-for-docs.appwrite.io -t http -``` - -{% /tabsitem %} -{% /tabs %} - -{% info title="Enable other API MCP tools" %} - -To enable additional API tools, learn more about [command-line arguments](/docs/tooling/mcp/api#command-line-arguments). - -{% /info %} - -{% /section %} - -{% section #step-2 step=2 title="Verify MCP tools" %} - -Run the following command in your terminal (where Claude Code is running). - -```bash -/mcp -``` - -You should see the added MCP servers listed there. - -![Verify MCP tools](/images/docs/mcp/claude-code/verify-mcp-tools.avif) - -{% /section %} - -{% section #step-3 step=3 title="Test the integration" %} - -Try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![Implement file uploads with Appwrite Storage](/images/docs/mcp/claude-code/implement-file-uploads.avif) - -{% /section %} \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/claude-desktop/+page.markdoc b/src/routes/docs/tooling/mcp/claude-desktop/+page.markdoc deleted file mode 100644 index a753be4147d..00000000000 --- a/src/routes/docs/tooling/mcp/claude-desktop/+page.markdoc +++ /dev/null @@ -1,144 +0,0 @@ ---- -layout: article -title: Appwrite MCP and Claude Desktop -description: Learn how to add the Appwrite MCP servers to Claude Desktop to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to Claude Desktop to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **pre-requisites** installed on your system: - -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -In the Claude Desktop app, open the app's **Settings** page (press `CTRL + ,` on Windows or `CMD + ,` on MacOS) and head to the **Developer** tab. - -![Claude Settings](/images/docs/mcp/claude-desktop/claude-settings.avif) - -Clicking on the **Edit Config** button will take you to the `claude_desktop_config.json` file. In case the file is missing, please visit the [Model Context Protocol](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server) docs. - -Choose which MCP server you want to configure: - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -Add the API server to your configuration: - -```json -{ - "mcpServers": { - "appwrite-api": { - "command": "uvx", - "args": [ - "mcp-server-appwrite", - "--users" - ], - "env": { - "APPWRITE_PROJECT_ID": "your-project-id", - "APPWRITE_API_KEY": "your-api-key", - "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" - } - } - } -} -``` - -**Configuration:** -- Replace `your-project-id` with your actual Appwrite project ID -- Replace `your-api-key` with your Appwrite API key -- Replace `` with your Appwrite Cloud region (e.g., `nyc`, `fra`) - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -Add the docs server to your configuration: - -```json -{ - "mcpServers": { - "appwrite-docs": { - "command": "npx", - "args": [ - "mcp-remote", - "https://mcp-for-docs.appwrite.io" - ] - } - } -} -``` - -**Why do we use the `mcp-remote` package?** - -Unlike other IDEs, Claude Desktop only supports local (stdio) MCP servers and not remote servers. The `mcp-remote` package acts as a proxy to connect to the docs MCP server. - -{% /tabsitem %} -{% /tabs %} - -{% info title="Enable other API MCP tools" %} - -To enable additional API tools, learn more about [command-line arguments](/docs/tooling/mcp/api#command-line-arguments). - -{% /info %} - -{% /section %} - -{% section #step-2 step=2 title="Verify MCP tools" %} - -Restart the Claude Desktop app, click on the MCP tools button (at the bottom right section of the prompt input) and click on it to view available Appwrite MCP tools. - -![Appwrite MCP tools](/images/docs/mcp/claude-desktop/claude-mcp-tools.avif) - -{% info title="uvx ENOENT error" %} - -In case you see a `uvx ENOENT` error, ensure that you either add `uvx` to the `PATH` environment variable on your system or use the full path to your `uvx` installation in the config file. - -{% /info %} - -{% /section %} - -{% section #step-3 step=3 title="Test the integration" %} - -Try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![List users in Appwrite project](/images/docs/mcp/claude-desktop/claude-list-users.avif) - -{% /section %} \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/cursor/+page.markdoc b/src/routes/docs/tooling/mcp/cursor/+page.markdoc deleted file mode 100644 index d59ff215a67..00000000000 --- a/src/routes/docs/tooling/mcp/cursor/+page.markdoc +++ /dev/null @@ -1,150 +0,0 @@ ---- -layout: article -title: Appwrite MCP and Cursor -description: Learn how to add the Appwrite MCP servers to Cursor to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to Cursor to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **pre-requisites** installed on your system: - -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -Open the **Cursor Settings** page, head to the **MCP** tab, and click on the **Add new global MCP server** button. This will open an `mcp.json` file in your editor. - -Choose which MCP server you want to configure: - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -Update the `mcp.json` file to include the API server: - -```json -{ - "mcpServers": { - "appwrite-api": { - "command": "uvx", - "args": [ - "mcp-server-appwrite", - "--users" - ], - "env": { - "APPWRITE_API_KEY": "your-api-key", - "APPWRITE_PROJECT_ID": "your-project-id", - "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" - } - } - } -} -``` - -**Configuration:** -- Replace `your-project-id` with your actual Appwrite project ID -- Replace `your-api-key` with your Appwrite API key -- Replace `` with your Appwrite Cloud region (e.g., `nyc`, `fra`) - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -Update the `mcp.json` file to include the docs server: - -```json -{ - "mcpServers": { - "appwrite-docs": { - "command": "npx", - "args": [ - "mcp-remote", - "https://mcp-for-docs.appwrite.io" - ] - } - } -} -``` - -{% /tabsitem %} -{% /tabs %} - -You can also **directly add the MCP servers to Cursor** using the following links: - -{% only_light %} -{% cards %} - -{% cards_item href="https://apwr.dev/api-mcp-cursor?ref=docs" title="API server" image="/images/docs/mcp/logos/cursor-ai.svg" %} -{% /cards_item %} - -{% cards_item href="https://apwr.dev/docs-mcp-cursor?ref=docs" title="Docs server" image="/images/docs/mcp/logos/cursor-ai.svg" %} -{% /cards_item %} - -{% /cards %} -{% /only_light %} - -{% only_dark %} -{% cards %} - -{% cards_item href="https://apwr.dev/api-mcp-cursor?ref=docs" title="API server" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} -{% /cards_item %} - -{% cards_item href="https://apwr.dev/docs-mcp-cursor?ref=docs" title="Docs server" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} -{% /cards_item %} - -{% /cards %} -{% /only_dark %} - -Once you save the details, Cursor will connect with the MCP server(s) and load all available tools. You may need to restart Cursor if it is unable to start the MCP server. - -{% info title="Enable other API MCP tools" %} - -To enable additional API tools, learn more about [command-line arguments](/docs/tooling/mcp/api#command-line-arguments). - -{% /info %} - -{% /section %} - -{% section #step-2 step=2 title="Test the integration" %} - -Open Cursor Agent and test your MCP integrations. You can try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![Create user in Appwrite project](/images/docs/mcp/cursor/cursor-create-user.avif) - -{% /section %} diff --git a/src/routes/docs/tooling/mcp/docs/+page.markdoc b/src/routes/docs/tooling/mcp/docs/+page.markdoc deleted file mode 100644 index 3f8bf006fb3..00000000000 --- a/src/routes/docs/tooling/mcp/docs/+page.markdoc +++ /dev/null @@ -1,72 +0,0 @@ ---- -layout: article -title: MCP server for Appwrite docs -description: Enable LLMs and code-generation tools to interact with the Appwrite docs ---- - -The MCP server for Appwrite documentation allows LLMs and code-generation tools to interact with comprehensive Appwrite documentation, enabling intelligent code generation for Appwrite's APIs and SDKs, troubleshooting assistance, and implementation guidance using natural language commands. - -Here are some of the key benefits of using the MCP server: - -- **Complete documentation access**: Provides AI assistants with access to all Appwrite documentation -- **Real-time context**: Ensures AI responses are based on the latest documentation -- **Intelligent search**: Enables semantic search across documentation content -- **Code examples**: Includes access to code samples and implementation guides -- **Best practices**: Shares recommended patterns and practices from official documentation - -# Pre-requisites {% #pre-requisites %} - -Install [Node.js](https://nodejs.org/en/download) and npm on your system. You can verify the installation by running the following commands in your terminal: - -```bash -node -v -npm -v -``` - -# Installation {% #installation %} - -{% partial file="mcp-add-ides-tools.md" /%} - -# Usage {% #usage %} - -Once configured, your AI assistant will have access to Appwrite documentation context. You can ask questions like: - -## Example 1: Code generation - -Run the following prompt in your preferred code editor/LLM after enabling the MCP server: - -``` -Show me how to set up real-time subscriptions that trigger on creation of a user -``` - -![Code generation example](/images/docs/mcp/mcp-for-docs/code-generation.avif) - -## Example 2: Troubleshooting - -Run the following prompt in your preferred code editor/LLM after enabling the MCP server: - -``` -I'm getting a 401 error when trying to delete a user. What could be wrong? -``` - -![Troubleshooting example](/images/docs/mcp/mcp-for-docs/troubleshooting.avif) - -## Example 3: Best practices - -Run the following prompt in your preferred code editor/LLM after enabling the MCP server: - -``` -What are some of the best security practices for Appwrite Auth in a web app with SSR? -``` - -![Best practices example](/images/docs/mcp/mcp-for-docs/best-practices.avif) - -## Example 4: API reference - -Run the following prompt in your preferred code editor/LLM after enabling the MCP server: - -``` -I want an example of how I can list all users in a Python app -``` - -![API reference example](/images/docs/mcp/mcp-for-docs/api-reference.avif) \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/opencode/+page.markdoc b/src/routes/docs/tooling/mcp/opencode/+page.markdoc deleted file mode 100644 index 4f4af41af1d..00000000000 --- a/src/routes/docs/tooling/mcp/opencode/+page.markdoc +++ /dev/null @@ -1,118 +0,0 @@ ---- -layout: article -title: Appwrite MCP and OpenCode -description: Learn how to add the Appwrite MCP servers to OpenCode to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to OpenCode to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **pre-requisites** installed on your system: - -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -Use the following configuration in your `opencode.json` file to use the Appwrite MCP servers. - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "appwrite": { - "type": "local", - "command": [ - "uvx", - "mcp-server-appwrite", - "--sites" - ], - "enabled": true, - "environment": { - "APPWRITE_PROJECT_ID": "your-project-id", - "APPWRITE_API_KEY": "your-api-key", - "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" - } - } - } -} -``` - -**Configuration:** -- Replace `your-project-id` with your actual Appwrite project ID -- Replace `your-api-key` with your Appwrite API key -- Replace `` with your Appwrite Cloud region (e.g., `nyc`, `fra`) - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "appwrite-docs": { - "type": "remote", - "enabled": true, - "url": "https://mcp-for-docs.appwrite.io" - } - } -} -``` - -{% /tabsitem %} -{% /tabs %} - -{% info title="Enable other API MCP tools" %} - -To enable additional API tools, learn more about [command-line arguments](/docs/tooling/mcp/api#command-line-arguments). - -{% /info %} - -{% /section %} - -{% section #step-2 step=2 title="Test the integration" %} - -Try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![Implement OAuth authentication in Appwrite](/images/docs/mcp/opencode/oauth-question.avif) - -{% /section %} \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/vscode/+page.markdoc b/src/routes/docs/tooling/mcp/vscode/+page.markdoc deleted file mode 100644 index 36c7cd7ad5b..00000000000 --- a/src/routes/docs/tooling/mcp/vscode/+page.markdoc +++ /dev/null @@ -1,121 +0,0 @@ ---- -layout: article -title: Appwrite MCP and VS Code -description: Learn how to add the Appwrite MCP servers to GitHub Copilot Chat in VS Code to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to GitHub Copilot Chat in VS Code to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **pre-requisites** installed on your system: - -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -In VS Code, open the **Command Palette** (press `CTRL + Shift + P` on Windows or `CMD + Shift + P` on MacOS) and run the `MCP: Open User Configuration` command. - -Choose which MCP servers you want to configure: - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -Update the `mcp.json` file to include the API server: - -```json -{ - "servers": { - "appwrite-api": { - "command": "uvx", - "args": [ - "mcp-server-appwrite", - "--sites" - ], - "env": { - "APPWRITE_PROJECT_ID": "your-project-id", - "APPWRITE_API_KEY": "your-api-key", - "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" - } - } - } -} -``` - -**Configuration:** -- Replace `your-project-id` with your actual Appwrite project ID -- Replace `your-api-key` with your Appwrite API key -- Replace `` with your Appwrite Cloud region (e.g., `nyc`, `fra`) - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -Update the `mcp.json` file to include the docs server: - -```json -{ - "servers": { - "appwrite-docs": { - "url": "https://mcp-for-docs.appwrite.io", - "type": "http" - } - } -} -``` - -{% /tabsitem %} -{% /tabs %} - -Once you save the configuration, Copilot Chat will connect with the MCP server(s) and load all available tools. - -{% info title="Enable other API MCP tools" %} - -To enable additional API tools, learn more about [command-line arguments](/docs/tooling/mcp/api#command-line-arguments). - -{% /info %} - -{% /section %} - -{% section #step-2 step=2 title="Test the integration" %} - -Open **Copilot Chat** in VS Code and switch to **Agent Mode** to test your MCP integrations. You can try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![Search for portfolio site in Appwrite project](/images/docs/mcp/vscode/copilot-chat.avif) - -{% /section %} \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/windsurf/+page.markdoc b/src/routes/docs/tooling/mcp/windsurf/+page.markdoc deleted file mode 100644 index c0d132a66ba..00000000000 --- a/src/routes/docs/tooling/mcp/windsurf/+page.markdoc +++ /dev/null @@ -1,123 +0,0 @@ ---- -layout: article -title: Appwrite MCP and Windsurf Editor -description: Learn how to add the Appwrite MCP servers to Windsurf Editor to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to Windsurf Editor to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **pre-requisites** installed on your system: - -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -Open the **Windsurf Settings** page, head to the **Cascade** tab, find the **Model Context Protocol (MCP) Servers** section, and click on the **View raw config** button. - -![Add MCP server](/images/docs/mcp/windsurf/windsurf-add-mcp-server.avif) - -Choose which MCP server you want to configure: - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -Update the `mcp_config.json` file to include the API server: - -```json -{ - "mcpServers": { - "appwrite-api": { - "command": "uvx", - "args": [ - "mcp-server-appwrite", - "--databases", - "--users" - ], - "env": { - "APPWRITE_PROJECT_ID": "your-project-id", - "APPWRITE_API_KEY": "your-api-key", - "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" - } - } - } -} -``` - -**Configuration:** -- Replace `your-project-id` with your actual Appwrite project ID -- Replace `your-api-key` with your Appwrite API key -- Replace `` with your Appwrite Cloud region (e.g., `nyc`, `fra`) - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -Update the `mcp_config.json` file to include the docs server: - -```json -{ - "mcpServers": { - "appwrite-docs": { - "serverUrl": "https://mcp-for-docs.appwrite.io" - } - } -} -``` - -{% /tabsitem %} -{% /tabs %} - -Once you save the details, head back to the MCP Servers section in the Windsurf Settings and click on the **Refresh** button. - -{% info title="Enable other API MCP tools" %} - -To enable additional API tools, learn more about [command-line arguments](/docs/tooling/mcp/api#command-line-arguments). - -{% /info %} - -{% /section %} - -{% section #step-2 step=2 title="Test the integration" %} - -Open Cascade chat in the Windsurf Editor and test your MCP integrations. You can try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![List data from an Appwrite database](/images/docs/mcp/windsurf/windsurf-cascade-chat.avif) - -{% /section %} \ No newline at end of file diff --git a/src/routes/docs/tooling/mcp/zenflow/+page.markdoc b/src/routes/docs/tooling/mcp/zenflow/+page.markdoc deleted file mode 100644 index d437a8b7dcd..00000000000 --- a/src/routes/docs/tooling/mcp/zenflow/+page.markdoc +++ /dev/null @@ -1,120 +0,0 @@ ---- -layout: article -title: Appwrite MCP and Zenflow -description: Learn how to add the Appwrite MCP servers to agents in Zenflow to interact with both the Appwrite API and documentation. ---- - -Learn how you can add the Appwrite MCP servers to agents in Zenflow to interact with both the Appwrite API and documentation. - -Before you begin, ensure you have the following **prerequisites** installed on your system: -{% tabs %} -{% tabsitem #api-server-prerequisites title="API server" %} - -[uv](https://docs.astral.sh/uv/getting-started/installation/) must be installed on your system. - -{% /tabsitem %} - -{% tabsitem #docs-server-prerequisites title="Docs server" %} - -[Node.js](https://nodejs.org/en/download) and npm must be installed on your system. - -{% /tabsitem %} -{% /tabs %} - -{% section #step-1 step=1 title="Add MCP servers" %} - -To add the Appwrite MCP server, open Zenflow and go to the **Settings** > **MCP servers**. From there, select your agent you want to configure MCP for, and then add your custom MCP server. - -{% tabs %} -{% tabsitem #api-only title="API server" %} - -```json -{ - "mcpServers": { - "appwrite-api": { - "command": "uvx", - "args": [ - "mcp-server-appwrite", - "--sites" - ], - "env": { - "APPWRITE_PROJECT_ID": "your-project-id", - "APPWRITE_API_KEY": "your-api-key", - "APPWRITE_ENDPOINT": "https://.cloud.appwrite.io/v1" - } - } - } -} -``` - -**Configuration:** -- Replace `your-project-id` with your actual Appwrite project ID -- Replace `your-api-key` with your Appwrite API key -- Replace `` with your Appwrite Cloud region (e.g., `nyc`, `fra`) - -{% /tabsitem %} - -{% tabsitem #docs-only title="Docs server" %} - -Update the -```json -{ - "mcpServers": {} -} -``` -to include the docs server: - -```json -{ - "mcpServers": { - "appwrite-docs": { - "url": "https://mcp-for-docs.appwrite.io", - "type": "http" - } - } -} -``` - -{% /tabsitem %} -{% /tabs %} - -Click **Save**. Once you save the configuration, Zenflow will connect with the MCP server(s) and load all available tools. - -{% /section %} - -{% section #step-2 step=2 title="Test the integration" %} - -Open **Zenflow Chat** of your existing task to test your MCP integrations. If you don't have an existing task, you can create one by clicking **New Task**, selecting a task type, and writing a task description. Click **Create and Run**. -If you are new to Zenflow, learn more about [how to set up Zenflow](https://docs.zencoder.ai/user-guides/guides/set-up-your-zenflow#step-6:-create-your-first-task). - -You can try out the following example prompts based on the MCP server you have configured: - -{% tabs %} -{% tabsitem #test-api title="API server" %} - -**Example prompts:** -- `Create a new user in my Appwrite project` -- `List all databases in my project` -- `Show me the collections in my database` -- `Create a new document in my collection` -- `Delete a specific user by ID` - -{% /tabsitem %} - -{% tabsitem #test-docs title="Docs server" %} - -**Example prompts:** -- `How do I set up real-time subscriptions in Appwrite?` -- `Show me how to authenticate users with OAuth` -- `What are the best practices for database queries?` -- `How do I implement file uploads with Appwrite Storage?` -- `Show me an example of using Appwrite Functions` - -{% /tabsitem %} - -{% /tabs %} - -![Search for portfolio site in Appwrite project](/images/docs/mcp/zenflow/zenflow-chat.avif) - - -{% /section %} \ No newline at end of file From 68f6516a52fd84cff2003477ecf0dfd98b9611df Mon Sep 17 00:00:00 2001 From: Aishwari Pahwa Date: Mon, 18 May 2026 12:43:30 +0530 Subject: [PATCH 05/49] new blogs --- .optimize-cache.json | 2 + .../+page.markdoc | 101 ++++++++++++++ .../+page.markdoc | 130 ++++++++++++++++++ .../cover.avif | Bin 0 -> 20600 bytes .../cover.avif | Bin 0 -> 14691 bytes 5 files changed, 233 insertions(+) create mode 100644 src/routes/blog/post/7-vibe-coding-trends-every-developer-should-know-in-2026/+page.markdoc create mode 100644 src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc create mode 100644 static/images/blog/7-vibe-coding-trends-every-developer-should-know-in-2026/cover.avif create mode 100644 static/images/blog/the-ultimate-guide-to-vibe-coding-in-2026/cover.avif diff --git a/.optimize-cache.json b/.optimize-cache.json index e3a535ba908..3a53b5c582e 100644 --- a/.optimize-cache.json +++ b/.optimize-cache.json @@ -129,6 +129,7 @@ "static/images/blog/7-prompting-mistakes-you-need-to-stop-making-right-now/cover.png": "eea096934c726917cfa1686a6c23198bf70c4215191101302047aef440404dde", "static/images/blog/7-steps-gdpr-startups/cover.png": "9894264a71940716de2ec5e09711834791ddd1c510dee9e5bf42a864343c5a2d", "static/images/blog/7-things-claude-can-do-that-will-blow-your-mind/cover.png": "7bb98ebda77a4cc582b67e5d72db18ee32b94278d17a805ee9a94f1f459db741", + "static/images/blog/7-vibe-coding-trends-every-developer-should-know-in-2026/cover.png": "5831de0b500eb362127df7a21ea63cd27045ac7e9bb5df6a60332f5802d3d70b", "static/images/blog/a-recap-of-init/init1.png": "446305a616f6ce3ec77b01e5f5ab5dbf0e68f32268a3d5aab7249fc055ff61cd", "static/images/blog/a-recap-of-init/init10.png": "8f39e8d643d0630ced6f5c96a8bc9dbac72a7759e2e7caf7a09699f03566b184", "static/images/blog/a-recap-of-init/init11.png": "77ee790eecb99b592884a2c55933011587b965a95bccafd9fbd6cbeec6f81416", @@ -1135,6 +1136,7 @@ "static/images/blog/the-shift-from-SaaS-to-Vertical-AI-what-startup-founders-need-to-know/cover-image.png": "e4030cb8b735baa8f4f6eec9d0d32233011759b160882015738cda2e79da14d6", "static/images/blog/the-subtle-art-of-hackathon ideation/cover.png": "a4007fb895ed8cb284e2409897282a784b803c199b91d58e90a2dd69f367ba33", "static/images/blog/the-top-3-claude-features-you-are-probably-not-using/cover.png": "429930ed746a8d47a42358c99c1eba6ee0d33531d4270ac4f24594438f65f503", + "static/images/blog/the-ultimate-guide-to-vibe-coding-in-2026/cover.png": "7ef566133387995330f9b6d5df85d880279da4104aee30624b2f243ebff433cb", "static/images/blog/the-underrated-value-of-great-sdk-design/cover.png": "9440e4f5a69b01d20796926ef3543dbba61fd1ba66e114713cf4ee503bc1d4b9", "static/images/blog/threads-cover.png": "fa44d6cd70000ac7a62d3b9446b171f8e9fe1b27f157cb6ba2f98c1f8c043526", "static/images/blog/three-important-steps-you-need-to-complete-with-appwrite/3-important-steps.png": "5a3ad677a3aff5f27f0ceb8b751ba830fb6e11ca5edca126df34777f378b15a4", diff --git a/src/routes/blog/post/7-vibe-coding-trends-every-developer-should-know-in-2026/+page.markdoc b/src/routes/blog/post/7-vibe-coding-trends-every-developer-should-know-in-2026/+page.markdoc new file mode 100644 index 00000000000..f520cb5a38f --- /dev/null +++ b/src/routes/blog/post/7-vibe-coding-trends-every-developer-should-know-in-2026/+page.markdoc @@ -0,0 +1,101 @@ +--- +layout: post +title: 7 vibe coding trends every developer should know in 2026 +description: Learn the biggest vibe coding trends shaping 2026, from AI coding agents and context engineering to backend infrastructure built for faster software development. +date: 2026-05-18 +cover: /images/blog/7-vibe-coding-trends-every-developer-should-know-in-2026/cover.avif +timeToRead: 5 +author: aishwari +category: ai +featured: false +unlisted: true +faqs: + - question: What is vibe coding in 2026? + answer: Vibe coding is the practice of building software by directing AI assistants and agents instead of typing every line yourself. In 2026, it covers everything from generating blocks of code to running long-horizon agents that build entire features autonomously while you focus on architecture and review. + - question: Is vibe coding only useful for junior developers? + answer: The opposite is closer to true. Senior developers tend to get the most leverage from vibe coding because they can scope work clearly, write tight specs, and review AI output critically. The skills that matter most are judgment, architecture, and taste, not typing speed. + - question: Does vibe coding actually work for backend development? + answer: Yes, when the backend is built for it. Platforms with clean SDKs, predictable APIs, and strong defaults let AI assistants generate working code on the first try. Backends with hidden complexity force the AI to guess, which is where bugs and outages come from. + - question: How do I keep AI-generated code safe and reliable in production? + answer: Three habits cover most of it. Have the AI write tests first and run them in a loop. Use a backend with built-in auth and permissions so you are not generating security logic from scratch. Review every diff that touches data access, payments, or user state before it ships. +--- +Vibe coding has moved from a developer side experiment to the default way production code gets built. AI assistants have grown into full agents that ship features, refactor codebases, and run long horizon tasks while the developer focuses on architecture, review, and judgment. + +The pace of change is the story. The workflows that defined this space a year ago already look dated, and the teams pulling ahead in 2026 are the ones tracking where it is going next. + +Here are seven vibe coding trends shaping how developers build in 2026. Each one is already running in production, at startups and at scale. + +# 1. Spec driven development replaces prompt and pray + +The first wave of vibe coding was casual. You typed "add a login form" and hoped for the best. The 2026 wave is structured. + +Developers now write tight specs before any code is generated. Not full PRDs, but clear contracts that describe the input, the output, the edge cases, and the constraints. The model reads the spec, plans the work, and executes against it. The spec becomes the source of truth, and the code becomes a renderable artifact you can regenerate when needs change. + +This shift matters because it changes what skills compound. The senior engineers pulling ahead are the ones who can write a clean spec in five minutes. The ones falling behind are still typing every line. + +# 2. Agents that run for hours, not seconds + +Early AI coding tools answered one prompt at a time. The 2026 generation runs autonomously for hours. You hand off a feature, walk away, and come back to a working pull request with passing tests and a changelog entry. + +Long horizon agents handle migrations, framework upgrades, dependency bumps, and full feature builds. They commit their own work, run their own tests, and ask for help only when they hit something genuinely ambiguous. + +The implication for developers is real. Your job is no longer to type code. It is to scope work, review output, and own the parts of the system that require taste and judgment. + +# 3. Context engineering becomes a core skill + +Models are smart. They are not psychic. The difference between a vibe coding session that ships and one that loops forever is almost always context. + +Context engineering is the discipline of giving the model the right information at the right time. That means structured rules files, scoped documentation, retrieval over your codebase, memory of past decisions, and clear signals about what matters and what does not. Teams that invest here ship faster and hit fewer dead ends. + +In 2026, every serious engineering org has a context strategy. The ones that do not are still wondering why their AI keeps reinventing the same broken pattern. + +# 4. AI native backends and the rise of fluent infrastructure + +Frontend code is easy to generate. Backend code is where vibe coding has historically broken down. Auth, databases, storage, real time, and security each have their own rules, and getting them wrong is expensive. + +The fix is infrastructure that AI can read, understand, and use without ambiguity. Backends with clean SDKs, predictable APIs, strong defaults, and documentation that doubles as model context. When the platform is fluent, the AI is fluent. When the platform is a maze of opaque services, the AI guesses, and you pay for those guesses in production. + +This is why developer platforms are being rebuilt with AI as a first class user, not an afterthought. + +# 5. Test first vibe coding + +Generated code without tests is a liability. Generated code with tests is leverage. + +The 2026 default is to have the model write tests first, then write the implementation, then run the tests in a loop until they pass. The model becomes its own quality gate. The human reviews the tests, because that is where intent lives. + +This pattern is already standard in the teams shipping the fastest. It catches regressions before they hit main, and it leaves a record of what the model thought it was building, which is gold during review. + +# 6. Multi model orchestration + +No single model is best at everything. The good vibe coders in 2026 route work between models the way a tech lead routes work between engineers. + +A fast cheap model handles boilerplate. A frontier model handles architecture and tricky logic. A specialized model handles security review. A local model handles autocomplete. The orchestration layer is the new IDE, and developers who understand it ship more for less. + +You no longer pick a model. You pick a workflow, and the workflow picks the model. + +# 7. The solo developer with a real product + +The quietest and most important trend of 2026 is the rise of the one person product team. Vibe coding plus modern backends plus AI native deployment means a single developer can now ship something a small team used to need a year to build. + +The implication is not that teams disappear. It is that the bar for shipping rises, the surface of what one person can do expands, and the next wave of meaningful products will come from places nobody is watching. Side projects, weekend builds, indie launches, and small teams that look like big ones. + +This is the exciting part. Software is becoming abundant. The bottleneck is moving from typing speed to taste. + +# What this means for the rest of 2026 + +The pattern across all seven trends is the same. The model handles the mechanics. The developer owns the judgment. The platforms that scale are the ones built so both can do their best work without fighting each other. + +Vibe coding is not a phase. It is a permanent shift in how code gets written, and the developers who lean in are already pulling ahead. + +# Start vibe coding with Appwrite + +Vibe coding only works when your backend works with you. Appwrite gives you auth, databases, storage, functions, real time, and messaging in one platform that AI assistants speak fluently. Clean SDKs, predictable APIs, and documentation that models can read without getting lost. + +Sign up for [Appwrite Cloud](https://cloud.appwrite.io/) or spin up a self hosted instance in minutes, and let your next idea ship at the speed your AI can actually keep up with. + +# Resources + +* [Appwrite documentation](https://appwrite.io/docs) +* [Appwrite quick start guides](https://appwrite.io/docs/quick-starts) +* [Appwrite on GitHub](https://github.com/appwrite/appwrite) +* [Join the Appwrite Discord](https://appwrite.io/discord) \ No newline at end of file diff --git a/src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc b/src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc new file mode 100644 index 00000000000..ff4016953bc --- /dev/null +++ b/src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc @@ -0,0 +1,130 @@ +--- +layout: post +title: The ultimate guide to vibe coding in 2026 +description: Learn how vibe coding works in 2026, from AI coding workflows to the backend stack powering modern apps. +date: 2026-05-18 +cover: /images/blog/the-ultimate-guide-to-vibe-coding-in-2026/cover.avif +timeToRead: 5 +author: aishwari +category: ai +featured: false +unlisted: true +faqs: + - question: Is vibe coding the same as no-code? + answer: No. No-code platforms abstract the codebase away entirely. Vibe coding keeps the code at the center. You use AI to generate it, but you still own, read, and deploy real source files. + - question: Do I still need to know how to code in 2026? + answer: Yes. The best vibe coders are also strong engineers. You do not need to write every line by hand, but you need to read code fluently, understand architecture, and recognize when the AI is wrong. Without that foundation, you are not directing the AI. You are following it. + - question: Which languages and frameworks work best for vibe coding? + answer: Anything with a large training footprint performs well. JavaScript, TypeScript, Python, Swift, Kotlin, Go, and Rust all have strong AI support. Mainstream frameworks like React, Next.js, SvelteKit, Flutter, and FastAPI generate cleanly, and they pair well with backend platforms that expose typed SDKs. + - question: Is Vibe-coded software safe to ship to production? + answer: It can be, with discipline. Reviews, automated tests, security scans, and a backend that handles auth and data correctly are non-negotiable. The AI does not own the outage. You do. +--- +Two years ago, vibe coding was a half joking phrase from a tweet. Today it is how millions of developers ship software. + +You describe what you want. The AI writes the code. You steer the result. You ship. The unit of work has moved from the line to the feature, and the bottleneck has moved from typing speed to taste. + +If you have been on the fence about going all in, this guide will catch you up. We will cover what vibe coding actually means in 2026, the workflow that holds up in production, the stack worth your time, and the traps that still catch people. By the end, you will have a clear playbook for building real apps without writing every line by hand. + +# What is vibe coding? + +Vibe coding is a development practice where you communicate intent in natural language and let an AI model translate that intent into working code. The phrase was popularized by Andrej Karpathy in early 2025, and it has since evolved from a casual experiment into a full discipline. + +The "vibe" part is the key. You are not dictating syntax. You are describing the feel, the behavior, the user experience, the data model. The AI handles the rest. You review, refine, and redirect. + +A few defining traits: + +* You write in plain English, or any spoken language. +* The AI generates, edits, and refactors entire files. +* You read code more than you write it. +* You judge output by behavior, not by lines. +* You ship faster, and you fix faster. + +This is not the same as no code. No code platforms abstract the codebase away entirely. Vibe coding keeps real source files at the center. You still own the repo, the deployment, and the architecture. You just stop hand typing the boilerplate. + +# Why vibe coding matters in 2026 + +The shift was not just better models. It was the surrounding ecosystem catching up. In 2026 we have: + +* Coding agents that hold the full repo in context. +* Backend platforms that expose entire stacks through a single SDK call. +* Generated UI components that match your design system out of the box. +* Evaluation and test agents that catch regressions before you do. +* Open source workflows that make AI generated commits as auditable as human ones. + +Put together, the friction between idea and production has collapsed. Solo founders ship apps that used to need a team of five. Enterprise teams cut feature timelines from weeks to hours. What changes is not the craft of engineering. It is the leverage one engineer can apply. + +# The vibe coding workflow that actually works + +Most failed vibe coding attempts share the same mistake: treating the AI like a magic wand. The teams winning in 2026 follow a tighter loop. + +## 1. Start with a clear spec + +The vaguer your prompt, the messier your output. Before you open your editor, write a short brief. What does the feature do? What does success look like? What edge cases matter? Five lines of clear intent beat a one line wish every time. + +## 2. Pick the right stack upfront + +You can vibe code anything, but you cannot vibe debug everything. Choose a stack with strong AI training data behind it, clear documentation, and an open ecosystem. The more your AI has seen, the cleaner the output, and the less time you spend correcting hallucinated APIs. + +## 3. Generate in small slices + +Ask for one component, one route, one function at a time. Smaller slices mean tighter feedback and easier review. Big prompts produce big messes, and big messes are slow to untangle. + +## 4. Always review the diff + +Read every change. The AI is a fast junior developer with infinite patience. It will hallucinate APIs, invent libraries, and miss security holes. Your review is the safety net, and it is the difference between vibe coding and gambling. + +## 5. Test as you go + +Pair every feature with a generated test. The AI can write the test before, after, or alongside the implementation. The key is to never skip this step, especially when the code feels obviously correct. + +## 6. Refactor when patterns emerge + +After three similar prompts, you have a pattern. Pause. Refactor. Otherwise your codebase becomes a quilt of half matched approaches that the AI keeps reinforcing because that is what it sees in your repo. + +# The 2026 vibe coding stack + +You do not need every tool on the market. You need one good choice in each of these layers: + +* **An AI coding agent** that lives in your editor and holds repo context. +* **A model router** so you can switch providers without rewriting prompts. +* **A backend platform** that handles auth, database, storage, and functions without infrastructure work. +* **A component library** with AI friendly primitives. +* **An evaluation tool** to catch regressions across prompt changes. +* **A version control workflow** that treats AI commits with the same scrutiny as human ones. + +The backend layer is where most vibe coded projects stall. You can generate a beautiful frontend in minutes, but if you spend the rest of the day wiring up databases, authentication, file storage, and serverless functions, the speed advantage disappears. A good backend platform keeps your AI focused on what is unique about your product, not on plumbing. + +# Common vibe coding pitfalls + +A few traps still catch even experienced teams. + +**Prompting like a search query.** "Make a login page" is not a prompt. It is a wish. Add context, constraints, and examples. + +**Skipping the read.** If you accept code you have not read, you are not vibe coding. You are gambling with your codebase. + +**Letting context drift.** Long sessions cause models to forget early decisions. Restart the conversation, summarize the state, and continue. + +**Ignoring security.** Generated code often skips input validation, rate limiting, and auth checks. Always ask for them explicitly, and verify them in review. + +**Choosing tools that do not integrate.** If your backend, your AI agent, and your deployment platform do not speak to each other, you will spend more time gluing than building. + +# Where Appwrite fits in your vibe coding workflow + +The fastest vibe coders in 2026 share one thing in common. They do not waste cycles on infrastructure. They keep their AI focused on what makes their product unique, and they let a backend platform handle the rest. + +That is exactly what Appwrite is built for. Auth, databases, storage, functions, messaging, and realtime, all available through a single SDK that your AI assistant already understands. Prompts like "add Google login," "store the user's avatar," or "send a welcome email" translate directly into Appwrite SDK calls, with no infrastructure setup required. + +Appwrite is open source, self hostable, and built for developers who want to ship fast without surrendering control. Whether you are building a side project on a weekend or scaling a serious product, the platform stays out of your way and lets your AI assistant do its best work. + +# Start vibe coding with Appwrite + +Vibe coding rewards developers who pair fast AI with solid infrastructure. Appwrite gives you both. A backend that AI assistants speak fluently, and a platform that scales with whatever you build next. + +Sign up for [Appwrite Cloud](https://appwrite.io/) or spin up a self hosted instance in minutes, and ship your next idea before the coffee gets cold. + +# Resources + +* [Appwrite documentation](https://appwrite.io/docs) +* [Appwrite quick start guides](https://appwrite.io/docs/quick-starts) +* [Appwrite on GitHub](https://github.com/appwrite/appwrite) +* [Join the Appwrite Discord](https://appwrite.io/discord) \ No newline at end of file diff --git a/static/images/blog/7-vibe-coding-trends-every-developer-should-know-in-2026/cover.avif b/static/images/blog/7-vibe-coding-trends-every-developer-should-know-in-2026/cover.avif new file mode 100644 index 0000000000000000000000000000000000000000..47b586ea5451d22013e3c9b21fd54b088433c0cf GIT binary patch literal 20600 zcmYIsQeeINSV} z0RB6fEv)T~{=3{v8IRBCUP4T}P;D0LY1}^SG|B+oBZ8>f1P0ap#2yz-( z7};|=c{n+n*f??i>#?x5FmUwXG_bZe|L=X_TG-qC7yPsSn;tj-2p9keBp3)Z^q+ub z;b`)|G5y-}F$HP>gIExG02KJA zcDGaK@*6R?4((#J6@FcEzTsB1u3S>AJ}tov9D5SKKhe z)007gyLUyj?zc;Iq}5FRVqvHnL~EQPCG_MQ_4+qhEloS8lLFxBh5}-*{@VzY9IMoY zTW++w)6N(0jRwS}oT9=bFE3$PAoGnOm#ukv8&#gob37NJ9fwFwm-JoKPO)Onf^@n8 zdyJYr^5JTs7Jmbgxadn_htahr4XS>dxK=;QRb9mtuRX9s10Y*T&9n^%JxfSHw+;rrP4bbQ^}CJPz_7x^JNNUNl{7!^ev*HffvC7^Dh?4U2vBd)B;gfloYf zY{%=cJ~+m%_3s19(R)f!j&~ok8Q?7eOTc{*e{Umr)vbt7A-lS@tlS#Ovr+JIDv}g> zf8DW-EI!&X)%{fH3tTgKjre2q7(8){a<}g^TE_G4o#yJztR0ocL5fh+r@vx z@+*Q665TPPd5dR~GRC`V9yR1bMee>1w`g3`6W=U3=;o1zN3i8fz+O$QCPiS)YeMWM zUeby3F^Sv(sUp*$F`M0oyFu{O6IKLeEDjHlJCw1>ulgHVwM!?oIr0_iyabo(g={O# zV&OAO9l-K9D?;ZQeun`39&#owuq1JeJw0j-o?kC$hY4WBVg=L6BcZ+Q%I+9JA|j%G zB+mM_*!XAO1r()v(A=5ya{uygfvHGH*(fmzgs{QVmhpN+Y3qTA`Zy~8a7RKb?g)xzcsr7Cd?KYtH5CM zj5^D>*PBQ=+f`#Js%{ur=5w!`Q!QEnI#&#cC`h(8CvnZ~EVGt|Yr8%-R+{4IM#-~! ztvuHw=fQTCe-m>onnLEu1V00%r4&!9OOin!?jwm<=uFA3i!aHR&v~~#y4-JyLMF7w9{ep!o3}9UO9%0D@dcT&BGuhl-yL&y?D%R6X-!g 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 KYjc#XrRwaGPo0ba literal 0 HcmV?d00001 diff --git a/static/images/blog/the-ultimate-guide-to-vibe-coding-in-2026/cover.avif b/static/images/blog/the-ultimate-guide-to-vibe-coding-in-2026/cover.avif new file mode 100644 index 0000000000000000000000000000000000000000..c8b9c20e29dca0b60182a7a2d5825849df87e5c6 GIT binary patch literal 14691 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 Y*CLd3L|S>X82mJZt({Y9OE0hg4-o4tRR910 literal 0 HcmV?d00001 From ace71bfa882d9d2e43bdaa59ab356b58645554c1 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Mon, 18 May 2026 13:51:25 +0530 Subject: [PATCH 06/49] Fix stale links in mcp partial and responsible-ai --- src/partials/mcp-add-ides-tools.md | 8 ++++---- src/routes/docs/tooling/ai/responsible-ai/+page.markdoc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/partials/mcp-add-ides-tools.md b/src/partials/mcp-add-ides-tools.md index 28d2f5c6067..f09c6b3c7d2 100644 --- a/src/partials/mcp-add-ides-tools.md +++ b/src/partials/mcp-add-ides-tools.md @@ -9,13 +9,13 @@ You can add the MCP server to various AI tools and code editors: {% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/mcp/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/mcp/zenflow" title="Zenflow" image="/images/docs/mcp/logos/zenflow.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/zenflow" title="Zenflow" image="/images/docs/mcp/logos/zenflow.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} @@ -39,13 +39,13 @@ You can add the MCP server to various AI tools and code editors: {% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/mcp/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/dark/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/mcp/zenflow" title="Zenflow" image="/images/docs/mcp/logos/dark/zenflow.svg" %} +{% cards_item href="/docs/tooling/ai/vibe-coding/zenflow" title="Zenflow" image="/images/docs/mcp/logos/dark/zenflow.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} diff --git a/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc b/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc index 641dad336fd..e8aeec7c1d6 100644 --- a/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc +++ b/src/routes/docs/tooling/ai/responsible-ai/+page.markdoc @@ -42,7 +42,7 @@ Users should understand when they are interacting with AI-generated content or A # AI-assisted development {% #ai-assisted-development %} -When using AI development tools like [Cursor, VS Code, or Claude Code](/docs/tooling/ai/agentic-coding/cursor) to build with Appwrite, keep the following in mind. +When using AI development tools like [Cursor, VS Code, or Claude Code](/docs/tooling/ai/) to build with Appwrite, keep the following in mind. - **Review generated code** before committing. AI-generated code may contain security vulnerabilities, incorrect API usage, or outdated patterns. - **Keep API keys out of prompts** when chatting with AI assistants. Avoid pasting secrets, credentials, or sensitive configuration into AI chat interfaces. From 2c3ec221af715f50daa5545734899ad99931a60e Mon Sep 17 00:00:00 2001 From: Aishwari Pahwa Date: Mon, 18 May 2026 16:33:48 +0530 Subject: [PATCH 07/49] Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc b/src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc index ff4016953bc..fec84931719 100644 --- a/src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc +++ b/src/routes/blog/post/the-ultimate-guide-to-vibe-coding-in-2026/+page.markdoc @@ -120,7 +120,7 @@ Appwrite is open source, self hostable, and built for developers who want to shi Vibe coding rewards developers who pair fast AI with solid infrastructure. Appwrite gives you both. A backend that AI assistants speak fluently, and a platform that scales with whatever you build next. -Sign up for [Appwrite Cloud](https://appwrite.io/) or spin up a self hosted instance in minutes, and ship your next idea before the coffee gets cold. +Sign up for [Appwrite Cloud](https://cloud.appwrite.io/) or spin up a self hosted instance in minutes, and ship your next idea before the coffee gets cold. # Resources From 8fcd080dd651c9cc146eece2e7d32100f2d731fe Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Tue, 19 May 2026 19:55:33 +0530 Subject: [PATCH 08/49] 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 09/49] 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 10/49] 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 11/49] 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 12/49] 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 13/49] 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 14/49] 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 15/49] 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 16/49] 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 17/49] 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 ff308450cbe09b88538d4aa3c59af68b4c1d19de Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Wed, 20 May 2026 11:50:13 +0530 Subject: [PATCH 18/49] Rename agentic-coding to agents --- src/partials/mcp-add-ides-tools.md | 24 ++++++++--------- src/redirects.json | 26 +++++++++---------- src/routes/(marketing)/(components)/ai.svelte | 12 ++++----- .../(marketing)/(components)/platforms.svelte | 2 +- src/routes/docs/+page.svelte | 2 +- src/routes/docs/tooling/ai/+layout.svelte | 12 ++++----- src/routes/docs/tooling/ai/+page.markdoc | 24 ++++++++--------- .../antigravity/+page.markdoc | 0 .../claude-code/+page.markdoc | 0 .../codex/+page.markdoc | 0 .../cursor/+page.markdoc | 0 .../opencode/+page.markdoc | 0 .../vscode/+page.markdoc | 0 .../windsurf/+page.markdoc | 0 14 files changed, 51 insertions(+), 51 deletions(-) rename src/routes/docs/tooling/ai/{agentic-coding => agents}/antigravity/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{agentic-coding => agents}/claude-code/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{agentic-coding => agents}/codex/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{agentic-coding => agents}/cursor/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{agentic-coding => agents}/opencode/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{agentic-coding => agents}/vscode/+page.markdoc (100%) rename src/routes/docs/tooling/ai/{agentic-coding => agents}/windsurf/+page.markdoc (100%) diff --git a/src/partials/mcp-add-ides-tools.md b/src/partials/mcp-add-ides-tools.md index f09c6b3c7d2..20078c6f169 100644 --- a/src/partials/mcp-add-ides-tools.md +++ b/src/partials/mcp-add-ides-tools.md @@ -3,13 +3,13 @@ You can add the MCP server to various AI tools and code editors: {% only_light %} {% cards %} -{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agents/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/claude.svg" %} @@ -18,13 +18,13 @@ You can add the MCP server to various AI tools and code editors: {% cards_item href="/docs/tooling/ai/vibe-coding/zenflow" title="Zenflow" image="/images/docs/mcp/logos/zenflow.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agents/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} {% /cards_item %} {% /cards %} @@ -33,13 +33,13 @@ You can add the MCP server to various AI tools and code editors: {% only_dark %} {% cards %} -{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agents/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} {% /cards_item %} {% cards_item href="/docs/tooling/ai/vibe-coding/claude-desktop" title="Claude Desktop" image="/images/docs/mcp/logos/dark/claude.svg" %} @@ -48,13 +48,13 @@ You can add the MCP server to various AI tools and code editors: {% cards_item href="/docs/tooling/ai/vibe-coding/zenflow" title="Zenflow" image="/images/docs/mcp/logos/dark/zenflow.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agents/antigravity" title="Google Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} {% /cards_item %} {% /cards %} diff --git a/src/redirects.json b/src/redirects.json index d4438f1f1e3..17340a23b84 100644 --- a/src/redirects.json +++ b/src/redirects.json @@ -782,27 +782,27 @@ }, { "link": "/docs/tooling/mcp/cursor", - "redirect": "/docs/tooling/ai/agentic-coding/cursor" + "redirect": "/docs/tooling/ai/agents/cursor" }, { "link": "/docs/tooling/mcp/vscode", - "redirect": "/docs/tooling/ai/agentic-coding/vscode" + "redirect": "/docs/tooling/ai/agents/vscode" }, { "link": "/docs/tooling/mcp/windsurf", - "redirect": "/docs/tooling/ai/agentic-coding/windsurf" + "redirect": "/docs/tooling/ai/agents/windsurf" }, { "link": "/docs/tooling/mcp/opencode", - "redirect": "/docs/tooling/ai/agentic-coding/opencode" + "redirect": "/docs/tooling/ai/agents/opencode" }, { "link": "/docs/tooling/mcp/antigravity", - "redirect": "/docs/tooling/ai/agentic-coding/antigravity" + "redirect": "/docs/tooling/ai/agents/antigravity" }, { "link": "/docs/tooling/mcp/claude-code", - "redirect": "/docs/tooling/ai/agentic-coding/claude-code" + "redirect": "/docs/tooling/ai/agents/claude-code" }, { "link": "/docs/tooling/mcp/claude-desktop", @@ -838,31 +838,31 @@ }, { "link": "/docs/tooling/ai/ai-dev-tools/claude-code", - "redirect": "/docs/tooling/ai/agentic-coding/claude-code" + "redirect": "/docs/tooling/ai/agents/claude-code" }, { "link": "/docs/tooling/ai/ai-dev-tools/codex", - "redirect": "/docs/tooling/ai/agentic-coding/codex" + "redirect": "/docs/tooling/ai/agents/codex" }, { "link": "/docs/tooling/ai/ai-dev-tools/cursor", - "redirect": "/docs/tooling/ai/agentic-coding/cursor" + "redirect": "/docs/tooling/ai/agents/cursor" }, { "link": "/docs/tooling/ai/ai-dev-tools/vscode", - "redirect": "/docs/tooling/ai/agentic-coding/vscode" + "redirect": "/docs/tooling/ai/agents/vscode" }, { "link": "/docs/tooling/ai/ai-dev-tools/opencode", - "redirect": "/docs/tooling/ai/agentic-coding/opencode" + "redirect": "/docs/tooling/ai/agents/opencode" }, { "link": "/docs/tooling/ai/ai-dev-tools/antigravity", - "redirect": "/docs/tooling/ai/agentic-coding/antigravity" + "redirect": "/docs/tooling/ai/agents/antigravity" }, { "link": "/docs/tooling/ai/ai-dev-tools/windsurf", - "redirect": "/docs/tooling/ai/agentic-coding/windsurf" + "redirect": "/docs/tooling/ai/agents/windsurf" }, { "link": "/docs/tooling/ai/ai-dev-tools/claude-desktop", diff --git a/src/routes/(marketing)/(components)/ai.svelte b/src/routes/(marketing)/(components)/ai.svelte index ac4a91b8eea..b4cac6212ab 100644 --- a/src/routes/(marketing)/(components)/ai.svelte +++ b/src/routes/(marketing)/(components)/ai.svelte @@ -19,7 +19,7 @@ const tools: AiStripTool[] = [ { name: 'Claude Code', - href: '/docs/tooling/ai/agentic-coding/claude-code', + href: '/docs/tooling/ai/agents/claude-code', dark: '/images/docs/mcp/logos/dark/claude.svg', light: '/images/docs/mcp/logos/claude.svg', primary: '#D97659', @@ -27,7 +27,7 @@ }, { name: 'Codex', - href: '/docs/tooling/ai/agentic-coding/codex', + href: '/docs/tooling/ai/agents/codex', dark: '/images/docs/mcp/logos/dark/openai.svg', light: '/images/docs/mcp/logos/openai.svg', primary: '#10A37F', @@ -35,7 +35,7 @@ }, { name: 'Cursor', - href: '/docs/tooling/ai/agentic-coding/cursor', + href: '/docs/tooling/ai/agents/cursor', dark: '/images/docs/mcp/logos/dark/cursor-ai.svg', light: '/images/docs/mcp/logos/cursor-ai.svg', primary: '#141414', @@ -43,7 +43,7 @@ }, { name: 'VS Code', - href: '/docs/tooling/ai/agentic-coding/vscode', + href: '/docs/tooling/ai/agents/vscode', dark: '/images/docs/mcp/logos/dark/vscode.svg', light: '/images/docs/mcp/logos/vscode.svg', primary: '#0078D7', @@ -51,7 +51,7 @@ }, { name: 'OpenCode', - href: '/docs/tooling/ai/agentic-coding/opencode', + href: '/docs/tooling/ai/agents/opencode', dark: '/images/docs/mcp/logos/dark/opencode.svg', light: '/images/docs/mcp/logos/opencode.svg', primary: '#FFFFFF', @@ -59,7 +59,7 @@ }, { name: 'Google Antigravity', - href: '/docs/tooling/ai/agentic-coding/antigravity', + href: '/docs/tooling/ai/agents/antigravity', dark: '/images/docs/mcp/logos/dark/google-antigravity.svg', light: '/images/docs/mcp/logos/google-antigravity.svg', primary: '#4285F4', diff --git a/src/routes/(marketing)/(components)/platforms.svelte b/src/routes/(marketing)/(components)/platforms.svelte index c820514ba12..a4e49e496dc 100644 --- a/src/routes/(marketing)/(components)/platforms.svelte +++ b/src/routes/(marketing)/(components)/platforms.svelte @@ -37,7 +37,7 @@ name: 'Codex', dark: '/images/docs/mcp/logos/dark/openai.svg', light: '/images/docs/mcp/logos/openai.svg', - href: '/docs/tooling/ai/agentic-coding/codex', + href: '/docs/tooling/ai/agents/codex', primary: '#10A37F', secondary: '#064E3B' }, diff --git a/src/routes/docs/+page.svelte b/src/routes/docs/+page.svelte index 0597c09e277..29767b8e501 100644 --- a/src/routes/docs/+page.svelte +++ b/src/routes/docs/+page.svelte @@ -38,7 +38,7 @@ event: 'docs-ai-ide_claude-code-click' }, { - href: '/docs/tooling/ai/agentic-coding/codex', + href: '/docs/tooling/ai/agents/codex', title: 'Codex', logoDark: '/images/docs/mcp/logos/dark/openai.svg', logoLight: '/images/docs/mcp/logos/openai.svg', diff --git a/src/routes/docs/tooling/ai/+layout.svelte b/src/routes/docs/tooling/ai/+layout.svelte index 57d8b0eeee6..df9507fae5a 100644 --- a/src/routes/docs/tooling/ai/+layout.svelte +++ b/src/routes/docs/tooling/ai/+layout.svelte @@ -51,27 +51,27 @@ items: [ { label: 'Claude Code', - href: '/docs/tooling/ai/agentic-coding/claude-code' + href: '/docs/tooling/ai/agents/claude-code' }, { label: 'Codex', - href: '/docs/tooling/ai/agentic-coding/codex' + href: '/docs/tooling/ai/agents/codex' }, { label: 'Cursor', - href: '/docs/tooling/ai/agentic-coding/cursor' + href: '/docs/tooling/ai/agents/cursor' }, { label: 'VS Code', - href: '/docs/tooling/ai/agentic-coding/vscode' + href: '/docs/tooling/ai/agents/vscode' }, { label: 'OpenCode', - href: '/docs/tooling/ai/agentic-coding/opencode' + href: '/docs/tooling/ai/agents/opencode' }, { label: 'Google Antigravity', - href: '/docs/tooling/ai/agentic-coding/antigravity' + href: '/docs/tooling/ai/agents/antigravity' } ] }, diff --git a/src/routes/docs/tooling/ai/+page.markdoc b/src/routes/docs/tooling/ai/+page.markdoc index 0376a9b180b..64bd235aba1 100644 --- a/src/routes/docs/tooling/ai/+page.markdoc +++ b/src/routes/docs/tooling/ai/+page.markdoc @@ -13,22 +13,22 @@ AI-powered IDEs and code editors provide intelligent code completion and context {% only_light %} {% cards %} -{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agents/claude-code" title="Claude Code" image="/images/docs/mcp/logos/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/codex" title="Codex" image="/images/docs/mcp/logos/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/cursor" title="Cursor" image="/images/docs/mcp/logos/cursor-ai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/vscode" title="VS Code" image="/images/docs/mcp/logos/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/opencode" title="OpenCode" image="/images/docs/mcp/logos/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agents/antigravity" title="Antigravity" image="/images/docs/mcp/logos/google-antigravity.svg" %} {% /cards_item %} {% /cards %} @@ -37,22 +37,22 @@ AI-powered IDEs and code editors provide intelligent code completion and context {% only_dark %} {% cards %} -{% cards_item href="/docs/tooling/ai/agentic-coding/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} +{% cards_item href="/docs/tooling/ai/agents/claude-code" title="Claude Code" image="/images/docs/mcp/logos/dark/claude.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/codex" title="Codex" image="/images/docs/mcp/logos/dark/openai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} +{% cards_item href="/docs/tooling/ai/agents/cursor" title="Cursor" image="/images/docs/mcp/logos/dark/cursor-ai.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/vscode" title="VS Code" image="/images/docs/mcp/logos/dark/vscode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} +{% cards_item href="/docs/tooling/ai/agents/opencode" title="OpenCode" image="/images/docs/mcp/logos/dark/opencode.svg" %} {% /cards_item %} -{% cards_item href="/docs/tooling/ai/agentic-coding/antigravity" title="Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} +{% cards_item href="/docs/tooling/ai/agents/antigravity" title="Antigravity" image="/images/docs/mcp/logos/dark/google-antigravity.svg" %} {% /cards_item %} {% /cards %} diff --git a/src/routes/docs/tooling/ai/agentic-coding/antigravity/+page.markdoc b/src/routes/docs/tooling/ai/agents/antigravity/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/agentic-coding/antigravity/+page.markdoc rename to src/routes/docs/tooling/ai/agents/antigravity/+page.markdoc diff --git a/src/routes/docs/tooling/ai/agentic-coding/claude-code/+page.markdoc b/src/routes/docs/tooling/ai/agents/claude-code/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/agentic-coding/claude-code/+page.markdoc rename to src/routes/docs/tooling/ai/agents/claude-code/+page.markdoc diff --git a/src/routes/docs/tooling/ai/agentic-coding/codex/+page.markdoc b/src/routes/docs/tooling/ai/agents/codex/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/agentic-coding/codex/+page.markdoc rename to src/routes/docs/tooling/ai/agents/codex/+page.markdoc diff --git a/src/routes/docs/tooling/ai/agentic-coding/cursor/+page.markdoc b/src/routes/docs/tooling/ai/agents/cursor/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/agentic-coding/cursor/+page.markdoc rename to src/routes/docs/tooling/ai/agents/cursor/+page.markdoc diff --git a/src/routes/docs/tooling/ai/agentic-coding/opencode/+page.markdoc b/src/routes/docs/tooling/ai/agents/opencode/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/agentic-coding/opencode/+page.markdoc rename to src/routes/docs/tooling/ai/agents/opencode/+page.markdoc diff --git a/src/routes/docs/tooling/ai/agentic-coding/vscode/+page.markdoc b/src/routes/docs/tooling/ai/agents/vscode/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/agentic-coding/vscode/+page.markdoc rename to src/routes/docs/tooling/ai/agents/vscode/+page.markdoc diff --git a/src/routes/docs/tooling/ai/agentic-coding/windsurf/+page.markdoc b/src/routes/docs/tooling/ai/agents/windsurf/+page.markdoc similarity index 100% rename from src/routes/docs/tooling/ai/agentic-coding/windsurf/+page.markdoc rename to src/routes/docs/tooling/ai/agents/windsurf/+page.markdoc From cef477ad8b7cb78a56b93ec91c3a693f82bed5cd Mon Sep 17 00:00:00 2001 From: Aditya Oberai Date: Wed, 20 May 2026 17:48:37 +0530 Subject: [PATCH 19/49] 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 20/49] 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); } }); ``` From 0b9f6e720b27953d752e19b5e23a0ee80948612a Mon Sep 17 00:00:00 2001 From: singhvibhanshu Date: Wed, 20 May 2026 19:23:48 +0530 Subject: [PATCH 21/49] fix(blog): update active category chip UI state on filter selection Signed-off-by: singhvibhanshu --- src/routes/blog/[[page]]/+page.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/blog/[[page]]/+page.svelte b/src/routes/blog/[[page]]/+page.svelte index 6e69295b562..d7e5d71e4b7 100644 --- a/src/routes/blog/[[page]]/+page.svelte +++ b/src/routes/blog/[[page]]/+page.svelte @@ -44,6 +44,10 @@ let selectedCategory = $state(page.url.searchParams.get('category') ?? 'Latest'); + $effect(() => { + selectedCategory = page.url.searchParams.get('category') ?? 'Latest'; + }); + const handleSearch = async () => { const searchQuery = query.toLowerCase(); From 0f2548db532c1d8ea0f98c853130e701b5d44bd3 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 22 May 2026 15:41:17 +0530 Subject: [PATCH 22/49] docs: move SDK version compatibility matrix to self-hosted installation The matrix is self-hosted specific, so it belongs in the self-hosted installation docs rather than the general SDKs page. --- .../self-hosting/installation/+page.markdoc | 164 +++++++++++++++++ src/routes/docs/sdks/+page.markdoc | 166 ------------------ 2 files changed, 164 insertions(+), 166 deletions(-) diff --git a/src/routes/docs/advanced/self-hosting/installation/+page.markdoc b/src/routes/docs/advanced/self-hosting/installation/+page.markdoc index 23c266e4154..80554cff616 100644 --- a/src/routes/docs/advanced/self-hosting/installation/+page.markdoc +++ b/src/routes/docs/advanced/self-hosting/installation/+page.markdoc @@ -154,6 +154,170 @@ After installation completes: On non-Linux hosts, the server might take a few minutes to start after installation completes. This is normal behavior. {% /info %} +## SDK version compatibility {% #sdk-version-compatibility %} + +The tables below map each released self-hosted Appwrite version to the SDK versions that were current at the time of that release. Use this to pin your SDK to a version known to work with your server. The latest stable self-hosted Appwrite release is 1.9.0. + +### Client SDKs {% #client-compatibility %} + +{% table %} +* Appwrite {% width=120 %} +* Web +* Flutter +* React Native +* Apple +* Android +--- +* 1.7.0 +* 18.0.0 +* 16.0.0 +* 0.9.0 +* 10.0.0 +* 8.0.0 +--- +* 1.7.1 to 1.7.3 +* 18.0.0 +* 16.0.0 +* 0.9.0 +* 10.0.0 +* 8.0.0 +--- +* 1.7.4 +* 18.1.1 +* 17.0.0 +* 0.10.0 +* 10.0.0 +* 8.0.0 +--- +* 1.8.0 +* 21.4.0 +* 20.3.0 +* 0.18.0 +* 13.3.0 +* 11.3.0 +--- +* 1.7.5 +* 18.1.1 +* 17.0.0 +* 0.10.0 +* 10.0.0 +* 8.0.0 +--- +* 1.8.1 +* 21.4.0 +* 20.3.0 +* 0.18.0 +* 13.3.0 +* 11.3.0 +--- +* 1.9.0 +* 24.2.0 +* 23.0.0 +* 0.27.1 +* 16.0.0 +* 14.1.0 +{% /table %} + +### Server SDKs {% #server-compatibility %} + +{% table %} +* Appwrite {% width=120 %} +* Node.js +* Python +* PHP +* Dart +* Ruby +* .NET +* Go +* Swift +* Kotlin +* CLI +--- +* 1.7.0 +* 17.0.0 +* 11.0.0 +* 15.0.0 +* 16.0.0 +* 16.0.0 +* 0.13.0 +* v0.7.0 +* 10.0.0 +* 9.0.0 +* 6.2.3 +--- +* 1.7.1 to 1.7.3 +* 17.0.0 +* 11.0.0 +* 15.0.0 +* 16.0.0 +* 16.0.0 +* 0.13.0 +* v0.7.0 +* 10.0.0 +* 9.0.0 +* 6.2.3 +--- +* 1.7.4 +* 17.1.0 +* 11.0.0 +* 15.0.0 +* 16.1.0 +* 16.0.0 +* 0.13.0 +* v0.7.0 +* 10.0.0 +* 9.0.0 +* 12.0.1 +--- +* 1.8.0 +* 20.2.1 +* 13.4.1 +* 17.5.0 +* 19.3.0 +* 19.3.0 +* 0.22.0 +* v0.13.1 +* 13.2.2 +* 12.3.0 +* 12.0.1 +--- +* 1.7.5 +* 17.1.0 +* 11.0.0 +* 15.0.0 +* 16.1.0 +* 16.0.0 +* 0.13.0 +* v0.7.0 +* 10.0.0 +* 9.0.0 +* 12.0.1 +--- +* 1.8.1 +* 20.3.0 +* 13.6.1 +* 18.0.1 +* 19.3.0 +* 19.3.0 +* 0.22.0 +* v0.14.0 +* 13.3.0 +* 12.3.0 +* 17.2.1 +--- +* 1.9.0 +* 22.0.0 +* 15.0.0 +* 20.0.0 +* 21.0.0 +* 21.0.0 +* 0.24.0 +* v2.0.0 +* 15.0.0 +* 14.0.0 +* 17.3.0 +{% /table %} + # Managing your installation {% #managing-installation %} ## Stop Appwrite {% #stop %} diff --git a/src/routes/docs/sdks/+page.markdoc b/src/routes/docs/sdks/+page.markdoc index 5ad3c160477..27b232c6881 100644 --- a/src/routes/docs/sdks/+page.markdoc +++ b/src/routes/docs/sdks/+page.markdoc @@ -122,172 +122,6 @@ Server libraries for integrating with Appwrite to build server side integrations If you would like to help us extend our platforms and SDKs stack, you are more than welcome to contact us or contribute to the [Appwrite SDK Generator](https://github.com/appwrite/sdk-generator) project GitHub repository and read our contribution guide. -# Version compatibility {% #version-compatibility %} - -The tables below map each released self-hosted Appwrite version to the SDK versions that were current at the time of that release. Use this to pin your SDK to a version known to work with your server. The latest stable self-hosted Appwrite release is `1.9.0`. - -If you are running Appwrite Cloud, use the latest published SDK for your platform. Cloud rolls forward ahead of self-hosted releases. - -## Client SDKs {% #client-compatibility %} - -{% table %} -* Appwrite {% width=120 %} -* Web -* Flutter -* React Native -* Apple -* Android ---- -* `1.7.0` -* `18.0.0` -* `16.0.0` -* `0.9.0` -* `10.0.0` -* `8.0.0` ---- -* `1.7.1` to `1.7.3` -* `18.0.0` -* `16.0.0` -* `0.9.0` -* `10.0.0` -* `8.0.0` ---- -* `1.7.4` -* `18.1.1` -* `17.0.0` -* `0.10.0` -* `10.0.0` -* `8.0.0` ---- -* `1.8.0` -* `21.4.0` -* `20.3.0` -* `0.18.0` -* `13.3.0` -* `11.3.0` ---- -* `1.7.5` -* `18.1.1` -* `17.0.0` -* `0.10.0` -* `10.0.0` -* `8.0.0` ---- -* `1.8.1` -* `21.4.0` -* `20.3.0` -* `0.18.0` -* `13.3.0` -* `11.3.0` ---- -* `1.9.0` -* `24.2.0` -* `23.0.0` -* `0.27.1` -* `16.0.0` -* `14.1.0` -{% /table %} - -## Server SDKs {% #server-compatibility %} - -{% table %} -* Appwrite {% width=120 %} -* Node.js -* Python -* PHP -* Dart -* Ruby -* .NET -* Go -* Swift -* Kotlin -* CLI ---- -* `1.7.0` -* `17.0.0` -* `11.0.0` -* `15.0.0` -* `16.0.0` -* `16.0.0` -* `0.13.0` -* `v0.7.0` -* `10.0.0` -* `9.0.0` -* `6.2.3` ---- -* `1.7.1` to `1.7.3` -* `17.0.0` -* `11.0.0` -* `15.0.0` -* `16.0.0` -* `16.0.0` -* `0.13.0` -* `v0.7.0` -* `10.0.0` -* `9.0.0` -* `6.2.3` ---- -* `1.7.4` -* `17.1.0` -* `11.0.0` -* `15.0.0` -* `16.1.0` -* `16.0.0` -* `0.13.0` -* `v0.7.0` -* `10.0.0` -* `9.0.0` -* `12.0.1` ---- -* `1.8.0` -* `20.2.1` -* `13.4.1` -* `17.5.0` -* `19.3.0` -* `19.3.0` -* `0.22.0` -* `v0.13.1` -* `13.2.2` -* `12.3.0` -* `12.0.1` ---- -* `1.7.5` -* `17.1.0` -* `11.0.0` -* `15.0.0` -* `16.1.0` -* `16.0.0` -* `0.13.0` -* `v0.7.0` -* `10.0.0` -* `9.0.0` -* `12.0.1` ---- -* `1.8.1` -* `20.3.0` -* `13.6.1` -* `18.0.1` -* `19.3.0` -* `19.3.0` -* `0.22.0` -* `v0.14.0` -* `13.3.0` -* `12.3.0` -* `17.2.1` ---- -* `1.9.0` -* `22.0.0` -* `15.0.0` -* `20.0.0` -* `21.0.0` -* `21.0.0` -* `0.24.0` -* `v2.0.0` -* `15.0.0` -* `14.0.0` -* `17.3.0` -{% /table %} - # Protocols {% #protocols %} We are always looking to add new SDKs to our platform. If the SDK you are looking for is still missing, labeled as beta or experimental, or you simply do not want to integrate with an SDK, you can always integrate with Appwrite directly using any standard HTTP, GraphQL, or WebSocket clients and the relevant Appwrite protocol. From ff97287df93b3237801d0e6ca550a563b8dabbc0 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 18:51:16 +0530 Subject: [PATCH 23/49] Update Presence API documentation to reflect changes in event emissions and method usage --- .../announcing-presence-api/+page.markdoc | 2 +- .../docs/apis/realtime/channels/+page.markdoc | 4 +- .../docs/apis/realtime/presence/+page.markdoc | 37 +++++++++---------- .../docs/products/auth/presence/+page.markdoc | 2 +- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 742f3dc8fc1..592cc8695d7 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`, `upsert`, `update`, and `delete` events for every record a subscriber has permission to read. +- **Dedicated Realtime channels** (`presences` and `presences.`) that emit `upsert` 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. diff --git a/src/routes/docs/apis/realtime/channels/+page.markdoc b/src/routes/docs/apis/realtime/channels/+page.markdoc index a7271295336..5399050ce92 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, upsert, update, or delete event on any [presence](/docs/apis/realtime/presence) the subscriber can read. +* Any upsert or delete event on any [presence](/docs/apis/realtime/presence) the subscriber can read. --- * `presences.` * `Channel.presence('')` -* Any create, upsert, update, or delete event on a given presence record. +* Any upsert 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 index 0ba5ab1629a..ba1725cf2b0 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -16,7 +16,7 @@ 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. +**It is live.** Every change to a presence fires an event on the `presences` and `presences.` [Realtime](/docs/apis/realtime) channels. Subscribers get `upsert` 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: @@ -29,7 +29,7 @@ A typical "online dot" loop looks like this: 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. +- **While connected.** Call `realtime.upsertPresence(...)` over an open Realtime connection and the record is automatically deleted when that connection closes. Best for "online while the tab is open" UIs where you do not want to manage a heartbeat yourself. # Upsert a presence {% #upsert-a-presence %} @@ -1462,7 +1462,7 @@ const realtime = new Realtime(client); const subscription = await realtime.subscribe(Channel.presences(), response => { 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')) { + } else if (response.events.includes('presences.*.upsert')) { console.log('Presence created or updated', response.payload); } }); @@ -1480,11 +1480,10 @@ 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')) { + if (response.events.contains('presences.*.delete')) { print('Presence expired or removed: ${response.payload}'); + } else if (response.events.contains('presences.*.upsert')) { + print('Presence created or updated: ${response.payload}'); } }); ``` @@ -1499,11 +1498,10 @@ let client = Client() 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) { + if (response.events?.contains("presences.*.delete") == true) { print("Presence expired or removed: \(String(describing: response.payload))") + } else if (response.events?.contains("presences.*.upsert") == true) { + print("Presence created or updated: \(String(describing: response.payload))") } } ``` @@ -1520,11 +1518,10 @@ val client = Client(context) 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")) { + if (it.events.contains("presences.*.delete")) { println("Presence expired or removed: ${it.payload}") + } else if (it.events.contains("presences.*.upsert")) { + println("Presence created or updated: ${it.payload}") } } ``` @@ -1532,11 +1529,11 @@ 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. +`update()` is a REST-only operation and does not emit a Realtime event. If you need subscribers to see a status or metadata change live, use `upsert()` instead. + 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 %} @@ -1548,14 +1545,14 @@ This gives you a clean signal for "user just came online", "user changed status" --- * `presences` * `Channel.presences()` -* Any create, upsert, update, or delete event on any presence the subscriber can read. +* Any upsert or delete event on any presence the subscriber can read. --- * `presences.` * `Channel.presence('')` -* Any create, upsert, update, or delete event on a specific presence record. +* Any upsert 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. +You can also append `.upsert()` 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 %} diff --git a/src/routes/docs/products/auth/presence/+page.markdoc b/src/routes/docs/products/auth/presence/+page.markdoc index 13f1f13c0ff..1a2c337b935 100644 --- a/src/routes/docs/products/auth/presence/+page.markdoc +++ b/src/routes/docs/products/auth/presence/+page.markdoc @@ -93,7 +93,7 @@ Most apps update presence on a few specific signals: ```client-web async function setStatus(status, metadata = {}) { - await presences.update({ + await presences.upsert({ presenceId, status, metadata From d3969326000e7462b0635fb8e23bbfd1b746630b Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 18:59:48 +0530 Subject: [PATCH 24/49] update date --- src/routes/blog/post/announcing-presence-api/+page.markdoc | 2 +- .../(entries)/{2026-05-20.markdoc => 2026-05-22.markdoc} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/routes/changelog/(entries)/{2026-05-20.markdoc => 2026-05-22.markdoc} (98%) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 592cc8695d7..d2188f7254b 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-20 +date: 2026-05-22 cover: /images/blog/announcing-presence-api/cover.png timeToRead: 5 author: aditya-oberai diff --git a/src/routes/changelog/(entries)/2026-05-20.markdoc b/src/routes/changelog/(entries)/2026-05-22.markdoc similarity index 98% rename from src/routes/changelog/(entries)/2026-05-20.markdoc rename to src/routes/changelog/(entries)/2026-05-22.markdoc index 81e9946576f..8e95deb2a7e 100644 --- a/src/routes/changelog/(entries)/2026-05-20.markdoc +++ b/src/routes/changelog/(entries)/2026-05-22.markdoc @@ -1,7 +1,7 @@ --- layout: changelog title: "Track who is online with the new Presence API" -date: 2026-05-20 +date: 2026-05-22 cover: /images/blog/announcing-presence-api/cover.avif --- From 61ea6bacefff83be759f25fdabc0cc272ffad541 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 19:04:11 +0530 Subject: [PATCH 25/49] Additional examples for listing and subscribing to user presences --- .../docs/products/auth/presence/+page.markdoc | 80 ++++++++++++++++--- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/src/routes/docs/products/auth/presence/+page.markdoc b/src/routes/docs/products/auth/presence/+page.markdoc index 1a2c337b935..5781dfd7c08 100644 --- a/src/routes/docs/products/auth/presence/+page.markdoc +++ b/src/routes/docs/products/auth/presence/+page.markdoc @@ -108,7 +108,77 @@ There is no fixed heartbeat interval enforced by the server, so pick whichever c # 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. +List the presences the current user can read to paint the initial "online now" view, a list of viewers on a page, or a typing dot in a chat. The list call honors the same [permissions](/docs/advanced/platform/permissions) you set on each record, so each client only sees the statuses it is allowed to render. + +{% 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 result = await presences.list(); + +const onlineUsers = new Map( + result.presences.map(presence => [presence.userId, presence]) +); +``` + +```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(); + +final onlineUsers = { + for (final presence in result.presences) presence.userId: presence +}; +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let result = try await presences.list() + +var onlineUsers: [String: Any] = [:] +for presence in result.presences { + onlineUsers[presence.userId] = presence +} +``` + +```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 result = presences.list() + +val onlineUsers = result.presences + .associateBy { it.userId } + .toMutableMap() +``` +{% /multicode %} + +Then subscribe to the global `presences` channel to keep that snapshot live. Apply the same patch to the same `onlineUsers` map on every event, add or replace on upsert, remove on delete. {% multicode %} ```client-web @@ -120,8 +190,6 @@ const client = new Client() 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')) { @@ -143,8 +211,6 @@ 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')) { @@ -164,8 +230,6 @@ let client = Client() 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 } @@ -189,8 +253,6 @@ val client = Client(context) 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 From 804e334d9fffd7cc8f28b9496ef60842b4b78200 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 19:31:09 +0530 Subject: [PATCH 26/49] include update events --- .../announcing-presence-api/+page.markdoc | 2 +- .../changelog/(entries)/2026-05-22.markdoc | 2 +- .../docs/apis/realtime/channels/+page.markdoc | 4 ++-- .../docs/apis/realtime/presence/+page.markdoc | 19 ++++++++++--------- .../docs/products/auth/presence/+page.markdoc | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index d2188f7254b..3c126b5c246 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 `upsert` and `delete` events for every record a subscriber has permission to read. +- **Dedicated Realtime channels** (`presences` and `presences.`) that emit `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. diff --git a/src/routes/changelog/(entries)/2026-05-22.markdoc b/src/routes/changelog/(entries)/2026-05-22.markdoc index 8e95deb2a7e..6549086387c 100644 --- a/src/routes/changelog/(entries)/2026-05-22.markdoc +++ b/src/routes/changelog/(entries)/2026-05-22.markdoc @@ -7,7 +7,7 @@ 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 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. +Presences broadcast every change over dedicated Realtime channels (`presences` and `presences.`) as `upsert`, `update`, and `delete` events, so an "online now" list, a typing indicator, or a "viewing this page" cue is a single `Channel.presences()` subscription away. Stale records emit `delete` events automatically when they expire, 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. diff --git a/src/routes/docs/apis/realtime/channels/+page.markdoc b/src/routes/docs/apis/realtime/channels/+page.markdoc index 5399050ce92..f930b098263 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 upsert or delete event on any [presence](/docs/apis/realtime/presence) the subscriber can read. +* Any upsert, update, or delete event on any [presence](/docs/apis/realtime/presence) the subscriber can read. --- * `presences.` * `Channel.presence('')` -* Any upsert or delete event on a given presence record. +* Any upsert, 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 index ba1725cf2b0..2d628e8fe1e 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -16,7 +16,7 @@ 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 `upsert` and `delete` events in milliseconds, over the same Realtime connection they are already using for rows and files. +**It is live.** Every change to a presence fires an event on the `presences` and `presences.` [Realtime](/docs/apis/realtime) channels. Subscribers get `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: @@ -1462,7 +1462,7 @@ const realtime = new Realtime(client); const subscription = await realtime.subscribe(Channel.presences(), response => { if (response.events.includes('presences.*.delete')) { console.log('Presence expired or removed', response.payload); - } else if (response.events.includes('presences.*.upsert')) { + } else if (response.events.includes('presences.*.upsert') || response.events.includes('presences.*.update')) { console.log('Presence created or updated', response.payload); } }); @@ -1482,7 +1482,7 @@ final subscription = realtime.subscribe([Channel.presences()]); subscription.stream.listen((response) { if (response.events.contains('presences.*.delete')) { print('Presence expired or removed: ${response.payload}'); - } else if (response.events.contains('presences.*.upsert')) { + } else if (response.events.contains('presences.*.upsert') || response.events.contains('presences.*.update')) { print('Presence created or updated: ${response.payload}'); } }); @@ -1500,7 +1500,7 @@ let realtime = Realtime(client) let subscription = realtime.subscribe(channels: [Channel.presences()]) { response in if (response.events?.contains("presences.*.delete") == true) { print("Presence expired or removed: \(String(describing: response.payload))") - } else if (response.events?.contains("presences.*.upsert") == true) { + } else if (response.events?.contains("presences.*.upsert") == true || response.events?.contains("presences.*.update") == true) { print("Presence created or updated: \(String(describing: response.payload))") } } @@ -1520,7 +1520,7 @@ val realtime = Realtime(client) val subscription = realtime.subscribe(Channel.presences()) { if (it.events.contains("presences.*.delete")) { println("Presence expired or removed: ${it.payload}") - } else if (it.events.contains("presences.*.upsert")) { + } else if (it.events.contains("presences.*.upsert") || it.events.contains("presences.*.update")) { println("Presence created or updated: ${it.payload}") } } @@ -1530,9 +1530,10 @@ val subscription = realtime.subscribe(Channel.presences()) { The `events` array follows the same pattern as every other Appwrite resource: - `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 made via the REST `update()` operation. - `presences.*.delete` and `presences..delete` for records that were deleted explicitly or expired automatically. -`update()` is a REST-only operation and does not emit a Realtime event. If you need subscribers to see a status or metadata change live, use `upsert()` instead. +Note that there is no separate `create` event, the `upsert` event covers both first-time creation and subsequent writes. This gives you a clean signal for "user just came online", "user changed status", and "user went offline", without writing any custom socket logic. @@ -1545,14 +1546,14 @@ This gives you a clean signal for "user just came online", "user changed status" --- * `presences` * `Channel.presences()` -* Any upsert or delete event on any presence the subscriber can read. +* Any upsert, update, or delete event on any presence the subscriber can read. --- * `presences.` * `Channel.presence('')` -* Any upsert or delete event on a specific presence record. +* Any upsert, update, or delete event on a specific presence record. {% /table %} -You can also append `.upsert()` or `.delete()` to `Channel.presence('')` to narrow the stream to a single event type, identical to how channel filters work on every other resource. +You can also append `.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 %} diff --git a/src/routes/docs/products/auth/presence/+page.markdoc b/src/routes/docs/products/auth/presence/+page.markdoc index 5781dfd7c08..997f0e4a948 100644 --- a/src/routes/docs/products/auth/presence/+page.markdoc +++ b/src/routes/docs/products/auth/presence/+page.markdoc @@ -178,7 +178,7 @@ val onlineUsers = result.presences ``` {% /multicode %} -Then subscribe to the global `presences` channel to keep that snapshot live. Apply the same patch to the same `onlineUsers` map on every event, add or replace on upsert, remove on delete. +Then subscribe to the global `presences` channel to keep that snapshot live. Apply the same patch to the same `onlineUsers` map on every event, add or replace on upsert or update, remove on delete. {% multicode %} ```client-web From e27b82d69a5b5b4a2881ea4c2f23e8bdb24c730e Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 19:31:59 +0530 Subject: [PATCH 27/49] fix .gitignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index d78e97fedc9..c819cc91e90 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,4 @@ terraform/**/**/*.tfstate* # Sentry Config File .env.sentry-build-plugin -.playwright-mcp - -SKILL.md -AGENTS.md \ No newline at end of file +.playwright-mcp \ No newline at end of file From 8f18c3444ccbcc444af9bbde8645911356386d30 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 19:39:09 +0530 Subject: [PATCH 28/49] Add expiry and permissions examples --- .../docs/apis/realtime/presence/+page.markdoc | 153 +++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index 2d628e8fe1e..b020a78192a 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -1561,6 +1561,77 @@ 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. +{% 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', + expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString() +}); +``` + +```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', + expiresAt: DateTime.now().add(Duration(minutes: 5)).toIso8601String(), +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let presences = Presences(client) + +let formatter = ISO8601DateFormatter() +let presence = try await presences.upsert( + presenceId: ID.unique(), + status: "online", + expiresAt: formatter.string(from: Date().addingTimeInterval(300)) +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.services.Presences +import java.time.Instant +import java.time.temporal.ChronoUnit + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val presences = Presences(client) + +val presence = presences.upsert( + presenceId = ID.unique(), + status = "online", + expiresAt = Instant.now().plus(5, ChronoUnit.MINUTES).toString() +) +``` +{% /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 %} @@ -1571,7 +1642,87 @@ Presences use the standard Appwrite [permissions system](/docs/advanced/platform - `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. +Pass a `permissions` array to `upsert()` to attach roles to a presence. For example, to make a presence visible only to a specific team: + +{% multicode %} +```client-web +import { Client, Presences, ID, Permission, Role } 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', + permissions: [ + Permission.read(Role.team('')) + ] +}); +``` + +```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', + permissions: [ + Permission.read(Role.team('')), + ], +); +``` + +```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", + permissions: [ + Permission.read(Role.team("")) + ] +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.Permission +import io.appwrite.Role +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", + permissions = listOf( + Permission.read(Role.team("")) + ) +) +``` +{% /multicode %} + +Server SDKs need an API key with the `presences.read` scope to list or read presences, and `presences.write` to upsert or delete them. Client sessions can always update their own presence without an extra scope. # Use cases {% #use-cases %} From 9cda1a6f08295ada5b7103cbc5753b245b51b427 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 19:42:04 +0530 Subject: [PATCH 29/49] Update presence subscription example to include initial list rendering --- .../blog/post/announcing-presence-api/+page.markdoc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 3c126b5c246..e4212d7a1ab 100644 --- a/src/routes/blog/post/announcing-presence-api/+page.markdoc +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -117,18 +117,23 @@ val presence = presences.upsert( # 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. +Render the initial "online now" list with `presences.list()`, then subscribe to the global presences channel to keep it in lockstep. 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"; +import { Client, Presences, Realtime, Channel } from "appwrite"; const client = new Client() .setEndpoint('https://.cloud.appwrite.io/v1') .setProject(''); +const presences = new Presences(client); const realtime = new Realtime(client); -const onlineUsers = new Map(); +const result = await presences.list(); + +const onlineUsers = new Map( + result.presences.map(presence => [presence.userId, presence]) +); await realtime.subscribe(Channel.presences(), response => { const presence = response.payload; From dc78e5513003aecedbe416957eff38a5c87ee8f8 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 22 May 2026 20:44:05 +0530 Subject: [PATCH 30/49] docs(presence): fix Rust list example, add announcement cover - Rust `list` code block did not compile: `Query::equal` returns a `Query` but `list` expects `Vec`, and `vec!["online".into()]` was type-ambiguous. Use `Query::equal("status", vec!["online".to_string()]).to_string()`. - Add the Presence API announcement cover image, optimized to avif. --- .optimize-cache.json | 1 + .../post/announcing-presence-api/+page.markdoc | 2 +- .../docs/apis/realtime/presence/+page.markdoc | 2 +- .../blog/announcing-presence-api/cover.avif | Bin 0 -> 34084 bytes 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 static/images/blog/announcing-presence-api/cover.avif diff --git a/.optimize-cache.json b/.optimize-cache.json index ecaeb30e0c0..2258aedbaa2 100644 --- a/.optimize-cache.json +++ b/.optimize-cache.json @@ -231,6 +231,7 @@ "static/images/blog/announcing-new-push-notifications-features/cover.png": "a0c758cf6c8a95e09a0d2ca562b0775a50d34a4d691d675cda70e44ad21805ac", "static/images/blog/announcing-opt-in-relationship-loading/cover.png": "e16cc16ea6d968b29af19bcd6274741141584a7efe5e1bb18be19b77c3a380c8", "static/images/blog/announcing-phone-OTP-pricing/cover.png": "598d55359ca4cb2b46846a8fd76b1f051be7c5f3199b50ffa92a28e84e5f3d67", + "static/images/blog/announcing-presence-api/cover.png": "d7b8b109f833791442a8ef908e0644db4b2acd8ae967263496e7d82db8a5ef02", "static/images/blog/announcing-realtime-channel-helpers/cover.png": "cbcffde3edfb77908566ff6361cb31bb1175d64bb1958a038720c52748dfa904", "static/images/blog/announcing-realtime-queries/cover.png": "2e11ad5d30399bced1817ce0edb6d266cc57a70955d2102af173d26461c9bf57", "static/images/blog/announcing-relationship-queries/cover.png": "7e615c0a9dcbb3949d5fb7ed71f36bb44de40ae67c8cd832b96ff5bbd4b0f451", diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index e4212d7a1ab..9b2118ebb4e 100644 --- a/src/routes/blog/post/announcing-presence-api/+page.markdoc +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -3,7 +3,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-22 -cover: /images/blog/announcing-presence-api/cover.png +cover: /images/blog/announcing-presence-api/cover.avif timeToRead: 5 author: aditya-oberai category: announcement diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index b020a78192a..a94f1d2acdc 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -876,7 +876,7 @@ async fn main() -> Result<(), Box> { let presences = Presences::new(&client); let result = presences.list( - Some(vec![Query::equal("status", vec!["online".into()])]), + Some(vec![Query::equal("status", vec!["online".to_string()]).to_string()]), None, None, ).await?; diff --git a/static/images/blog/announcing-presence-api/cover.avif b/static/images/blog/announcing-presence-api/cover.avif new file mode 100644 index 0000000000000000000000000000000000000000..09efacd65d1f3c497aeb2efaadbf7d80bebc987d GIT binary patch literal 34084 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 literal 0 HcmV?d00001 From a391e2874e54cf8746d1983c8ff4fb29d2de4cb9 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 21:33:22 +0530 Subject: [PATCH 31/49] add realtime.upsertPresence explanation --- .../announcing-presence-api/+page.markdoc | 2 +- .../docs/apis/realtime/presence/+page.markdoc | 72 +++++++++++++++++-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/routes/blog/post/announcing-presence-api/+page.markdoc b/src/routes/blog/post/announcing-presence-api/+page.markdoc index 9b2118ebb4e..f206e9ffe70 100644 --- a/src/routes/blog/post/announcing-presence-api/+page.markdoc +++ b/src/routes/blog/post/announcing-presence-api/+page.markdoc @@ -113,7 +113,7 @@ val presence = presences.upsert( ``` {% /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. +`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. Persist the returned `$id` (in local storage, a state store, or wherever your session lives) and reuse it on every subsequent `upsert` call so each user keeps a single record across heartbeats and route changes. # Subscribing to presence updates diff --git a/src/routes/docs/apis/realtime/presence/+page.markdoc b/src/routes/docs/apis/realtime/presence/+page.markdoc index a94f1d2acdc..c444a609a4d 100644 --- a/src/routes/docs/apis/realtime/presence/+page.markdoc +++ b/src/routes/docs/apis/realtime/presence/+page.markdoc @@ -31,6 +31,74 @@ 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.** Call `realtime.upsertPresence(...)` over an open Realtime connection and the record is automatically deleted when that connection closes. Best for "online while the tab is open" UIs where you do not want to manage a heartbeat yourself. +The `realtime.upsertPresence(...)` call mirrors the REST `presences.upsert(...)` signature, but the record's lifetime is tied to the WebSocket rather than to `expiresAt`: + +{% multicode %} +```client-web +import { Client, Realtime, ID } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +const realtime = new Realtime(client); + +await realtime.upsertPresence({ + presenceId: ID.unique(), + status: 'online' +}); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject(''); + +final realtime = Realtime(client); + +await realtime.upsertPresence( + presenceId: ID.unique(), + status: 'online', +); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +let realtime = Realtime(client) + +try await realtime.upsertPresence( + presenceId: ID.unique(), + status: "online" +) +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.ID +import io.appwrite.services.Realtime + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + +val realtime = Realtime(client) + +realtime.upsertPresence( + presenceId = ID.unique(), + status = "online" +) +``` +{% /multicode %} + +The SDK remembers the latest payload and re-sends it after a reconnect, so a brief network drop will not flip the user offline. There is no heartbeat to manage. The record disappears automatically the moment the WebSocket closes for good (tab close, sign out, sustained network loss). + # Upsert a presence {% #upsert-a-presence %} `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. @@ -341,10 +409,6 @@ 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 ea1a50b54c9a9dbb722cb3b912eadd1e2ff9f3f4 Mon Sep 17 00:00:00 2001 From: adityaoberai Date: Fri, 22 May 2026 22:55:29 +0530 Subject: [PATCH 32/49] Add Presence to sidenavs --- src/routes/docs/apis/realtime/+layout.svelte | 6 ++++++ src/routes/docs/products/auth/+layout.svelte | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/routes/docs/apis/realtime/+layout.svelte b/src/routes/docs/apis/realtime/+layout.svelte index 1feb957699c..b3387824f74 100644 --- a/src/routes/docs/apis/realtime/+layout.svelte +++ b/src/routes/docs/apis/realtime/+layout.svelte @@ -1,6 +1,7 @@