Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .optimize-cache.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "03c2b9a8aef562caf6b491ddb22626c405e64f2e6ac83525e28e6537f3f927a5",
"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",
Expand Down
287 changes: 287 additions & 0 deletions src/routes/blog/post/announcing-user-impersonation/+page.markdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
---
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-14
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://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>')
.setImpersonateUserId('<TARGET_USER_ID>');

const account = new Account(client);
const user = await account.get();
```

```client-flutter
import 'package:appwrite/appwrite.dart';

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

Account account = Account(client);
final user = await account.get();
```

```client-apple
import Appwrite

let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")
.setImpersonateUserId("<TARGET_USER_ID>")

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://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")
.setImpersonateUserId("<TARGET_USER_ID>")

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://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>')
.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://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>')
.setImpersonateUserEmail('user@example.com');

Account account = Account(client);
final user = await account.get();
```

```client-apple
import Appwrite

let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")
.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://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")
.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://<REGION>.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>')
.setImpersonateUserPhone('+12065550100');

const account = new Account(client);
const user = await account.get();
```

```client-flutter
import 'package:appwrite/appwrite.dart';

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

Account account = Account(client);
final user = await account.get();
```

```client-apple
import Appwrite

let client = Client()
.setEndpoint("https://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")
.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://<REGION>.cloud.appwrite.io/v1")
.setProject("<PROJECT_ID>")
.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 is intentionally designed to work from a **real user-authenticated session**, not from requests authenticated with your **server integration** (project API key) alone.

That means operators still authenticate as themselves first. Appwrite then uses the impersonation setting on the client only to select the effective user for the request. This keeps the flow explicit and better aligned with internal support tooling, admin panels, and controlled QA environments.

# 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)
Loading
Loading