Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .optimize-cache.json
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,15 @@
"images/blog/understand-oauth2/cover.png": "f263e8dae70606276f8bba28b74a2521645cf45c969b91b4fd9975d917e050f0",
"images/blog/understanding-idp-vs-sp-initiated-sso/cover.png": "2a01d6d18f165d0d684dfa3d4bb5acd4712b2ed6a62e87a46025f264756c058e",
"images/blog/user-authentication-guide/cover.png": "b30435919392cb55056d5e82fb7fa64ed1716afcc05507abf5b1b63a16dbc91b",
"images/blog/user-impersonation-tutorial/cover.png": "838e47901ff914e4d1d77a0e994ab6efdb0d43fe23d9325817e3df17e2721c1c",
"images/blog/user-impersonation-tutorial/demo-dashboard.png": "e5f8d940207f0d3f4f06e16b10311d1b2294f95e65fb055d5c9ed4cc45cfd037",
"images/blog/user-impersonation-tutorial/demo-impersonating-note.png": "50bfc457863ab9f08885f4573ba97395fd36b7cb28735c9d7bc06b11e32e623e",
"images/blog/user-impersonation-tutorial/demo-impersonating.png": "a33348d28c67c5405d79dfe2dad448a77edb7c535fee6f04ac610b339187af10",
"images/blog/user-impersonation-tutorial/demo-login.png": "77ab384fd692fb5b648d507ebdb785c11ba30814aee0b1e1340466fbae744c70",
"images/blog/user-impersonation-tutorial/demo-users.png": "7d2bb5df84e9715a360f8d618615010e85ba4f8d158768096ebc07f004595492",
"images/blog/user-impersonation-tutorial/impersonator-toggle.png": "e7d3e0670b30d9fc988b6fa5fd875862776c5b685f46d2d80e10a8b51fc2cb02",
"images/blog/user-impersonation-tutorial/project-overview.png": "80e9c05ac34a4725bbf77dd7068e44a947b34ca53b6984ccfe3cdfcf9ccc757a",
"images/blog/user-impersonation-tutorial/table-permissions.png": "8006587edcb02c3c11326e2415ebe4190720816067a27d35bf50770a77b3d0de",
"images/blog/user-role-guests-missing-scope-account/cover.png": "4e2407b36d1975eec9ae211861df6b5841d00df52ff7d4709b599eab11d7151d",
"images/blog/using-nextjs-wrong/cover.png": "52805acd1a6a7107d71896271a480dd8608ebdadbc62f9d80a072310f71a8c10",
"images/blog/valentines-day-sonnet-generator/cover.png": "0534103f14d66efee62b6953b4b4ac0bf586891135f04201b4142cb8f846b56a",
Expand Down
286 changes: 286 additions & 0 deletions src/routes/blog/post/user-impersonation-tutorial/+page.markdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
---
layout: post
title: Build a notes app with user impersonation
description: Learn how to build a notes app where an admin can impersonate users to view and manage their private data using Appwrite's user impersonation feature.
date: 2026-03-31
cover: /images/blog/user-impersonation-tutorial/cover.png
timeToRead: 12
author: atharva
category: tutorial, product
featured: false
---

User impersonation lets a trusted operator act as another user without sharing credentials. It is useful when your support team needs to debug a user-specific issue, verify what someone sees with their permissions, or manage data on their behalf.

In this tutorial, you will build a notes app where an admin with the impersonator capability can browse all users, impersonate any of them, and view or create notes as that user. You will also learn how Appwrite handles this under the hood using request headers, scoped permissions, and audit logging.

# Prerequisites

- An [Appwrite Cloud](https://cloud.appwrite.io) account or a self-hosted Appwrite instance
- [Node.js](https://nodejs.org/) 18+ installed
- Basic knowledge of React and TypeScript

# Set up the Appwrite project

Start by creating a new project in the Appwrite Console. Head to the Console, create a project, and note the **project ID** and **API endpoint** from the overview page.

![Project overview in the Appwrite Console](/images/blog/user-impersonation-tutorial/project-overview.png)

# Create users

Navigate to **Auth → Users** and create four users:

| Name | Email | Password |
| --- | --- | --- |
| Sarah Chen | sarah@demo.test | password123 |
| Alex Rivera | alex@demo.test | password123 |
| Jordan Kim | jordan@demo.test | password123 |
| Walter O'Brien | walter@demo.test | password123 |

Sarah Chen will be the admin with impersonation capability. The other three are regular users.

# Enable impersonation

Open Sarah Chen's user profile and scroll down to the **User impersonation** section. Toggle it on and click **Update**.

![Impersonator toggle enabled for Sarah Chen](/images/blog/user-impersonation-tutorial/impersonator-toggle.png)

When a user is marked as an impersonator, Appwrite automatically grants them the `users.read` scope. This allows them to list all users in the project, which is what makes the "Users" tab in our app possible. Regular users without this capability cannot call the list users endpoint.

# Create the database

Navigate to **Databases** and create a new database called **Notes App** (ID: `notes-app`). Inside it, create a table called **Notes** (ID: `notes`) with the following columns:

| Column | Type | Required |
| --- | --- | --- |
| `title` | text | Yes |
| `content` | text | Yes |
| `color` | text | No |
| `userId` | text | Yes |

# Configure permissions

Go to the table's **Settings** tab. You need two things:

1. Under **Permissions**, add the **Users** role and check only **Create**. This allows any authenticated user to create rows.
2. Under **Row security**, enable the toggle. This ensures each row's permissions are respected individually.

With this setup, when a user creates a note through the Client SDK, Appwrite automatically grants that user read, update, and delete permissions on the row. No manual permission assignment needed in your code.

![Table permissions and row security settings](/images/blog/user-impersonation-tutorial/table-permissions.png)

# Build the app

{% info title="Full source code" %}
The complete source code for this project is available on [GitHub](https://github.com/appwrite-community/impersonation-demo).
{% /info %}

Scaffold a new React project and install the Appwrite SDK:

```bash
npm create vite@latest impersonation-demo -- --template react-ts
cd impersonation-demo
npm install
npm install appwrite
```

## Environment variables

Create a `.env` file in the project root with your Appwrite credentials:

```bash
VITE_APPWRITE_ENDPOINT=https://fra.cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=<YOUR_PROJECT_ID>
VITE_APPWRITE_DATABASE_ID=notes-app
VITE_APPWRITE_TABLE_ID=notes
```

Replace `<YOUR_PROJECT_ID>` with your project ID from the Console, and update the endpoint to match your project's region.

## Appwrite client setup

Create `src/lib/appwrite.ts` with the client configuration and helper functions:

```ts
import { Client, Account, TablesDB, type Models } from "appwrite";

const ENDPOINT = import.meta.env.VITE_APPWRITE_ENDPOINT;
const PROJECT_ID = import.meta.env.VITE_APPWRITE_PROJECT_ID;

export const DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID;
export const TABLE_ID = import.meta.env.VITE_APPWRITE_TABLE_ID;

export const client = new Client().setEndpoint(ENDPOINT).setProject(PROJECT_ID);
export const account = new Account(client);
export const tablesDB = new TablesDB(client);

export type AppwriteUser = Models.User<Models.Preferences>;

export interface NoteData {
title: string;
content: string;
color: string | null;
userId: string;
}

export type Note = Models.Row & NoteData;
```

A single `client` is shared across the app. When impersonation is active, we call `client.setImpersonateUserId(userId)` to set the header, and `client.setImpersonateUserId("")` to clear it. Since `account` and `tablesDB` reference the same client, they automatically pick up the impersonation context.

The Client SDK cannot query all users, so impersonators list them through the REST API directly. The session cookie and the auto-granted `users.read` scope handle authentication:

```ts
export async function listUsers(): Promise<AppwriteUser[]> {
const res = await fetch(`${ENDPOINT}/users`, {
headers: {
"Content-Type": "application/json",
"X-Appwrite-Project": PROJECT_ID,
},
credentials: "include",
});
if (!res.ok) throw new Error("Failed to list users");
const data = await res.json();
return data.users;
}
```

## Application component

Replace `src/App.tsx` with the main application. The key parts are session detection, impersonation, and note management.

**Session detection on mount** checks if the user is already logged in:

```tsx
useEffect(() => {
const checkSession = async () => {
try {
const user = await account.get();
setCurrentUser(user);
await fetchNotes(user.$id);

if (user.impersonator) {
const allUsers = await listUsers();
setUsers(allUsers.filter((u) => u.$id !== user.$id));
}
} catch {
// No active session
} finally {
setLoading(false);
}
};
checkSession();
}, [fetchNotes]);
```

Notice how `user.impersonator` controls whether the user list is fetched. Regular users skip this entirely.

**Starting impersonation** sets the impersonation header on the existing client:

```tsx
const impersonate = async (userId: string) => {
client.setImpersonateUserId(userId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

What is the correct API for user impersonation in the Appwrite Client SDK? Does client.setImpersonateUserId() exist, and what are the correct parameters?

💡 Result:

The correct API for user impersonation in Appwrite is through the Server SDKs (not Client SDKs), using the Users service to generate a session or JWT for the target user, typically after enabling an "impersonator" capability on users via the Console or Users API. This allows admins/trusted operators to act as another user for debugging/support without sharing credentials. Recent feature (announced March 2026). client.setImpersonateUserId does not exist in Appwrite Client SDKs. Client SDKs use Account service for user authentication (createSession, createJWT), but impersonation requires Server SDK with API key. Correct flow (Server SDK example, Node.js): 1. Use Users service (with API key) to create a session for the target userId: users.createSession(userId) 2. Use the returned session ID with Client SDK: client.setSession(sessionId) to make calls as that user. Or generate JWT: users.createJWT(userId) then client.setJWT(jwt). Parameters: userId (string, required). Client SDKs cannot impersonate directly; use Server SDK to generate user-specific session/JWT, then set on Client instance. Respects user permissions unlike API keys. No direct client.setImpersonateUserId; method not found in docs/SDK references.

Citations:


Remove client.setImpersonateUserId() — this method does not exist in Appwrite Client SDKs.

User impersonation is only available in Server SDKs. The correct approach is to use the Server SDK's Users service to create a session or JWT for the target user, then use client.setSession(sessionId) or client.setJWT(jwt) on the Client instance. The tutorial should demonstrate the Server SDK flow: users.createSession(userId) to generate a session, then pass the returned session ID to the Client instance.

This affects lines 182 and 189 where the non-existent method is called.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/blog/post/user-impersonation-tutorial/`+page.markdoc at line 182,
Remove the non-existent client.setImpersonateUserId call and replace the flow
with the Server SDK Users service: use Users.createSession(userId, /*
credentials */) or Users.createJWT(userId) on the server to generate a session
ID or JWT, return that to the client, and then call client.setSession(sessionId)
or client.setJWT(jwt) on the Client instance; update the tutorial text and the
code examples that reference client.setImpersonateUserId (and the related line
at 189) to demonstrate users.createSession/users.createJWT on the server and
client.setSession/client.setJWT on the browser client instead.

const impUser = await account.get();
setImpersonatedUser(impUser);
await fetchNotes(userId);
};

const stopImpersonating = async () => {
client.setImpersonateUserId("");
setImpersonatedUser(null);
if (currentUser) await fetchNotes(currentUser.$id);
};
```

Calling `setImpersonateUserId` on the client adds the `X-Appwrite-Impersonate-User-Id` header to every subsequent request. Appwrite resolves the target user and executes requests using their permissions. Passing an empty string clears the header and returns to the operator's own context.

**Creating notes while impersonating** just uses the same `tablesDB`. Since it shares the client, the impersonation header is already set:

```tsx
const createNote = async () => {
await tablesDB.createRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: {
title: newTitle.trim(),
content: newContent.trim(),
color,
userId: activeUserId,
},
});
};
```

Because the request runs as the impersonated user, Appwrite automatically grants that user read, update, and delete permissions on the new row.

**The sidebar** conditionally shows the Users tab based on the impersonator capability and current impersonation state:

```tsx
{isImpersonator && !isImpersonating && (
<button onClick={() => setTab("users")}>
Users
</button>
)}
```

When impersonating, the Users tab disappears and the impersonation banner appears at the top of the page with a button to stop impersonating.

# Run the demo

Start the development server:

```bash
npm run dev
```

The app flow works like this:

1. **Sign in as Sarah Chen.** The app detects her impersonator capability and shows the Users tab in the sidebar.

![Dashboard showing Sarah's notes with the Users tab visible](/images/blog/user-impersonation-tutorial/demo-dashboard.png)

2. **Click Users** to see all other users in the project with an Impersonate button next to each one.

![Users list with Impersonate buttons](/images/blog/user-impersonation-tutorial/demo-users.png)

3. **Click Impersonate** on a user. The Users tab disappears, an amber banner shows who you are viewing as, and the notes switch to that user's private notes.

![Impersonating Alex Rivera with the amber banner](/images/blog/user-impersonation-tutorial/demo-impersonating.png)

4. **Create or edit notes** as the impersonated user. The notes are created with that user's permissions.

![A note created while impersonating Alex](/images/blog/user-impersonation-tutorial/demo-impersonating-note.png)

5. **Click Stop impersonating** to return to Sarah's own notes. The Users tab reappears.

# How it works under the hood

When you call `client.setImpersonateUserId(userId)`, the SDK adds the `X-Appwrite-Impersonate-User-Id` header to every request. On the server side, Appwrite:

1. Verifies the requesting user has a valid session and the `impersonator` capability enabled
2. Looks up the target user by the provided ID
3. Switches the request context to the target user, executing the request with their permissions
4. Records the original impersonator in metadata for audit purposes

The target user's `accessedAt` timestamp is not updated during impersonation, so it does not inflate their activity metrics.

Appwrite also supports impersonation by email (`setImpersonateUserEmail`) and by phone (`setImpersonateUserPhone`) for different lookup workflows.

{% info title="Audit logging" %}
Internal audit logs attribute the action to the original impersonator, not the impersonated user. The impersonated target is recorded separately in the audit payload. This means you always know who actually performed an action and on whose behalf.
{% /info %}

# Security considerations

- **Each operator should have their own account.** Do not share a single impersonator login across a team. Individual accounts make audit trails meaningful.
- **Only grant the capability to users who need it.** The impersonator flag gives broad access. Treat it like an admin privilege.
- **A real user session is required.** An API key alone cannot trigger impersonation. The operator must authenticate first.
- **Always show the impersonation state in the UI.** Use the `impersonatorUserId` field from `account.get()` to detect active impersonation and display a clear banner.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Security guidance inconsistent with tutorial code

The security consideration advises readers to use the impersonatorUserId field from account.get() to detect active impersonation. However, the tutorial code tracks the impersonation state via a React component state variable (isImpersonating) set when impersonate() is called, not by reading impersonatorUserId from the response.

The two approaches serve slightly different purposes:

  • The component state approach only works for the operator's own UI session.
  • Checking impersonatorUserId on the result of account.get() is the reliable, SDK-native way that also works in scenarios where the impersonation state might be initialised from an existing session on page load.

Consider updating the impersonate() function snippet to show reading impUser.impersonatorUserId for banner detection, which would both demonstrate the recommended pattern and keep the advice consistent with the shown code.

- **Only one target at a time.** You cannot impersonate multiple users simultaneously. To switch targets, replace the impersonation value on the client or create a fresh one.

# Next steps

- [Read the user impersonation docs](/docs/products/auth/impersonation)
- [Read the impersonation announcement post](/blog/post/announcing-user-impersonation)
- [Explore Appwrite Authentication](/docs/products/auth)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading