diff --git a/.optimize-cache.json b/.optimize-cache.json
index 0e9b24dd9b..7113e5e70b 100644
--- a/.optimize-cache.json
+++ b/.optimize-cache.json
@@ -1594,6 +1594,42 @@
"static/images/docs/network/edges-map.png": "12ecc1ea200905ba75eb7cfd17055a156fd51fccf746869a1058f923dfd7ac1b",
"static/images/docs/network/pops-map.png": "205ead599703cf47d0df316db8fcc4f48d5eed01508109fc740d17914275e9ab",
"static/images/docs/network/regions-map.png": "c65f1423ab19c3048bf8bf93117e8f2e1d13a2bc705c00307de7ee821e5668a1",
+ "static/images/docs/partners/apps/account-applications.png": "85b60bff922db940aafe30a3eb5c1646d66a30f6d7894df2f0c14c65bd79d7e6",
+ "static/images/docs/partners/apps/consent-screen.png": "5c6875f7d9af0180d0c5859bac2c57bb620a2243aac72530ca122a40f3758b50",
+ "static/images/docs/partners/apps/dark/account-applications.png": "03389413091ae6530ebb592d9aade7248b0b8b1a34e4a1b90c3f48480bf01ae3",
+ "static/images/docs/partners/apps/dark/consent-screen.png": "cfb1385940b821a39a90e2c2010d6a9d86b7d02373adb564e78711088acd092c",
+ "static/images/docs/partners/apps/dark/device-code.png": "e84f23cfcd904cf5fa5aef2642738fb6ce2289135c8f7d74c5b85d36b3da127c",
+ "static/images/docs/partners/apps/dark/diagram-consent.png": "cdf260c28c98ebebe8e553eb8600dece94989d160bece85c1fee63e9cf21de21",
+ "static/images/docs/partners/apps/dark/diagram-device.png": "208a3dc343bfd1c4ea16d64b6b19e8efec363855231b0eaf467de1397bc29a64",
+ "static/images/docs/partners/apps/dark/diagram-overview.png": "1fd06d30fc53efa69157f3c6be62c991610acdc7c97463e58341310d6fc0ebe5",
+ "static/images/docs/partners/apps/dark/diagram-registration.png": "66abebb561a61bc51923f42c6f85ec63a4de4ce57901f74072fa3efb50277f30",
+ "static/images/docs/partners/apps/dark/diagram-scopes.png": "ee95a74b1ab45607c4ab72f8522ecc624471f81b74139cf5288066e3c8fe9c2f",
+ "static/images/docs/partners/apps/dark/diagram-tokens.png": "99c1951b4640fba1d7e116ef0dd75ed5c171f843f6ff312bc16f2b0f08e2239f",
+ "static/images/docs/partners/apps/dark/quick-start-consent.png": "7b5c60b821ea7d3cc037ba77f97e858429c6a63a99b453ac063fbe9628e458c5",
+ "static/images/docs/partners/apps/dark/quick-start-provider-setup.png": "55a2bd989f7d7237d17d2bdae78d5d1cce8adadd01e6c155ff1c766cb26a1fb2",
+ "static/images/docs/partners/apps/dark/registration-general.png": "790e305253b5371db9997c07234a9f464fb8b1f2529206f4b737ddca4a1729a5",
+ "static/images/docs/partners/apps/dark/registration-legal.png": "aa1e4180df13f90e2d2797b2166bb401d05c7d90307bf9553f6fe06c0ecf00cf",
+ "static/images/docs/partners/apps/dark/registration-marketplace.png": "e7af87266c488d7627d5334f9b4264afbbbcdf26f61e3cef1d44685e19008948",
+ "static/images/docs/partners/apps/dark/registration-oauth-client.png": "5cedae65dc096c51c644b7a8946525171a62398b20ffa593db86b6ad4aef7972",
+ "static/images/docs/partners/apps/dark/registration-publish.png": "e50dd334c7df7d926821ee37c07762e0fea196fd3da8a5bf6ff7b92d21b8b09f",
+ "static/images/docs/partners/apps/dark/registration-secrets.png": "fb81e51364c7ff4267fcc985b9556e697b5541b16b7520e9b5780b1cb3477dc2",
+ "static/images/docs/partners/apps/dark/registration-support.png": "f2d54e58b11f638401b0f4888dc5f404816dac86584af3fa571f6c045e7b3053",
+ "static/images/docs/partners/apps/device-code.png": "1afe19a6a183e2a0da2a4777db710d2b88e28e3dc51d0fedaed3dbcee1b29580",
+ "static/images/docs/partners/apps/diagram-consent.png": "3ba2989ce6f09ea5c3a0e49665d005c6f6b2fb1b92b624c2c2c30460649f96ae",
+ "static/images/docs/partners/apps/diagram-device.png": "f620b3b450ca3b34fc9326cb1b4bd9fc7066e4c912b229c211e1ef30ff77010d",
+ "static/images/docs/partners/apps/diagram-overview.png": "81932121776e4ac7af3a93fe4cfdd93a44aa8c4dbc739bfc08a56c92665bb612",
+ "static/images/docs/partners/apps/diagram-registration.png": "46b85ba2d4bbcb932c6b15d338586542d67346d981f204d36aa3e4f5bdd6debb",
+ "static/images/docs/partners/apps/diagram-scopes.png": "cc01f8f049ce13956c0311f3192c41ce964f89819e016e61deb29475ca5a600c",
+ "static/images/docs/partners/apps/diagram-tokens.png": "c1f169da1c3c4abb51936a117d41dba5a71c721c508a1a91bc6cd8cf0b25149b",
+ "static/images/docs/partners/apps/quick-start-consent.png": "43ceef1d6e6a1f4b73ae0150508909f7af903332dc2817ba168fef361b185e57",
+ "static/images/docs/partners/apps/quick-start-provider-setup.png": "1c3dce9dc68df86c0744d86cfa0e2bcb2185fdc52f5fe22138ab8d21b1f571f2",
+ "static/images/docs/partners/apps/registration-general.png": "78b66e81d64fcb3bc421ac8ae923f03753ee730996c56e16fe3d4aeedac591c2",
+ "static/images/docs/partners/apps/registration-legal.png": "7d11e03dbf12709c2a349accd813e86315e85cc1fbf74d7e4c69f69ede48d39f",
+ "static/images/docs/partners/apps/registration-marketplace.png": "0b9aa3fb30a5c8dc0fa857c177f6bc714c490933c395c14957c56d27f099ccc3",
+ "static/images/docs/partners/apps/registration-oauth-client.png": "bf6d7c6afb0d8f75371ca261386c06a3f0a8e1942a1cec2fb910a296c2f707fb",
+ "static/images/docs/partners/apps/registration-publish.png": "734184fab22320d2c473d25ee8218ee6ebaed5971787d1e1930057d0135cad8c",
+ "static/images/docs/partners/apps/registration-secrets.png": "92e6ecff867e92d64aa6ceea0b67a711b7e1d715c9f3f99dc4df10e663a68aa0",
+ "static/images/docs/partners/apps/registration-support.png": "da585bd47fa71c9277a8f98a2b561c9fd31bc99a11a9169367a82bd3e744f8ae",
"static/images/docs/platform/add-platform.png": "5a05bb9d75a8d5270bfa5e67df7e6de20a9fad174476a112b5bdab72e7bdad30",
"static/images/docs/platform/create-api-key.png": "7661b3845e13704643f8ff4f763faa8e61efb90878c3ffa7466ece0910b8ecab",
"static/images/docs/platform/create-webhook.png": "77e08173da6ac534524e025433cf75e532d853a135944a7c6ba2278357d88b2d",
diff --git a/src/routes/docs/Sidebar.svelte b/src/routes/docs/Sidebar.svelte
index e63faa4715..a183efb855 100644
--- a/src/routes/docs/Sidebar.svelte
+++ b/src/routes/docs/Sidebar.svelte
@@ -102,6 +102,13 @@
icon: 'icon-briefcase',
isParent: true,
new: isNewUntil('22 Aug 2026')
+ },
+ {
+ label: 'Apps',
+ href: '/docs/partners/apps',
+ icon: 'icon-key',
+ isParent: true,
+ new: isNewUntil('31 Aug 2026')
}
]
},
diff --git a/src/routes/docs/partners/apps/+layout.svelte b/src/routes/docs/partners/apps/+layout.svelte
new file mode 100644
index 0000000000..454f33a34d
--- /dev/null
+++ b/src/routes/docs/partners/apps/+layout.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
+
diff --git a/src/routes/docs/partners/apps/+page.markdoc b/src/routes/docs/partners/apps/+page.markdoc
new file mode 100644
index 0000000000..42d45d2d22
--- /dev/null
+++ b/src/routes/docs/partners/apps/+page.markdoc
@@ -0,0 +1,79 @@
+---
+layout: article
+title: Sign in with Appwrite
+description: Build apps that access your users' Appwrite projects and organizations with consent-based, scoped OAuth2 tokens instead of pasted API keys.
+back: /docs
+---
+
+Appwrite is an **OAuth 2.1 and OpenID Connect provider**. Your app can send any Appwrite user to a consent screen, ask for access to the projects and organizations they choose, and receive tokens that call their project APIs directly. This is **Sign in with Appwrite**: the same consent flow users know from "Sign in with Google", pointed at their Appwrite account and backend.
+
+Before this, a tool that worked with a user's Appwrite project asked them to create an API key and paste it in. The key carried whatever scopes and expiry the user picked at creation, worked for one project only, and lived outside their control once pasted. With Sign in with Appwrite, the user approves once, picks the projects your app can reach, and can revoke everything from their account page at any time.
+
+{% info title="Building your own provider?" %}
+This section is for apps that build on top of Appwrite itself. To make your own product an OAuth2 provider so that third parties can offer "Sign in with your product", see the [OAuth2 server](/docs/products/auth/oauth-server) documentation.
+{% /info %}
+
+# What you can build {% #what-you-can-build %}
+
+Your app authenticates the user once and then acts on their Appwrite resources with the scopes they granted.
+
+- **Dashboards and monitors** that read usage, logs, and data across the projects a user selects.
+- **Deployment tools** that push functions and sites into a customer's project without holding a key.
+- **CLIs and devices** that sign in with a short user code instead of a browser redirect.
+- **AI agents and MCP clients** that operate under scopes the user can narrow to read-only.
+
+# How it works {% #how-it-works %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Sign in with Appwrite is the authorization code flow from OAuth 2.1, served by Appwrite.
+
+1. **Your app redirects the user** to the Appwrite authorization endpoint with the scopes it needs.
+2. **Appwrite shows the consent screen.** The user sees your app's name and logo, reviews the requested permissions, and picks which projects and organizations they apply to. They can grant fewer projects than you asked for, or decline entirely.
+3. **Your app receives an authorization code** at its redirect URI and exchanges it for an access token, a refresh token, and an ID token.
+4. **Your app calls Appwrite APIs** with the access token as a bearer token, on any of the granted projects, in any region.
+
+Because the provider is spec-compliant, any OAuth2 or OIDC library works against the discovery document without Appwrite-specific code:
+
+```text
+https://cloud.appwrite.io/v1/oauth2/console/.well-known/openid-configuration
+```
+
+Users stay in control after the redirect too. Every authorization appears on their account's applications page, where they see the scopes your app holds and the tokens issued under it, and can revoke a token family or the whole authorization at any time.
+
+# Explore {% #explore %}
+
+{% cards %}
+{% cards_item href="/docs/partners/apps/quick-start" title="Quick start" %}
+Register an app and run the full flow, from consent to your first authorized API call.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/registration" title="Registration" %}
+Register your app in the Console and shape its consent screen listing.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/scopes" title="Scopes" %}
+The scope catalog and how grants target specific projects and organizations.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/consent" title="Consent" %}
+What users see, what they can change, and what your app receives.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/tokens" title="Tokens" %}
+Use, refresh, and revoke the tokens Appwrite issues to your app.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/dashboards" title="Dashboards" %}
+Build a read-only dashboard over the projects a user selects.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/deployments" title="Deployments" %}
+Deploy functions and sites into a user's project with write scopes.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/device-flow" title="Device flow" %}
+Sign in from CLIs and devices with a short user code.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/agents" title="Agents" %}
+Connect AI agents and MCP clients to your users' projects.
+{% /cards_item %}
+{% /cards %}
diff --git a/src/routes/docs/partners/apps/consent/+page.markdoc b/src/routes/docs/partners/apps/consent/+page.markdoc
new file mode 100644
index 0000000000..524bd4bdcc
--- /dev/null
+++ b/src/routes/docs/partners/apps/consent/+page.markdoc
@@ -0,0 +1,67 @@
+---
+layout: article
+title: Consent
+description: What users see on the Sign in with Appwrite consent screen, what they can change, and what your app receives.
+back: /docs/partners/apps
+---
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Consent is where the user decides. Appwrite hosts the screen, renders what your app asked for, and gives the user the final say over scopes and targets. Your app never sees the screen; it sees the outcome.
+
+# The consent screen {% #consent-screen %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The screen is built from your [registration](/docs/partners/apps/registration) and your request:
+
+- **Your app's identity**: the name, logo, and tagline you registered.
+- **Permissions**: each requested scope as a plain-language line, under a one-line summary of the overall reach.
+- **Project access**: a picker for the projects or organizations the grant covers when the request carries project or organization scopes.
+
+The user signs in first if no session exists, and can switch accounts from the screen itself.
+
+# Users can grant less {% #partial-grants %}
+
+What you request is not always what you get. On the consent screen, the user can:
+
+- Deselect projects or organizations, shrinking where the grant applies.
+- Decline the whole request.
+
+The permission list itself is not editable. Scopes are granted as you requested them or not at all, which is another reason to request the smallest set that serves your app.
+
+The token response tells you what you got: `scope` holds the granted scopes, `authorization_details` the granted projects. Build for this from the start. A dashboard that asked for five projects but got two should show two projects, not an error.
+
+# When the screen is skipped {% #consent-reuse %}
+
+Appwrite remembers approvals. When a returning user's request asks for nothing new, they skip the screen and land straight back in your app. When anything differs from the stored approval, the screen reappears. Send the same authorization parameters on every sign-in and returning users sign in silently.
+
+Two `prompt` values override this:
+
+- `prompt=consent` always shows the screen, even when an approval exists.
+- `prompt=none` never shows it, returning `error=consent_required` or `error=login_required` instead. Use it to check for existing access silently.
+
+# Revocation {% #revocation %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Every approval appears on the user's account applications page. From there, the user revokes a single token or the whole app, which kills every token your app holds for them.
+
+Your app gets no notification. Calls and refreshes start returning `401`, and the fix is to send the user through authorization again.
+
+Revocation goes both ways: when a user disconnects your app on your side, revoke the tokens you hold at the [revocation endpoint](/docs/partners/apps/tokens#revocation).
diff --git a/src/routes/docs/partners/apps/device-flow/+page.markdoc b/src/routes/docs/partners/apps/device-flow/+page.markdoc
new file mode 100644
index 0000000000..44bf217f4e
--- /dev/null
+++ b/src/routes/docs/partners/apps/device-flow/+page.markdoc
@@ -0,0 +1,108 @@
+---
+layout: article
+title: Device flow
+description: Sign in with Appwrite from TVs, CLIs, and other input-constrained devices with the OAuth2 device authorization grant.
+back: /docs/partners/apps
+---
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Some clients cannot run the redirect flow: a TV has no browser to send the user back to, and a CLI has no redirect URI to receive a code. The device flow ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) replaces the redirect with a short code. Your app shows the code, the user approves it from their phone or laptop, and your app picks up the tokens by polling.
+
+The flow is off by default. Turn it on with the device flow toggle on your app's [OAuth client page](/docs/partners/apps/registration#console-oauth-client); the authorization request fails without it.
+
+# Request a device code {% #request-code %}
+
+Instead of building an authorization URL, ask for a device code, authenticated with your client credentials:
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/device_authorization \
+ -H "Content-Type: application/json" \
+ -d '{
+ "client_id": "",
+ "client_secret": "",
+ "scope": "openid profile email project:databases.read"
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/device_authorization
+{
+ "client_id": "",
+ "client_secret": "",
+ "scope": "openid profile email project:databases.read"
+}
+```
+{% /multicode %}
+
+Public clients send only their `client_id`. The response carries everything both sides need:
+
+```json
+{
+ "device_code": "6015241e3d2023091dcabf86d5ab85c3f16c985c49d418f37f0c446e1d2c2a9c",
+ "user_code": "MDF2TN39",
+ "verification_uri": "https://cloud.appwrite.io/console/oauth2/device",
+ "verification_uri_complete": "https://cloud.appwrite.io/console/oauth2/device?user_code=MDF2TN39",
+ "expires_in": 600,
+ "interval": 1
+}
+```
+
+Show the user the `user_code` and the `verification_uri`. The `verification_uri_complete` variant carries the code in the URL, which is what you encode into a QR code so the user skips typing it. The codes expire after `expires_in` seconds, ten minutes here, after which your app requests a fresh pair.
+
+# The user approves {% #user-approves %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+On their phone or laptop, the user opens the verification URL, signs in if no session exists, and enters the code. From there they land on the same [consent screen](/docs/partners/apps/consent) as the redirect flow, with one difference: after approving, the screen tells them to return to their device.
+
+# Poll for tokens {% #poll %}
+
+While the user approves, your app polls the token endpoint with the `device_code`, waiting at least `interval` seconds between attempts:
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/token \
+ -H "Content-Type: application/json" \
+ -d '{
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
+ "device_code": "",
+ "client_id": "",
+ "client_secret": ""
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/token
+{
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
+ "device_code": "",
+ "client_id": "",
+ "client_secret": ""
+}
+```
+{% /multicode %}
+
+Until the user decides, each poll returns a pending error:
+
+```json
+{
+ "error": "authorization_pending",
+ "error_description": "The user has not yet approved or denied the device authorization request."
+}
+```
+
+Keep polling. The moment the user approves, the same call returns the full [token response](/docs/partners/apps/tokens), with the access, refresh, and ID tokens and the granted scopes. If the user declines instead, polls return `error=access_denied`, and your app starts over with a fresh device code.
+
+From here, nothing is device-specific. The tokens behave like the tokens from any other grant: the same scopes and targets apply, and the refresh token [rotates on every use](/docs/partners/apps/tokens#refresh-tokens).
diff --git a/src/routes/docs/partners/apps/quick-start/+page.markdoc b/src/routes/docs/partners/apps/quick-start/+page.markdoc
new file mode 100644
index 0000000000..238ab9e098
--- /dev/null
+++ b/src/routes/docs/partners/apps/quick-start/+page.markdoc
@@ -0,0 +1,580 @@
+---
+layout: article
+title: Start with Sign in with Appwrite
+description: Register an app and run the full Sign in with Appwrite flow, from consent screen to your first authorized API call.
+back: /docs/partners/apps
+---
+
+This guide builds Sign in with Appwrite into an app, start to finish. The running example is Horizon, a deployment dashboard:
+
+- A user clicks its sign-in button.
+- They approve access on the Appwrite consent screen.
+- Horizon reads their projects with the tokens it receives.
+
+By the end, you will have run the same journey with your own app.
+
+You need a server that can receive a redirect and keep a client secret. The examples use `http://localhost:7700/oauth/callback` as the redirect URI; replace it with your own.
+
+# Register your app {% #register %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Before Appwrite can show your app to anyone, it needs to know the app exists. In the Console, open your organization's **Marketplace** tab and click **Add app**. Give it a name, a slug, a category, and a short description; the slug becomes your client ID, so Horizon signs in as `horizon`.
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The new app opens on its settings pages. Go to **OAuth client** and configure two things:
+
+- **Client type**: keep **Confidential**. Your app has a server, and the server will hold the client secret.
+- **Redirect URIs**: add your callback URL, `http://localhost:7700/oauth/callback` in this guide. After the user approves, Appwrite only ever redirects to a URL on this list, so a stolen client ID cannot send your users anywhere else.
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Last, your app needs a way to prove it is really Horizon when it exchanges codes for tokens. On **OAuth secrets**, select **Create secret** and copy the value; it is shown once. Store the client ID and the secret in your server's environment.
+
+[Registration](/docs/partners/apps/registration) covers the Console pages in more detail.
+
+# Redirect the user {% #redirect %}
+
+With credentials in your environment, Horizon can offer a Sign in with Appwrite button. The button sends the user to the authorization endpoint.
+
+The query string carries everything your app asks for:
+
+- Identity scopes, so Horizon knows who signed in.
+- One project scope, `project:databases.read`, for the data it needs.
+
+```text
+https://cloud.appwrite.io/v1/oauth2/console/authorize
+ ?client_id=
+ &redirect_uri=http://localhost:7700/oauth/callback
+ &response_type=code
+ &scope=openid profile email project:databases.read
+```
+
+URL-encode the values. Because the request carries a project scope, the consent screen asks the user which projects the grant covers. Scopes are described in the [scope catalog](/docs/partners/apps/scopes).
+
+# User grants access {% #consent %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+This is the moment the user meets your app inside Appwrite. The consent screen shows Horizon's name and logo, lists each requested permission in plain language, and lets the user select the projects the grant covers. On approval, the browser returns to your redirect URI:
+
+```text
+http://localhost:7700/oauth/callback?code=
+```
+
+Use the code within five minutes. It is single-use.
+
+# Exchange the code {% #exchange %}
+
+From your server, exchange the code for tokens, sending your client credentials in the request body.
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/token \
+ -H "Content-Type: application/json" \
+ -d '{
+ "grant_type": "authorization_code",
+ "code": "",
+ "redirect_uri": "http://localhost:7700/oauth/callback",
+ "client_id": "",
+ "client_secret": ""
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/token
+{
+ "grant_type": "authorization_code",
+ "code": "",
+ "redirect_uri": "http://localhost:7700/oauth/callback",
+ "client_id": "",
+ "client_secret": ""
+}
+```
+{% /multicode %}
+
+The response carries the three tokens and echoes what the user actually granted:
+
+```json
+{
+ "access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiJ9...",
+ "token_type": "Bearer",
+ "expires_in": 28800,
+ "refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
+ "scope": "openid profile email project:databases.read",
+ "authorization_details": [{ "type": "project", "identifiers": ["*"] }],
+ "id_token": "eyJhbGciOiJSUzI1NiJ9..."
+}
+```
+
+Check `authorization_details` for the projects the user selected; they may cover less than you asked for. Store the refresh token like a password. [Tokens](/docs/partners/apps/tokens) covers lifetimes, refresh rotation, and revocation.
+
+# Verify the token {% #verify-token %}
+
+Before doing anything else with the token, ask Appwrite who it belongs to. The userinfo endpoint answers with the user's identity, and only for tokens that are still active:
+
+{% multicode %}
+```curl
+curl https://cloud.appwrite.io/v1/oauth2/console/userinfo \
+ -H "Authorization: Bearer "
+```
+
+```hurl
+GET https://cloud.appwrite.io/v1/oauth2/console/userinfo
+Authorization: Bearer
+```
+{% /multicode %}
+
+The response is the signed-in user:
+
+```json
+{
+ "sub": "6a150ace003bc4c2919e",
+ "name": "Walter O'Brien",
+ "email": "walter@example.com",
+ "email_verified": true,
+ "updated_at": 1784707579
+}
+```
+
+An expired or revoked token returns a `401` of type `oauth2_invalid_token` instead, so a successful response also proves the token is still live.
+
+# List the user's projects {% #list-projects %}
+
+The user is signed in and Horizon holds its tokens. Now the payoff: every Sign in with Appwrite token can list the projects and organizations it was granted. Call the listing endpoint with the access token as a bearer token.
+
+{% multicode %}
+```curl
+curl https://cloud.appwrite.io/v1/oauth2/console/projects \
+ -H "Authorization: Bearer "
+```
+
+```hurl
+GET https://cloud.appwrite.io/v1/oauth2/console/projects
+Authorization: Bearer
+```
+{% /multicode %}
+
+The response lists every project the grant covers:
+
+```json
+{
+ "total": 2,
+ "projects": [
+ {
+ "$id": "6a357f7e001c7237296b",
+ "region": "fra",
+ "endpoint": "https://fra.cloud.appwrite.io/v1"
+ },
+ {
+ "$id": "6a357fa40031c857fa9f",
+ "region": "nyc",
+ "endpoint": "https://nyc.cloud.appwrite.io/v1"
+ }
+ ]
+}
+```
+
+Each entry carries the project's `$id`, `region`, and `endpoint`, which is everything your app needs to call it. The listing returns 25 projects per page and accepts `limit` and `offset` parameters for paging through more. A matching `/oauth2/console/organizations` endpoint lists granted organizations the same way.
+
+# Call a project API {% #call-project-api %}
+
+The listing gives Horizon everything it needs to do real work. Point a client at a granted project's endpoint, keep the same bearer token, and call the APIs your scopes allow. With `project:databases.read`, that is listing the databases the project holds.
+
+{% info title="No SDK required" %}
+Prefer not to use an SDK? The cURL and Hurl tabs show the raw API call: the project's endpoint, the `X-Appwrite-Project` header, and the access token in the `Authorization` header. Every Appwrite API accepts the same shape.
+{% /info %}
+
+{% multicode %}
+```curl
+curl "/tablesdb" \
+ -H "X-Appwrite-Project: " \
+ -H "Authorization: Bearer "
+```
+
+```hurl
+GET /tablesdb
+X-Appwrite-Project:
+Authorization: Bearer
+```
+
+```server-nodejs
+import { Client, TablesDB } from 'node-appwrite';
+
+const client = new Client()
+ .setEndpoint('') // From the project listing
+ .setProject('')
+ .setBearer('Bearer '); // The same access token
+
+const tablesDB = new TablesDB(client);
+
+const result = await tablesDB.list();
+```
+
+```server-deno
+import { Client, TablesDB } from 'npm:node-appwrite';
+
+const client = new Client()
+ .setEndpoint('') // From the project listing
+ .setProject('')
+ .setBearer('Bearer '); // The same access token
+
+const tablesDB = new TablesDB(client);
+
+const result = await tablesDB.list();
+```
+
+```server-python
+from appwrite.client import Client
+from appwrite.services.tables_db import TablesDB
+
+client = Client()
+client.set_endpoint('') # From the project listing
+client.set_project('')
+client.set_bearer('Bearer ') # The same access token
+
+tables_db = TablesDB(client)
+
+result = tables_db.list()
+```
+
+```server-php
+setEndpoint('') // From the project listing
+ ->setProject('')
+ ->setBearer('Bearer '); // The same access token
+
+$tablesDB = new TablesDB($client);
+
+$result = $tablesDB->list();
+```
+
+```server-ruby
+require 'appwrite'
+
+include Appwrite
+
+client = Client.new
+ .set_endpoint('') # From the project listing
+ .set_project('')
+ .set_bearer('Bearer ') # The same access token
+
+tables_db = TablesDB.new(client)
+
+result = tables_db.list()
+```
+
+```server-dart
+import 'package:dart_appwrite/dart_appwrite.dart';
+
+Client client = Client()
+ .setEndpoint('') // From the project listing
+ .setProject('')
+ .setBearer('Bearer '); // The same access token
+
+TablesDB tablesDB = TablesDB(client);
+
+DatabaseList result = await tablesDB.list();
+```
+
+```server-kotlin
+import io.appwrite.Client
+import io.appwrite.services.TablesDB
+
+val client = Client()
+ .setEndpoint("") // From the project listing
+ .setProject("")
+ .setBearer("Bearer ") // The same access token
+
+val tablesDB = TablesDB(client)
+
+val result = tablesDB.list()
+```
+
+```server-java
+import io.appwrite.Client;
+import io.appwrite.coroutines.CoroutineCallback;
+import io.appwrite.services.TablesDB;
+
+Client client = new Client()
+ .setEndpoint("") // From the project listing
+ .setProject("")
+ .setBearer("Bearer "); // The same access token
+
+TablesDB tablesDB = new TablesDB(client);
+
+tablesDB.list(
+ null, // queries (optional)
+ null, // search (optional)
+ null, // total (optional)
+ new CoroutineCallback<>((result, error) -> {
+ if (error != null) {
+ error.printStackTrace();
+ return;
+ }
+
+ System.out.println(result);
+ })
+);
+```
+
+```server-swift
+import Appwrite
+
+let client = Client()
+ .setEndpoint("") // From the project listing
+ .setProject("")
+ .setBearer("Bearer ") // The same access token
+
+let tablesDB = TablesDB(client)
+
+let databaseList = try await tablesDB.list()
+```
+
+```server-dotnet
+using Appwrite;
+using Appwrite.Models;
+using Appwrite.Services;
+
+Client client = new Client()
+ .SetEndPoint("") // From the project listing
+ .SetProject("")
+ .SetBearer("Bearer "); // The same access token
+
+TablesDB tablesDB = new TablesDB(client);
+
+DatabaseList result = await tablesDB.List();
+```
+
+```server-go
+package main
+
+import (
+ "fmt"
+
+ "github.com/appwrite/sdk-for-go/client"
+ "github.com/appwrite/sdk-for-go/tablesdb"
+)
+
+func main() {
+ c := client.New(
+ client.WithEndpoint(""), // From the project listing
+ client.WithProject(""),
+ client.WithBearer("Bearer "), // The same access token
+ )
+
+ service := tablesdb.New(c)
+
+ result, err := service.List()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(result)
+}
+```
+
+```server-rust
+use appwrite::Client;
+use appwrite::services::TablesDB;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let client = Client::new();
+ client.set_endpoint(""); // From the project listing
+ client.set_project("");
+ client.set_bearer("Bearer "); // The same access token
+
+ let tables_db = TablesDB::new(&client);
+
+ let result = tables_db.list(None, None, None).await?;
+ println!("{:?}", result.total);
+
+ Ok(())
+}
+```
+{% /multicode %}
+
+A call outside the granted scopes or projects fails with a `401` error of type `general_unauthorized_scope`. The token works on every region, so your app never maps regions itself; use each project's `endpoint` value from the listing.
+
+# Refresh the token {% #refresh-token %}
+
+Access tokens expire after 8 hours. When one does, trade the refresh token for a new pair instead of sending the user back through consent:
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/token \
+ -H "Content-Type: application/json" \
+ -d '{
+ "grant_type": "refresh_token",
+ "refresh_token": "",
+ "client_id": "",
+ "client_secret": ""
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/token
+{
+ "grant_type": "refresh_token",
+ "refresh_token": "",
+ "client_id": "",
+ "client_secret": ""
+}
+```
+{% /multicode %}
+
+The response has the same shape as the original exchange: a fresh access token and a new refresh token. Refresh tokens are single-use, so store the new one as soon as it arrives; reusing a spent token invalidates the whole pair. [Tokens](/docs/partners/apps/tokens#refresh-tokens) covers rotation in detail.
+
+# Sign in through Appwrite Auth {% #appwrite-provider %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+If your product itself runs on an Appwrite project, you do not need to handle the flow by hand. Appwrite Auth ships an **Appwrite** OAuth2 provider.
+
+To enable it:
+
+- In your project, open **Auth**, then **Social providers**, and select **Appwrite**.
+- Use **Quick setup** to create or pick an organization app. Appwrite registers the callback URI and fills in the client ID and secret.
+
+Then sign users in with the same call you would use for GitHub or Google:
+
+{% multicode %}
+```client-web
+import { Client, Account, OAuthProvider } from 'appwrite';
+
+const client = new Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1')
+ .setProject(''); // Your project, not the user's
+
+const account = new Account(client);
+
+account.createOAuth2Session({
+ provider: OAuthProvider.Appwrite,
+ success: 'https://example.com/success',
+ failure: 'https://example.com/failure',
+ scopes: ['project:databases.read']
+});
+```
+
+```client-flutter
+import 'package:appwrite/appwrite.dart';
+import 'package:appwrite/enums.dart';
+
+Client client = Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1')
+ .setProject(''); // Your project, not the user's
+
+Account account = Account(client);
+
+await account.createOAuth2Session(
+ provider: OAuthProvider.appwrite,
+ scopes: ['project:databases.read'],
+);
+```
+
+```client-apple
+import Appwrite
+import AppwriteEnums
+
+let client = Client()
+ .setEndpoint("https://.cloud.appwrite.io/v1")
+ .setProject("") // Your project, not the user's
+
+let account = Account(client)
+
+try await account.createOAuth2Session(
+ provider: .appwrite,
+ scopes: ["project:databases.read"]
+)
+```
+
+```client-android-kotlin
+import io.appwrite.Client
+import io.appwrite.enums.OAuthProvider
+import io.appwrite.services.Account
+
+val client = Client(context)
+ .setEndpoint("https://.cloud.appwrite.io/v1")
+ .setProject("") // Your project, not the user's
+
+val account = Account(client)
+
+account.createOAuth2Session(
+ activity = activity,
+ provider = OAuthProvider.APPWRITE,
+ scopes = listOf("project:databases.read"),
+)
+```
+
+```client-react-native
+import { Client, Account, OAuthProvider } from 'react-native-appwrite';
+
+const client = new Client()
+ .setEndpoint('https://.cloud.appwrite.io/v1')
+ .setProject(''); // Your project, not the user's
+
+const account = new Account(client);
+
+account.createOAuth2Token({
+ provider: OAuthProvider.Appwrite,
+ success: '',
+ failure: '',
+ scopes: ['project:databases.read']
+});
+```
+{% /multicode %}
+
+The provider always requests the identity scopes. The `scopes` parameter adds project scopes on top, and the consent screen lets the user select which projects they apply to.
+
+After sign-in, the issued tokens live on the user's [identity](/docs/products/auth/identities). Your backend reads them from there to call the granted projects.
+
+Use this path when your product runs on Appwrite. Use the manual flow above when you want to handle the tokens yourself.
+
+# Next steps {% #next-steps %}
+
+{% cards %}
+{% cards_item href="/docs/partners/apps/registration" title="Registration" %}
+Give your app a logo and consent screen details, and manage secrets.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/scopes" title="Scopes" %}
+Request exactly the access your app needs.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/dashboards" title="Dashboards" %}
+Build a read-only dashboard over granted projects.
+{% /cards_item %}
+{% cards_item href="/docs/partners/apps/tokens" title="Tokens" %}
+Refresh tokens before they expire and handle revocation.
+{% /cards_item %}
+{% /cards %}
diff --git a/src/routes/docs/partners/apps/registration/+page.markdoc b/src/routes/docs/partners/apps/registration/+page.markdoc
new file mode 100644
index 0000000000..526e0d7faf
--- /dev/null
+++ b/src/routes/docs/partners/apps/registration/+page.markdoc
@@ -0,0 +1,102 @@
+---
+layout: article
+title: Registration
+description: Register your app for Sign in with Appwrite through the Console.
+back: /docs/partners/apps
+---
+
+Your app appears on the consent screen as a registered client: a name, a logo, and a set of credentials tied to redirect URIs. Registration happens in the Console, in your organization's Marketplace tab.
+
+# Client types {% #client-types %}
+
+Every client is `confidential` or `public`, and the choice decides how it authenticates.
+
+- **Confidential** clients have a backend that keeps a secret. They authenticate token requests with the client secret, and their tokens live longer: 8 hours for access tokens and 365 days for refresh tokens by default.
+- **Public** clients run where a secret cannot survive: single-page apps, native apps, CLIs. They carry no secret and must use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) with `S256` on every authorization. Their tokens default to 1 hour and 30 days.
+
+Choose confidential whenever a server takes part in the flow. Public exists for the rest.
+
+# Console {% #console %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Register through the Console when your app is a product: the same flow that creates the client also builds its marketplace listing. In your organization, open the **Marketplace** tab and click **Add app**. Give the app a name, a slug, a category, and a description. The slug becomes the app's client ID, and the app starts as a draft that only your organization sees.
+
+Once created, the app opens on its settings pages. Each page below is one of them, and every page has an **Update** button that saves that page's fields.
+
+## General {% #console-general %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The **General** page holds the app's identity: its name, a one-line tagline, a longer description, and a category. The name appears on the consent screen; all four appear on the marketplace listing. This page also shows the app ID and the control to delete the app.
+
+## OAuth client {% #console-oauth-client %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The **OAuth client** page sets the protocol details: the [client type](#client-types), the device flow toggle for TVs and CLIs, and the redirect URIs. Add every URI your app returns to; the authorization endpoint rejects anything not on the list.
+
+## OAuth secrets {% #console-oauth-secrets %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The **OAuth secrets** page issues client secrets for confidential apps. Each secret's value appears once, at creation; store it in your deployment environment before closing the dialog. The list keeps only a hint of each secret, its creation date, and when it was last used.
+
+An app holds several active secrets at once, so rotation needs no downtime: create a new secret, deploy it, then delete the old one. The last-used timestamp tells you when the old secret is safe to delete.
+
+## Legal {% #console-legal %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The **Legal** page holds the privacy policy, terms of service, and data deletion URLs. The consent screen links these so users can read them before they approve your app.
+
+## Support {% #console-support %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The **Support** page holds a support URL and contact emails, so users have somewhere to turn when they need help with your app.
+
+## Publish {% #console-publish %}
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+The **Publish** page carries the branding shown on the consent screen, a homepage URL, a logo, and optional listing images, along with the **Published** toggle. Turning it on lists the app in the marketplace catalog for every organization. Publishing changes discovery, not authorization: users consent to your app the same way whether it is a draft or published.
+
+# Labels {% #labels %}
+
+Labels such as `official` and `partner` mark apps the platform vouches for, and consent screens surface them as trust signals. Labels are assigned by Appwrite, not by the app itself, so there is no field for them in the Console. Contact your partner manager to have your app labeled.
diff --git a/src/routes/docs/partners/apps/scopes/+page.markdoc b/src/routes/docs/partners/apps/scopes/+page.markdoc
new file mode 100644
index 0000000000..71cc98fb66
--- /dev/null
+++ b/src/routes/docs/partners/apps/scopes/+page.markdoc
@@ -0,0 +1,122 @@
+---
+layout: article
+title: Scopes
+description: The Sign in with Appwrite scope catalog, and how grants target the projects and organizations a user chooses.
+back: /docs/partners/apps
+---
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+A scope names an action your app wants to perform. A grant pairs scopes with the projects and organizations they apply to. Both halves matter: `project:databases.read` by itself says what, and the user's project selection on the consent screen says where.
+
+Request the smallest set that serves your app. Every scope you ask for appears on the consent screen as a permission line, and users decline requests that want too much.
+
+# Identity scopes {% #identity-scopes %}
+
+The OpenID Connect scopes cover who the user is. They are the same four scopes every OIDC provider uses.
+
+| Scope | Grants |
+| --- | --- |
+| `openid` | The user's subject identifier. Required to receive an ID token. |
+| `profile` | Profile claims, such as the user's name. |
+| `email` | The user's email address and its verification state. |
+| `phone` | The user's phone number and its verification state. |
+
+An app that only signs users in requests `openid profile email` and stops there.
+
+# Project scopes {% #project-scopes %}
+
+Project scopes carry the `project:` prefix and map to the permissions [API keys](/docs/partners/project/api-keys) use, one `.read` and one `.write` scope per resource. The catalog covers every Appwrite product:
+
+| Product | Scopes |
+| --- | --- |
+| Databases | `project:databases.read`, `project:databases.write`, `project:tables.read`, `project:tables.write`, `project:columns.read`, `project:columns.write`, `project:rows.read`, `project:rows.write`, `project:indexes.read`, `project:indexes.write` |
+| Auth | `project:users.read`, `project:users.write`, `project:sessions.read`, `project:sessions.write`, `project:teams.read`, `project:teams.write` |
+| Storage | `project:buckets.read`, `project:buckets.write`, `project:files.read`, `project:files.write`, `project:tokens.read`, `project:tokens.write` |
+| Functions | `project:functions.read`, `project:functions.write`, `project:executions.read`, `project:executions.write` |
+| Sites | `project:sites.read`, `project:sites.write`, `project:log.read`, `project:log.write` |
+| Messaging | `project:providers.read`, `project:providers.write`, `project:topics.read`, `project:topics.write`, `project:subscribers.read`, `project:subscribers.write`, `project:targets.read`, `project:targets.write`, `project:messages.read`, `project:messages.write` |
+| Projects | `project:project.read`, `project:project.write`, `project:keys.read`, `project:keys.write`, `project:platforms.read`, `project:platforms.write`, `project:webhooks.read`, `project:webhooks.write`, `project:policies.read`, `project:policies.write` |
+| Proxy | `project:rules.read`, `project:rules.write` |
+| Domains | `project:domains.read`, `project:domains.write` |
+| WAF | `project:wafRules.read`, `project:wafRules.write` |
+| Usage | `project:usage.read` |
+| Health | `project:health.read` |
+| Migrations | `project:migrations.read`, `project:migrations.write` |
+| Backups | `project:backups.policies.read`, `project:backups.policies.write`, `project:archives.read`, `project:archives.write`, `project:restorations.read`, `project:restorations.write` |
+
+`project:all` grants the full project catalog at once. Reserve it for tools that genuinely administer whole projects; the consent screen presents it as full project access.
+
+The full list is published in the discovery document under `scopes_supported`:
+
+```text
+https://cloud.appwrite.io/v1/oauth2/console/.well-known/openid-configuration
+```
+
+# Organization scopes {% #organization-scopes %}
+
+Organization scopes carry the `organization:` prefix and cover what a user's organization owns: its profile, members, projects, domains, and keys.
+
+| Area | Scopes |
+| --- | --- |
+| Organization | `organization:organization.read`, `organization:organization.write` |
+| Members | `organization:organization.memberships.read`, `organization:organization.memberships.write` |
+| Projects | `organization:projects.read`, `organization:projects.write` |
+| Domains | `organization:domains.read`, `organization:domains.write` |
+| Keys | `organization:organization.keys.read`, `organization:organization.keys.write` |
+
+`organization:all` grants the full set. `organization:projects.write` is the scope that creates projects, which lets an app provision on a user's behalf.
+
+# Targeting projects and organizations {% #targeting %}
+
+Scopes say what your app can do. Targets say where. Every grant carries both.
+
+When your request includes a project or organization scope, the consent screen asks the user which projects or organizations the scope applies to. You send only the scopes; the user's selection is written into the grant, and your app does nothing extra to make it happen.
+
+## Reading what was granted {% #targets-granted %}
+
+The token response's `authorization_details` field holds the projects the user ended up granting.
+
+To check a token you already hold, decode it. The access token is a JWT, and its payload carries the granted scopes and targets alongside the standard claims:
+
+```json
+{
+ "iss": "https://cloud.appwrite.io/v1/oauth2/console",
+ "sub": "6a150ace003bc4c2919e",
+ "client_id": "horizon",
+ "scope": "openid project:databases.read",
+ "authorization_details": [{ "type": "project", "identifiers": ["*"] }],
+ "exp": 1784832895
+}
+```
+
+Neither value changes after issuance, so a decode is authoritative for what the token holds. [Tokens](/docs/partners/apps/tokens#validation) covers verifying the signature.
+
+To turn the targets into concrete projects, list them with the token itself:
+
+{% multicode %}
+```curl
+curl https://cloud.appwrite.io/v1/oauth2/console/projects \
+ -H "Authorization: Bearer "
+```
+
+```hurl
+GET https://cloud.appwrite.io/v1/oauth2/console/projects
+Authorization: Bearer
+```
+{% /multicode %}
+
+Each project in the response carries its `$id`, `region`, and `endpoint`, which is everything your app needs to call it. The listing returns 25 projects per page and accepts `limit` and `offset` parameters; a matching `/oauth2/console/organizations` endpoint lists granted organizations the same way.
+
+A scope only works inside its granted targets. A token with `project:databases.read` lists databases in the selected projects and returns `401 general_unauthorized_scope` everywhere else.
+
+# When requests fail {% #failures %}
+
+A scope outside the catalog fails before the user ever sees a consent screen: the authorization endpoint redirects straight back to your app with `error=invalid_scope`. The catalog is published in the discovery document under `scopes_supported`, so your app can validate requests before sending them.
+
+The user can also grant less than you asked by deselecting projects or organizations on the consent screen, so treat the token response's `authorization_details` as the truth, not your request. [Consent](/docs/partners/apps/consent) shows what partial grants look like.
diff --git a/src/routes/docs/partners/apps/tokens/+page.markdoc b/src/routes/docs/partners/apps/tokens/+page.markdoc
new file mode 100644
index 0000000000..45b7da94ae
--- /dev/null
+++ b/src/routes/docs/partners/apps/tokens/+page.markdoc
@@ -0,0 +1,292 @@
+---
+layout: article
+title: Tokens
+description: Use, refresh, and revoke the tokens Appwrite issues to your app through Sign in with Appwrite.
+back: /docs/partners/apps
+---
+
+{% only_dark %}
+
+{% /only_dark %}
+{% only_light %}
+
+{% /only_light %}
+
+Your app holds three tokens with different jobs:
+
+- The **access token** calls APIs.
+- The **refresh token** replaces expired access tokens.
+- The **ID token** proves who signed in.
+
+All three come from one call. After the user approves your app on the consent screen, exchange the authorization code at the token endpoint, sending your client credentials in the request body:
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/token \
+ -H "Content-Type: application/json" \
+ -d '{
+ "grant_type": "authorization_code",
+ "code": "",
+ "redirect_uri": "",
+ "client_id": "",
+ "client_secret": ""
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/token
+{
+ "grant_type": "authorization_code",
+ "code": "",
+ "redirect_uri": "",
+ "client_id": "",
+ "client_secret": ""
+}
+```
+{% /multicode %}
+
+The response carries all three tokens, along with what the user granted:
+
+```json
+{
+ "access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiJ9...",
+ "token_type": "Bearer",
+ "expires_in": 28800,
+ "refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
+ "scope": "openid profile email project:databases.read",
+ "authorization_details": [{ "type": "project", "identifiers": ["6a357f7e001c7237296b"] }],
+ "id_token": "eyJhbGciOiJSUzI1NiJ9..."
+}
+```
+
+# Public clients {% #public-clients %}
+
+Mobile apps, desktop apps, and SPAs cannot keep a client secret, so they register as [public clients](/docs/partners/apps/registration#client-types) and prove themselves with PKCE ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)) instead. Before redirecting the user, generate a random `code_verifier`, hash it with SHA-256, and send the hash along in the authorization request:
+
+```text
+https://cloud.appwrite.io/v1/oauth2/console/authorize
+ ?client_id=
+ &redirect_uri=
+ &response_type=code
+ &scope=openid profile email project:databases.read
+ &code_challenge=
+ &code_challenge_method=S256
+```
+
+The challenge is not optional: a public client authorizing without one is redirected back with `error=invalid_request`. At the token endpoint, the original verifier replaces the client secret:
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/token \
+ -H "Content-Type: application/json" \
+ -d '{
+ "grant_type": "authorization_code",
+ "code": "",
+ "redirect_uri": "",
+ "client_id": "",
+ "code_verifier": ""
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/token
+{
+ "grant_type": "authorization_code",
+ "code": "",
+ "redirect_uri": "",
+ "client_id": "",
+ "code_verifier": ""
+}
+```
+{% /multicode %}
+
+The response carries the same three tokens, on shorter leases: access tokens last 1 hour and refresh tokens 30 days, against 8 hours and 365 days for confidential clients.
+
+# Access tokens {% #access-tokens %}
+
+The access token is a JWT of type `at+jwt` ([RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068)), signed with `RS256`. Its claims carry the whole grant:
+
+- `sub`: the user who signed in.
+- `client_id`: your app.
+- `scope`: the granted scopes.
+- `authorization_details`: the granted projects and organizations.
+- The standard JWT claims:
+ - `iss`: who issued the token, always `https://cloud.appwrite.io/v1/oauth2/console`.
+ - `aud`: who the token is for, the Appwrite API.
+ - `exp`: when the token expires, as a Unix timestamp.
+ - `iat`: when the token was issued.
+ - `jti`: a unique ID for this token.
+ - `auth_time`: when the user last authenticated.
+
+Decoded, the payload reads:
+
+```json
+{
+ "iss": "https://cloud.appwrite.io/v1/oauth2/console",
+ "aud": ["https://cloud.appwrite.io/v1/console"],
+ "sub": "6a150ace003bc4c2919e",
+ "client_id": "horizon",
+ "scope": "openid profile email project:databases.read",
+ "authorization_details": [{ "type": "project", "identifiers": ["*"] }],
+ "exp": 1784832895,
+ "iat": 1784804095,
+ "jti": "d0fe924a1fe6113ba1bab1eaf631603d",
+ "auth_time": 1784803731,
+ "tokenId": "6a61f2f3c94806401116"
+}
+```
+
+Pass it as a bearer token in the `Authorization` header. The userinfo endpoint is the simplest call to try it on:
+
+{% multicode %}
+```curl
+curl https://cloud.appwrite.io/v1/oauth2/console/userinfo \
+ -H "Authorization: Bearer "
+```
+
+```hurl
+GET https://cloud.appwrite.io/v1/oauth2/console/userinfo
+Authorization: Bearer
+```
+{% /multicode %}
+
+The response is the signed-in user:
+
+```json
+{
+ "sub": "6a150ace003bc4c2919e",
+ "name": "Walter O'Brien",
+ "email": "walter@example.com",
+ "email_verified": true,
+ "updated_at": 1784707579
+}
+```
+
+The same token works in two places:
+
+- The [listing endpoints](/docs/partners/apps/scopes#targets-granted), to see which projects and organizations the user granted.
+- Every granted project's API, whatever region the project lives in.
+
+Your app never handles regions itself. Take each project's `endpoint` from the listing, call it, and send the same token.
+
+Access tokens last 8 hours for confidential clients and 1 hour for public ones.
+
+# Refresh tokens {% #refresh-tokens %}
+
+When the access token expires, exchange the refresh token for a new pair:
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/token \
+ -H "Content-Type: application/json" \
+ -d '{
+ "grant_type": "refresh_token",
+ "refresh_token": "",
+ "client_id": "",
+ "client_secret": ""
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/token
+{
+ "grant_type": "refresh_token",
+ "refresh_token": "",
+ "client_id": "",
+ "client_secret": ""
+}
+```
+{% /multicode %}
+
+The response is a fresh pair, in the same shape as the original exchange:
+
+```json
+{
+ "access_token": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiJ9...",
+ "token_type": "Bearer",
+ "expires_in": 28800,
+ "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+ "scope": "openid profile email project:databases.read",
+ "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
+}
+```
+
+Refresh tokens are single-use. Each exchange invalidates the token you sent and returns a new one, so store the new token as soon as the response arrives.
+
+This is deliberate. A refresh token that gets used twice looks like theft, and Appwrite responds by killing the entire token family. The user has to authorize your app again. The most common way to trip this by accident is a crash between refreshing and saving, after which your app retries with the token it already spent.
+
+Refresh tokens last 365 days for confidential clients and 30 days for public ones. Treat them like passwords and store them encrypted.
+
+# ID tokens {% #id-tokens %}
+
+The ID token is an OpenID Connect JWT, issued when the grant includes `openid`. It answers one question: who signed in. Decoded, it looks like this:
+
+```json
+{
+ "iss": "https://cloud.appwrite.io/v1/oauth2/console",
+ "sub": "6a150ace003bc4c2919e",
+ "aud": "horizon",
+ "name": "Walter O'Brien",
+ "email": "walter@example.com",
+ "email_verified": true,
+ "updated_at": 1784707579,
+ "auth_time": 1784617272,
+ "iat": 1784711196,
+ "exp": 1784739996,
+ "at_hash": "ro6WSzsVqgrWqQy4m7_DGA"
+}
+```
+
+The granted scopes decide which of these fields the token carries:
+
+- `profile` adds `name` and `updated_at`.
+- `email` adds `email` and `email_verified`.
+- `phone` adds `phone_number` and `phone_number_verified`.
+
+Never use the ID token to call APIs; it only tells your app who signed in, and Appwrite rejects it as a bearer token. To fetch fresh values for the same fields, call the userinfo endpoint with the access token.
+
+# Validating tokens {% #validation %}
+
+Your app never needs to check a token before using it. Every Appwrite API validates the bearer token on each call and rejects anything expired or revoked, so the normal move is to send the token and handle the `401`.
+
+Decoding the token still helps in two places:
+
+- **UX**: read `exp` locally and send the user to sign-in the moment the token expires, instead of letting a request fail first.
+- **Your own API**: if your backend accepts Appwrite access tokens from its clients, verify them like any JWT against the published keys:
+
+```text
+https://cloud.appwrite.io/v1/oauth2/console/.well-known/jwks.json
+```
+
+Check the signature, the `iss` claim, the audience, the expiry, and require the `at+jwt` type so an ID token can never pass as an access token.
+
+Signature checks cannot see revocation, so your own API accepts a revoked token until it expires; the short access token lifetime bounds that window. Appwrite's own APIs reject revoked tokens the moment revocation happens.
+
+# Revocation {% #revocation %}
+
+Revoke tokens when a user disconnects your app:
+
+{% multicode %}
+```curl
+curl -X POST https://cloud.appwrite.io/v1/oauth2/console/revoke \
+ -H "Content-Type: application/json" \
+ -d '{
+ "token": "",
+ "client_id": "",
+ "client_secret": ""
+ }'
+```
+
+```hurl
+POST https://cloud.appwrite.io/v1/oauth2/console/revoke
+{
+ "token": "",
+ "client_id": "",
+ "client_secret": ""
+}
+```
+{% /multicode %}
+
+The response is an empty `200`, whether or not the token existed, as [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009) requires. Revoking either token of a pair invalidates both.
+
+Revocation also arrives from outside your app. Users revoke from their account applications page, and reuse detection tears tokens down on its own. Treat a `401` with a previously working token as the signal to re-authorize, not to retry.
diff --git a/static/images/docs/partners/apps/account-applications.avif b/static/images/docs/partners/apps/account-applications.avif
new file mode 100644
index 0000000000..2916460af6
Binary files /dev/null and b/static/images/docs/partners/apps/account-applications.avif differ
diff --git a/static/images/docs/partners/apps/consent-screen.avif b/static/images/docs/partners/apps/consent-screen.avif
new file mode 100644
index 0000000000..f5755bb120
Binary files /dev/null and b/static/images/docs/partners/apps/consent-screen.avif differ
diff --git a/static/images/docs/partners/apps/dark/account-applications.avif b/static/images/docs/partners/apps/dark/account-applications.avif
new file mode 100644
index 0000000000..b22d22b1d3
Binary files /dev/null and b/static/images/docs/partners/apps/dark/account-applications.avif differ
diff --git a/static/images/docs/partners/apps/dark/consent-screen.avif b/static/images/docs/partners/apps/dark/consent-screen.avif
new file mode 100644
index 0000000000..dda846de1c
Binary files /dev/null and b/static/images/docs/partners/apps/dark/consent-screen.avif differ
diff --git a/static/images/docs/partners/apps/dark/device-code.avif b/static/images/docs/partners/apps/dark/device-code.avif
new file mode 100644
index 0000000000..eba213c961
Binary files /dev/null and b/static/images/docs/partners/apps/dark/device-code.avif differ
diff --git a/static/images/docs/partners/apps/dark/diagram-consent.avif b/static/images/docs/partners/apps/dark/diagram-consent.avif
new file mode 100644
index 0000000000..a96625756d
Binary files /dev/null and b/static/images/docs/partners/apps/dark/diagram-consent.avif differ
diff --git a/static/images/docs/partners/apps/dark/diagram-device.avif b/static/images/docs/partners/apps/dark/diagram-device.avif
new file mode 100644
index 0000000000..3652f071d4
Binary files /dev/null and b/static/images/docs/partners/apps/dark/diagram-device.avif differ
diff --git a/static/images/docs/partners/apps/dark/diagram-overview.avif b/static/images/docs/partners/apps/dark/diagram-overview.avif
new file mode 100644
index 0000000000..60f201d29d
Binary files /dev/null and b/static/images/docs/partners/apps/dark/diagram-overview.avif differ
diff --git a/static/images/docs/partners/apps/dark/diagram-scopes.avif b/static/images/docs/partners/apps/dark/diagram-scopes.avif
new file mode 100644
index 0000000000..cf5737d19d
Binary files /dev/null and b/static/images/docs/partners/apps/dark/diagram-scopes.avif differ
diff --git a/static/images/docs/partners/apps/dark/diagram-tokens.avif b/static/images/docs/partners/apps/dark/diagram-tokens.avif
new file mode 100644
index 0000000000..74a5e93869
Binary files /dev/null and b/static/images/docs/partners/apps/dark/diagram-tokens.avif differ
diff --git a/static/images/docs/partners/apps/dark/quick-start-consent.avif b/static/images/docs/partners/apps/dark/quick-start-consent.avif
new file mode 100644
index 0000000000..574014a40d
Binary files /dev/null and b/static/images/docs/partners/apps/dark/quick-start-consent.avif differ
diff --git a/static/images/docs/partners/apps/dark/quick-start-provider-setup.avif b/static/images/docs/partners/apps/dark/quick-start-provider-setup.avif
new file mode 100644
index 0000000000..4daf8904de
Binary files /dev/null and b/static/images/docs/partners/apps/dark/quick-start-provider-setup.avif differ
diff --git a/static/images/docs/partners/apps/dark/registration-general.avif b/static/images/docs/partners/apps/dark/registration-general.avif
new file mode 100644
index 0000000000..67e3c1530e
Binary files /dev/null and b/static/images/docs/partners/apps/dark/registration-general.avif differ
diff --git a/static/images/docs/partners/apps/dark/registration-legal.avif b/static/images/docs/partners/apps/dark/registration-legal.avif
new file mode 100644
index 0000000000..a44fbf077f
Binary files /dev/null and b/static/images/docs/partners/apps/dark/registration-legal.avif differ
diff --git a/static/images/docs/partners/apps/dark/registration-marketplace.avif b/static/images/docs/partners/apps/dark/registration-marketplace.avif
new file mode 100644
index 0000000000..f16bd337a4
Binary files /dev/null and b/static/images/docs/partners/apps/dark/registration-marketplace.avif differ
diff --git a/static/images/docs/partners/apps/dark/registration-oauth-client.avif b/static/images/docs/partners/apps/dark/registration-oauth-client.avif
new file mode 100644
index 0000000000..819a4fb64a
Binary files /dev/null and b/static/images/docs/partners/apps/dark/registration-oauth-client.avif differ
diff --git a/static/images/docs/partners/apps/dark/registration-publish.avif b/static/images/docs/partners/apps/dark/registration-publish.avif
new file mode 100644
index 0000000000..a71d29bb4b
Binary files /dev/null and b/static/images/docs/partners/apps/dark/registration-publish.avif differ
diff --git a/static/images/docs/partners/apps/dark/registration-secrets.avif b/static/images/docs/partners/apps/dark/registration-secrets.avif
new file mode 100644
index 0000000000..7eafc1cfc6
Binary files /dev/null and b/static/images/docs/partners/apps/dark/registration-secrets.avif differ
diff --git a/static/images/docs/partners/apps/dark/registration-support.avif b/static/images/docs/partners/apps/dark/registration-support.avif
new file mode 100644
index 0000000000..e72937c95d
Binary files /dev/null and b/static/images/docs/partners/apps/dark/registration-support.avif differ
diff --git a/static/images/docs/partners/apps/device-code.avif b/static/images/docs/partners/apps/device-code.avif
new file mode 100644
index 0000000000..56cd50b08c
Binary files /dev/null and b/static/images/docs/partners/apps/device-code.avif differ
diff --git a/static/images/docs/partners/apps/diagram-consent.avif b/static/images/docs/partners/apps/diagram-consent.avif
new file mode 100644
index 0000000000..5138ef57ee
Binary files /dev/null and b/static/images/docs/partners/apps/diagram-consent.avif differ
diff --git a/static/images/docs/partners/apps/diagram-device.avif b/static/images/docs/partners/apps/diagram-device.avif
new file mode 100644
index 0000000000..e7580a0fd7
Binary files /dev/null and b/static/images/docs/partners/apps/diagram-device.avif differ
diff --git a/static/images/docs/partners/apps/diagram-overview.avif b/static/images/docs/partners/apps/diagram-overview.avif
new file mode 100644
index 0000000000..fbf2483f6d
Binary files /dev/null and b/static/images/docs/partners/apps/diagram-overview.avif differ
diff --git a/static/images/docs/partners/apps/diagram-scopes.avif b/static/images/docs/partners/apps/diagram-scopes.avif
new file mode 100644
index 0000000000..b19481b1f7
Binary files /dev/null and b/static/images/docs/partners/apps/diagram-scopes.avif differ
diff --git a/static/images/docs/partners/apps/diagram-tokens.avif b/static/images/docs/partners/apps/diagram-tokens.avif
new file mode 100644
index 0000000000..7aa81b2e30
Binary files /dev/null and b/static/images/docs/partners/apps/diagram-tokens.avif differ
diff --git a/static/images/docs/partners/apps/quick-start-consent.avif b/static/images/docs/partners/apps/quick-start-consent.avif
new file mode 100644
index 0000000000..0a3fc7e3b7
Binary files /dev/null and b/static/images/docs/partners/apps/quick-start-consent.avif differ
diff --git a/static/images/docs/partners/apps/quick-start-provider-setup.avif b/static/images/docs/partners/apps/quick-start-provider-setup.avif
new file mode 100644
index 0000000000..9dd61da651
Binary files /dev/null and b/static/images/docs/partners/apps/quick-start-provider-setup.avif differ
diff --git a/static/images/docs/partners/apps/registration-general.avif b/static/images/docs/partners/apps/registration-general.avif
new file mode 100644
index 0000000000..8c67d6d7a3
Binary files /dev/null and b/static/images/docs/partners/apps/registration-general.avif differ
diff --git a/static/images/docs/partners/apps/registration-legal.avif b/static/images/docs/partners/apps/registration-legal.avif
new file mode 100644
index 0000000000..1adbafc1a8
Binary files /dev/null and b/static/images/docs/partners/apps/registration-legal.avif differ
diff --git a/static/images/docs/partners/apps/registration-marketplace.avif b/static/images/docs/partners/apps/registration-marketplace.avif
new file mode 100644
index 0000000000..2243b76deb
Binary files /dev/null and b/static/images/docs/partners/apps/registration-marketplace.avif differ
diff --git a/static/images/docs/partners/apps/registration-oauth-client.avif b/static/images/docs/partners/apps/registration-oauth-client.avif
new file mode 100644
index 0000000000..87ded977b4
Binary files /dev/null and b/static/images/docs/partners/apps/registration-oauth-client.avif differ
diff --git a/static/images/docs/partners/apps/registration-publish.avif b/static/images/docs/partners/apps/registration-publish.avif
new file mode 100644
index 0000000000..65303f5eb7
Binary files /dev/null and b/static/images/docs/partners/apps/registration-publish.avif differ
diff --git a/static/images/docs/partners/apps/registration-secrets.avif b/static/images/docs/partners/apps/registration-secrets.avif
new file mode 100644
index 0000000000..caadee0041
Binary files /dev/null and b/static/images/docs/partners/apps/registration-secrets.avif differ
diff --git a/static/images/docs/partners/apps/registration-support.avif b/static/images/docs/partners/apps/registration-support.avif
new file mode 100644
index 0000000000..b6726b0070
Binary files /dev/null and b/static/images/docs/partners/apps/registration-support.avif differ