User Management

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

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:

FilterOptionsDescription
SearchText inputSearch by name or email
RoleAll / User / AdminFilter by user role
StatusAll / Active / BannedFilter by account status
SortMultiple fieldsOrder by name, email, date, role, or status

Results are paginated at 25 users per page.

Admin Users Table with filtersClick to expand

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
  • Functions: Dropdown menu with available functions

User Functions

Click the function 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
User Details SheetClick to expand

Change Role

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

// 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 function 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.

// 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.

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.

export const unbanUserFunction = createServerFn({ method: 'POST' }).middleware([
...adminFunctionMiddleware,
withAdminPermission({ user: ['ban'] }),
] // Same permission as 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 function is irreversible
  • Associated data (sessions, memberships) is also deleted
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

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.

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
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:

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

Permission Requirements

User management functions require specific RBAC permissions:

FunctionRequired Permission
View user listuser:list
View user detailsuser:get
Change roleuser:set-role
Ban/Unban useruser:ban
Delete useruser:delete
List sessionssession:list
Revoke sessionssession:revoke
View subscriptionssubscriptions:list

Note: Resource names must be singular (user, session) to match Better Auth's internal permission model. The function 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 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().

// 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,
});
};
// 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

Next: Organization Management