# User Management

> Manage users, sessions, and account status from the admin panel.

*Canonical: https://makerkit.dev/docs/tanstack-drizzle/admin/user-management*

---

The User Management section at `/admin/users` provides full control over user accounts, sessions, and access permissions.

## User List

### Search and Filtering

The user table supports multiple search and filter options:

| Filter | Options | Description |
|--------|---------|-------------|
| **Search** | Text input | Search by name or email |
| **Role** | All / User / Admin | Filter by user role |
| **Status** | All / Active / Banned | Filter by account status |
| **Sort** | Multiple fields | Order by name, email, date, role, or status |

Results are paginated at 25 users per page.

{% img src="/images/docs/admin-users-table.webp" width="2844" height="1938" alt="Admin Users Table with filters" /%}

### User Table Columns

Each row displays:

- **Avatar and Name**: Profile picture with display name
- **Email**: User's email address
- **Role**: User or admin badge
- **Status**: Active or banned indicator
- **Created**: Account creation date
- **Actions**: Dropdown menu with available actions

## User Actions

Click the action menu (three dots) on any user row to access these options:

### View Details

Opens a side panel with comprehensive user information:

- **Profile**: Avatar, name, email, user ID
- **Status**: Account status, role, email verification, 2FA status
- **Dates**: Account creation and last update timestamps
- **Subscriptions**: Active subscription details (if billing is enabled)
- **Ban Info**: Reason and expiration for banned users
- **Sessions**: All active sessions with device info

{% img src="/images/docs/admin-users-detail.webp" width="2844" height="1938" alt="User Details Sheet" /%}

### Change Role

Promote a user to admin or demote an admin to user:

```typescript
// Server function with RBAC permission check
import { createServerFn } from '@tanstack/react-start';
import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const changeRoleFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ user: ['set-role'] })])
  .validator(changeRoleSchema)
  .handler(async ({ data, context }) => {
    const result = await service.changeRole({
      adminId: context.user.id,
      ...data,
    });

    return result;
  });
```

After the mutation resolves on the client, refresh the table with TanStack Query / `router.invalidate()` in the mutation's `onSuccess` callback rather than a server-side `revalidatePath`.

**Requirements:**
- Admin must have `user:set-role` permission
- Changes take effect on the user's next session

### Impersonate User

Sign in as another user for debugging and support purposes. After confirming, you'll be redirected to the app dashboard as that user.

**How it works:**

1. Admin clicks "Impersonate User" in the action menu
2. Confirmation dialog appears
3. System generates a session for the target user
4. Admin is signed in as that user with their permissions

**Restrictions:**
- Cannot impersonate admin users
- Cannot impersonate banned users
- A banner should appear during impersonation indicating the original admin

To end impersonation, sign out normally.

```typescript
// Impersonation using Better Auth
const { mutateAsync } = useMutation({
  mutationFn: async (userId: string) => {
    const { data, error } = await authClient.admin.impersonateUser({
      userId,
    });
    if (error) throw new Error(error.message);
    return data;
  },
});
```

### Ban User

Restrict a user's access to the platform:

- **Reason**: Optional explanation for the ban (stored for reference)
- **Duration**: Temporary (with expiration date) or permanent
- Banned users cannot sign in
- Existing sessions are not automatically revoked (use "Revoke All Sessions" if needed)

**Implementation note:** Both ban and unban operations use the same `user:ban` permission. This matches Better Auth's internal permission model.

```typescript
import { createServerFn } from '@tanstack/react-start';
import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const banUserFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ user: ['ban'] })])
  .validator(banUserSchema)
  .handler(async ({ data, context }) => {
    const result = await service.banUser({
      adminId: context.user.id,
      ...data,
    });

    return result;
  });
```

### Unban User

Restore access for a banned user. The ban reason and expiration are cleared.

```typescript
import { createServerFn } from '@tanstack/react-start';
import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

// Same permission as ban
export const unbanUserFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ user: ['ban'] })])
  .validator(unbanUserSchema)
  .handler(async ({ data, context }) => {
    const result = await service.unbanUser({
      adminId: context.user.id,
      ...data,
    });

    return result;
  });
```

### Remove User

Permanently delete a user account.

**Important:**
- Cannot delete admin users (demote first if needed)
- This action is irreversible
- Associated data (sessions, memberships) is also deleted

```typescript
import { createServerFn } from '@tanstack/react-start';
import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const removeUserFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ user: ['delete'] })])
  .validator(removeUserSchema)
  .handler(async ({ data, context }) => {
    const result = await service.removeUser({
      adminId: context.user.id,
      ...data,
    });

    return result;
  });
```

## Session Management

### Viewing Sessions

The user details panel lists all active sessions with:

- **Device**: Browser and OS information (parsed from user agent)
- **IP Address**: Last known IP
- **Created**: Session start time
- **Expires**: Session expiration time

### Listing Sessions

```typescript
import { createServerFn } from '@tanstack/react-start';
import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const listUserSessionsFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ session: ['list'] })])
  .validator(revokeUserSessionsSchema)
  .handler(async ({ data, context }) => {
    return service.listUserSessions({
      adminId: context.user.id,
      ...data,
    });
  });
```

### Revoking Sessions

Two options for ending sessions:

**Revoke Single Session**: End a specific session while keeping others active.

```typescript
import { createServerFn } from '@tanstack/react-start';
import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const revokeSessionFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ session: ['revoke'] })])
  .validator(revokeSessionSchema)
  .handler(async ({ data, context }) => {
    const result = await service.revokeSession({
      adminId: context.user.id,
      ...data,
    });

    return result;
  });
```

**Revoke All Sessions**: Force logout from all devices. Useful when:
- User reports account compromise
- After changing sensitive account settings
- Before or after banning a user

```typescript
import { createServerFn } from '@tanstack/react-start';
import { adminFunctionMiddleware } from '@kit/function-middleware/functions';
import { withAdminPermission } from '@kit/function-middleware/server';

export const revokeAllUserSessionsFunction = createServerFn({ method: 'POST' })
  .middleware([...adminFunctionMiddleware, withAdminPermission({ session: ['revoke'] })])
  .validator(revokeUserSessionsSchema)
  .handler(async ({ data, context }) => {
    const result = await service.revokeAllUserSessions({
      adminId: context.user.id,
      ...data,
    });

    return result;
  });
```

## Subscription Information

When billing is enabled, the user details panel shows subscription data:

- **Plan Name**: Current subscription tier
- **Status**: Active, canceled, past due, etc.
- **Billing Period**: Monthly or yearly
- **Next Billing Date**: When the subscription renews

This requires the `subscriptions:list` permission in your RBAC config:

```typescript
export default defineAdminRBACConfig({
  resources: {
    SUBSCRIPTIONS: 'subscriptions',
  },
  accessController: {
    subscriptions: ['list'],
  },
});
```

## Permission Requirements

User management actions require specific RBAC permissions:

| Action | Required Permission |
|--------|-------------------|
| View user list | `user:list` |
| View user details | `user:get` |
| Change role | `user:set-role` |
| Ban/Unban user | `user:ban` |
| Delete user | `user:delete` |
| List sessions | `session:list` |
| Revoke sessions | `session:revoke` |
| View subscriptions | `subscriptions:list` |

**Note:** Resource names must be singular (`user`, `session`) to match Better Auth's internal permission model. The action names also follow Better Auth conventions (e.g., `set-role` not `change-role`).

The default `admin` role has all permissions. Custom roles need explicit grants.

See [RBAC Permissions](./rbac-permissions) for configuring custom admin roles.

## Data Loading Pattern

User data is loaded server-side with admin protection. The loader lives in the
`@kit/admin` package and is exposed to the route through a `createServerFn`
wrapper (`fetchAdminUsers` in `apps/web/src/lib/admin.functions.ts`); the
`/admin/users` route loader calls that wrapper and the page reads the result via
`Route.useLoaderData()`.

```typescript
// packages/admin/src/users/lib/loaders/users-page.loader.ts
import { requireAdmin } from '@kit/auth/require-admin';

export const loadUsersPageData = async (params: UsersPageParams) => {
  const admin = await requireAdmin(); // redirect / notFound if not admin

  const service = createUsersAdminService();

  return service.listUsers({
    searchValue: params.search,
    limit: 25,
    offset: ((params.page ?? 1) - 1) * 25,
    role: params.role,
    status: params.status,
    sortBy: params.sortBy,
    sortDirection: params.sortDirection,
  });
};
```

```typescript
// apps/web/src/routes/admin/users.tsx
import { createFileRoute } from '@tanstack/react-router';

import { fetchAdminUsers } from '#/lib/admin.functions';

export const Route = createFileRoute('/admin/users')({
  loaderDeps: ({ search }) => search,
  loader: ({ deps }) => fetchAdminUsers({ data: deps }),
  component: AdminUsersRoute,
});
```

Per-request session lookups are deduped by `getSession()`'s memoization, so no
React `cache()` wrapper is needed.

---

**Previous:** [Overview](./overview)

**Next:** [Organization Management](./organization-management)
