diff --git a/.optimize-cache.json b/.optimize-cache.json index 5d4e74e958..75557daa9f 100644 --- a/.optimize-cache.json +++ b/.optimize-cache.json @@ -191,6 +191,7 @@ "images/blog/announcing-time-helper-queries/cover.png": "0ee1d4d1edc65bf8fc3376b761b08efaffa55dd8ca84860ab3a9c34f7d78c25b", "images/blog/announcing-timestamp-overrides/cover.png": "5bfc2ba16b8ca4a82188c0f67b300ed0a7f38b4abc04b06a10ee52b2832fa65b", "images/blog/announcing-transactions-api/cover.png": "604a7721b7bf0a752460a721ffcaff10598abc5f398e7b16a8a58195c2ebf7ea", + "images/blog/announcing-user-impersonation/cover.png": "0d637f5d3a748e033945741a9e926a001b84c51854077ac5de4fedbc8801a40a", "images/blog/apply-appwrite-how/cover.png": "d23f45ced245b42c8712c021f5d2068c17aebd94fd049cb90222cb9647a41a4a", "images/blog/appwrite-1-8-0-self-hosted-release/cover.png": "c15a9d88ccd16c2dc8333dc74e715e1f4a6c7818d3b4a05f4d68342eacdc0523", "images/blog/appwrite-1-8-1-self-hosted-release/cover.png": "82f0a396c56b6b299b24133079acc6a317c66b2bf02fd91f4862bd3be0f8f373", diff --git a/src/routes/blog/post/announcing-user-impersonation/+page.markdoc b/src/routes/blog/post/announcing-user-impersonation/+page.markdoc new file mode 100644 index 0000000000..0bccdbbb5e --- /dev/null +++ b/src/routes/blog/post/announcing-user-impersonation/+page.markdoc @@ -0,0 +1,294 @@ +--- +layout: post +title: 'Introducing user impersonation for Appwrite Auth' +description: Trusted operators can now act as another user in Appwrite Auth to debug issues, validate permissions, and support customers without sharing credentials. +date: 2026-03-30 +cover: /images/blog/announcing-user-impersonation/cover.png +timeToRead: 4 +author: eldad-fux +category: announcement, product +featured: false +--- + +Debugging user-specific issues is one of the hardest parts of building authentication-heavy apps. + +You get a bug report that only happens for one customer. A teammate needs to confirm whether a permission is misconfigured. Support wants to walk through the exact experience an end user is seeing. Until now, that usually meant asking for screenshots, requesting temporary credentials, or trying to recreate the entire account setup by hand. + +Appwrite now supports **user impersonation** in Auth. + +With impersonation, a trusted operator can sign in as themselves, choose a target user, and send subsequent requests using that user's permissions and access level. That makes it much easier to troubleshoot production issues, validate access rules, and review critical flows exactly as the user experiences them. + +# Simpler than previous impersonation flows + +This kind of workflow was already possible before if you were willing to build it yourself with **Server SDKs**, **SSR session handling**, or custom internal tooling. + +Teams could create operator-specific flows that acted on behalf of users, pass sessions into a server-side client, and proxy requests through backend code. That approach worked, but it also meant more plumbing around session handling, more support-only code paths, and more room for mistakes in internal tools. + +With native impersonation support, the flow becomes much simpler: + +- Mark a trusted operator as an impersonator in the Console or through the Users API +- Have them authenticate as themselves +- Set the impersonation target directly on the Appwrite client +- Use the same SDKs and product APIs you already use everywhere else + +Instead of building the entire mechanism around impersonation, you can focus on the operator experience around it. + +# What ships with this release + +This release adds the building blocks needed to support impersonation safely inside Appwrite: + +- A new `impersonator` boolean on the user model +- A dedicated endpoint to enable or disable impersonation for a user from any of the server SDKs +- Console support so trusted operators can be marked as impersonators directly from **Auth > Users** +- Automatic `users.read` scope for impersonators so they can browse project users and build internal user-pickers from either client or server side +- Three client setters for selecting the target user by ID, email, or phone +- Response models that expose `impersonatorUserId` when impersonation is active + +In practice, the flow is simple: + +1. Mark a trusted operator as an impersonator. +2. Have them sign in normally using Appwrite Auth. +3. Set one impersonation target value on the client using the new client SDK methods. +4. Appwrite resolves the target user and evaluates the request as that user. + +# Why this matters + +Impersonation removes a lot of friction from real-world support and operations work. + +Here are a few scenarios where it helps immediately: + +- **Customer support**: Reproduce account-specific issues without asking users to share passwords. +- **QA and testing**: Confirm exactly what different users can and cannot access. +- **Permissions debugging**: Validate role, team, or label-based behavior from the user's point of view. +- **Onboarding reviews**: Walk through signup, billing, or upgrade flows as a real user would see them. +- **High-touch enterprise support**: Investigate tenant-specific issues without sharing temporary logins across teams. +- **Internal release validation**: Test new rollout logic against real-world user configurations before support tickets arrive. + +Instead of rebuilding the environment from scratch or guessing what a user is seeing, your team can verify the behavior directly. + +Another practical detail: once a user is marked as an impersonator, Appwrite automatically grants them the `users.read` scope. That means your internal support or operations tools can show a full project user list, add search, and let operators choose a target user from an admin-like UI before they start impersonating. + +# How it looks in code + +Once the operator has signed in as themselves, impersonation becomes a small client configuration step. + +# By user ID + +{% multicode %} + +```client-web +import { Client, Account } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setImpersonateUserId(''); + +const account = new Account(client); +const user = await account.get(); + +if (user.impersonatorUserId) { + console.log(`Impersonating as ${user.name}`); +} +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setImpersonateUserId(''); + +Account account = Account(client); +final user = await account.get(); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setImpersonateUserId("") + +let account = Account(client) +let user = try await account.get() +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setImpersonateUserId("") + +val account = Account(client) +val user = account.get() +``` + +{% /multicode %} + +# By email + +{% multicode %} + +```client-web +import { Client, Account } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setImpersonateUserEmail('user@example.com'); + +const account = new Account(client); +const user = await account.get(); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setImpersonateUserEmail('user@example.com'); + +Account account = Account(client); +final user = await account.get(); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setImpersonateUserEmail("user@example.com") + +let account = Account(client) +let user = try await account.get() +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setImpersonateUserEmail("user@example.com") + +val account = Account(client) +val user = account.get() +``` + +{% /multicode %} + +# By phone + +{% multicode %} + +```client-web +import { Client, Account } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setImpersonateUserPhone('+12065550100'); + +const account = new Account(client); +const user = await account.get(); +``` + +```client-flutter +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') + .setProject('') + .setImpersonateUserPhone('+12065550100'); + +Account account = Account(client); +final user = await account.get(); +``` + +```client-apple +import Appwrite + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setImpersonateUserPhone("+12065550100") + +let account = Account(client) +let user = try await account.get() +``` + +```client-android-kotlin +import io.appwrite.Client +import io.appwrite.services.Account + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject("") + .setImpersonateUserPhone("+12065550100") + +val account = Account(client) +val user = account.get() +``` + +{% /multicode %} + +That means support and QA teams can keep using the same Appwrite SDK patterns they already know, while switching the effective user only when needed. + +# Built for controlled use + +Impersonation requires a **real user-authenticated session**. An API key alone is not enough. Operators must sign in as themselves first, and Appwrite uses that session to verify who is performing the impersonation before resolving the target user. + +A few constraints worth knowing: + +- Only one impersonation target can be active per request. To switch targets, replace the impersonation value on the client or create a fresh one. +- Each operator should have their own dedicated account rather than sharing a single support login. This keeps audit activity attributable to individuals and makes it straightforward to revoke access when needed. + +# How logs are treated + +We also added clear behavior around auditability and visibility: + +- Users with the impersonator capability automatically receive `users.read`, so they can list project users before choosing a target +- The user model includes `impersonatorUserId` when impersonation is active +- Internal audit logs continue to attribute actions to the **original impersonator** +- Internal audit payload data also records the **impersonated target** + +This matters because support and operations teams usually need two answers at the same time: + +1. Who actually performed the action? +2. Which user experience was being impersonated? + +With this release, Appwrite keeps the real actor in internal audits while still preserving the effective target in the audit payload. That makes it much easier to investigate incidents, review support activity, and keep internal tooling accountable. + +On the app side, you can also use `impersonatorUserId` to show a persistent banner whenever someone is currently acting on behalf of another user. + +# How to get started + +To enable impersonation for an operator, you can use the Appwrite Console under **Auth > Users** or the Users API reference flow for updating the impersonator capability. + +After that, authenticated requests from that user can impersonate another user by setting one of the impersonation helpers on the Appwrite client: + +- `setImpersonateUserId()` +- `setImpersonateUserEmail()` +- `setImpersonateUserPhone()` + +If you're building an internal support dashboard, a good pattern is: + +- Let admins search for users +- Start impersonation only after an explicit action +- Show a visible banner while impersonation is active +- Make it easy to stop impersonating and return to the operator's own session + +# More resources + +- [Read the user impersonation docs](/docs/products/auth/impersonation) +- [Manage users with the Users API](/docs/products/auth/users) +- [Explore Appwrite Authentication](/docs/products/auth) diff --git a/src/routes/changelog/(entries)/2026-03-14.markdoc b/src/routes/changelog/(entries)/2026-03-14.markdoc new file mode 100644 index 0000000000..0e07e2e564 --- /dev/null +++ b/src/routes/changelog/(entries)/2026-03-14.markdoc @@ -0,0 +1,16 @@ +--- +layout: changelog +title: 'User impersonation for Appwrite Auth' +date: 2026-03-14 +cover: /images/blog/announcing-user-impersonation/cover.png +--- + +Trusted operators can now impersonate users in Appwrite Auth to reproduce issues, validate permissions, and provide hands-on support without sharing credentials. + +This release adds a new `impersonator` capability on users, available from the Appwrite Console and the Users API, plus direct SDK support for targeting a user by ID, email, or phone. + +Impersonated requests still begin from a real authenticated user session, and internal audit logs continue to attribute actions to the original impersonator while recording the impersonated target in internal audit payload data. + +{% arrow_link href="/blog/post/announcing-user-impersonation" %} +Read the announcement to learn more +{% /arrow_link %} diff --git a/src/routes/docs/apis/rest/+page.markdoc b/src/routes/docs/apis/rest/+page.markdoc index 656c56acc0..a1f19a42ee 100644 --- a/src/routes/docs/apis/rest/+page.markdoc +++ b/src/routes/docs/apis/rest/+page.markdoc @@ -11,33 +11,65 @@ Appwrite supports multiple protocols for accessing the server, including [REST]( Appwrite's REST APIs expect certain headers to be included with each request: {% table %} + - Header -- +- - Description + --- + - X-Appwrite-Project: [PROJECT-ID] - required - The ID of your Appwrite project + --- + - Content-Type: application/json - required - Content type of the HTTP request. Typically set to `application/json`. + --- + - X-Appwrite-Key: [API-KEY] - optional - API key used for server authentication. Your API key is a secret, **do not** use it in client applications. + --- + - X-Appwrite-JWT: [TOKEN] - optional - Token used for JWT authentication, tokens can be generated using the [Create JWT](/docs/products/auth/jwt) method. + --- + - X-Appwrite-Response-Format: [VERSION-NUMBER] - optional - Version number used for backward compatibility. The response will be formatted to be compatible with the provided version number. This helps Appwrite SDKs keep backward compatibility with Appwrite server API version. + --- + - X-Fallback-Cookies: [FALLBACK-COOKIES] - optional - Fallback cookies used in scenarios where browsers do not allow third-party cookies. Often used when there is no Custom Domain set for your Appwrite API. + +--- + +- X-Appwrite-Impersonate-User-Id: [USER-ID] +- optional +- Resolves the effective user for an already authenticated impersonator request by Appwrite user ID. Only works when the authenticated user has impersonation enabled. + +--- + +- X-Appwrite-Impersonate-User-Email: [EMAIL] +- optional +- Resolves the effective user for an already authenticated impersonator request by email address. Only works when the authenticated user has impersonation enabled. + +--- + +- X-Appwrite-Impersonate-User-Phone: [PHONE] +- optional +- Resolves the effective user for an already authenticated impersonator request by phone number. Only works when the authenticated user has impersonation enabled. + {% /table %} # Authentication {% #authentication %} @@ -101,6 +133,22 @@ X-Appwrite-Project: X-Appwrite-JWT: [TOKEN] ``` +## Impersonation headers {% #impersonation-headers %} + +When a request is already authenticated as a user with impersonation enabled, you can add one of Appwrite's impersonation headers to resolve a different effective user for that request. + +Use exactly one of these headers: + +- `X-Appwrite-Impersonate-User-Id` +- `X-Appwrite-Impersonate-User-Email` +- `X-Appwrite-Impersonate-User-Phone` + +These headers are ignored for plain API key requests. They are only honored when the request already belongs to a signed-in user who has been marked as an impersonator in the Appwrite Console or through the Users API. + +If you are using an Appwrite SDK instead of raw REST calls, use the corresponding client setters rather than manually attaching these headers. + +Learn more in the [user impersonation docs](/docs/products/auth/impersonation). + # Files {% #files %} Appwrite implements resumable, chunked uploads for files larger than 5MB. Chunked uploads send files in chunks of 5MB to reduce memory footprint and increase resilience when handling large files. [Appwrite SDKs](/docs/sdks) will automatically handle chunked uploads, but it is possible to implement this with the REST API directly. @@ -108,57 +156,77 @@ Appwrite implements resumable, chunked uploads for files larger than 5MB. Chunke Upload endpoints in Appwrite, such as [Create File](/docs/references/cloud/client-web/storage#createFile) and [Create Deployment](/docs/references/cloud/server-nodejs/functions#createDeployment), are different from other endpoints. These endpoints take multipart form data instead of JSON data. To implement chunked uploads, you will need to implement the following headers. If you wish, this logic is already available in any of the [Appwrite SDKs](/docs/sdks). {% table %} + - Header -- +- - Description + --- + - X-Appwrite-Project: [PROJECT-ID] - required - The ID of your Appwrite project + --- + - Content-Type: multipart/form-data; boundary=[FORM-BOUNDARY] - required - Contains the content type of the HTTP request and provides a [boundary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) that is used to parse the form data. + --- + - Content-Range: bytes [BYTE-RANGE] - required - Contains information about which bytes are being transmitted in this chunk, with the format `[FIRST-BYTE]-[LAST-BYTE]/[TOTAL-BYTES]`. + --- + - X-Appwrite-ID: [FILE-ID] - required - Contains ID of the file this chunk belongs to. + --- + - X-Appwrite-Key: [API-KEY] - optional - API key used for server authentication. Your API key is a secret, **do not** use it in client applications. + {% /table %} The multipart form data is structured as follows: {% table %} + - Key -- +- - Value - File Name - Description + --- + - fileId - optional - [FILE-ID] - N/A - Contains the file ID of the new file. Only used by file chunks following the first chunk uploaded. + --- + - file - required - [CHUNK-DATA] - [FILE-NAME] - Contains file chunk data. + --- + - permissions - required - [PERMISSION ARRAY] - N/A - Contains an array of permission strings about who can access the new file. + {% /table %} While cURL and fetch are great tools to explore other REST endpoints, it's impractical to use for chunked file uploads because you need to split files into chunks. @@ -201,23 +269,35 @@ Some use cases do not allow custom headers, such as embedding images from Appwri Appwrite SDKs have helpers to generate permission string formats, but when using Appwrite without SDKs, you'd need to create the strings yourself. {% table %} + - Query method - API string + --- + - `Permission.read()` - `read("")` + --- + - `Permission.create()` - `read("")` + --- + - `Permission.update()` - `update("")` + --- + - `Permission.delete()` - `delete("")` + --- + - `Permission.write()` - `write("")` + {% /table %} ## Roles {% #roles %} @@ -225,35 +305,55 @@ Appwrite SDKs have helpers to generate permission string formats, but when using Appwrite SDKs have helpers to generate roles string formats, but when using Appwrite without SDKs, you'd need to create the strings yourself. {% table %} + - Role method - API string + --- + - `Role.any()` - `any` + --- + - `Role.guests()` - `guests` + --- + - `Role.users()` - `users` + --- + - `Role.users([STATUS])` - `users/[STATUS]` + --- + - `Role.user([USER_ID])` - `user:[USER_ID]` + --- + - `Role.user([USER_ID], [STATUS])` - `user:[USER_ID]/[STATUS]` + --- + - `Role.team([TEAM_ID])` - `team:[TEAM_ID]` + --- + - `Role.team([TEAM_ID], [ROLE])` - `team:[TEAM_ID]/[ROLE]` + --- + - `Role.member([MEMBERSHIP_ID])` - `member:[MEMBERSHIP_ID]` + {% /table %} # Unique ID {% #unique-id %} @@ -266,7 +366,7 @@ Appwrite's SDKs provide a `Query` class to generate JSON query strings. When using Appwrite without an SDK, you can template your own JSON strings. You can discover the query methods available in the [Queries page.](/docs/products/databases/queries) -## Query string format {% #queries-string-format %} +## Query string format {% #queries-string-format %} Appwrite Queries are escaped JSON strings, which look like this. @@ -274,10 +374,11 @@ Appwrite Queries are escaped JSON strings, which look like this. "{\"method\":\"equal\",\"column\":\"name\",\"values\":[\"John\"]}" ``` -Query strings are passed to Appwrite using the `queries` parameter. +Query strings are passed to Appwrite using the `queries` parameter. You can attach multiple query strings by including the array parameter multiple times in the query string: `queries[]="..."&queries[]="..."` For example, the unescaped query string might look like this. + ```text ?queries[0]={"method":"equal","column":"name","values":["John"]}&queries[1]={"method":"limit","values":[6]} ``` @@ -305,16 +406,13 @@ For example, to query for all rows with the name "John" or "Jane", the query str ```json { - "method": "equal", - "column": "name", - "values": [ - "John", - "Jane" - ] + "method": "equal", + "column": "name", + "values": ["John", "Jane"] } ``` -Here are some more examples of the JSON query format. +Here are some more examples of the JSON query format. When in doubt, you can use the Appwrite SDKs to generate the query strings for you. ```json @@ -338,26 +436,27 @@ When in doubt, you can use the Appwrite SDKs to generate the query strings for y ``` ## Query nesting {% #query-nesting %} -Some Appwrite query methods, like `and` and `or`, allow you to nest queries. + +Some Appwrite query methods, like `and` and `or`, allow you to nest queries. When using Appwrite without an SDK, you can template your own JSON strings. In these cases, `column` is empty and `values` is an array of queries. ```json { - "method": "and", - "values": [ - { - "method": "equal", - "column": "name", - "values": ["John"] - }, - { - "method": "between", - "column": "age", - "values": [20, 30] - } - ] + "method": "and", + "values": [ + { + "method": "equal", + "column": "name", + "values": ["John"] + }, + { + "method": "between", + "column": "age", + "values": [20, 30] + } + ] } ``` diff --git a/src/routes/docs/products/auth/+layout.svelte b/src/routes/docs/products/auth/+layout.svelte index ed0fa99b64..418e2a3793 100644 --- a/src/routes/docs/products/auth/+layout.svelte +++ b/src/routes/docs/products/auth/+layout.svelte @@ -1,6 +1,7 @@