Keel

Docs / Admin panel / Users & moderation

Users & moderation

/admin/users lists every account with search, role, status, and team filters; clicking one opens a detail view with tabs for profile, teams, billing, sessions, and activity. The data comes from listAdminUsers and getAdminUserDetail in lib/dal/admin-users.ts.

Filtering happens in JS, on purpose

listAdminUsers runs one scan of user, joins memberships, then filters and paginates in JavaScript rather than SQL. "Highest role across all a user's memberships" and "which teams" aren't expressible as a single clean WHERE, and a starter SaaS's user count is small enough that the pragmatic version wins. These are power-user screens, not built to page through millions of rows, if you outgrow that, this is the function to rewrite as a heavier query.

Impersonation

An admin can act as a user to reproduce a bug. POST /api/admin/users/[id]/impersonate calls Better Auth's impersonateUser with asResponse: true:

ts
const response = await auth.api.impersonateUser({
  body: { userId: id },
  headers: await headers(),
  asResponse: true,
});

asResponse: true returns the real Response including the Set-Cookie that swaps the browser's session to the target, instead of the plain JSON a direct server call gives. Ending it is POST /api/admin/stop-impersonating, which has no requireSuperadmin gate on purpose, by then the current session is the impersonated user, and Better Auth restores the admin from the session's impersonatedBy field, so the gate is implicit.

Ban and delete

Both are guarded against acting on another superadmin (see Admin access). Ban goes through auth.api.banUser, which revokes the banned user's own sessions.

Banning revokes impersonation sessions separately

banUser only kills sessions where session.userId is the banned user. It does not touch sessions that user was impersonating (impersonatedBy = them). revokeImpersonationSessionsByAdmin is called right after ban to close that, otherwise a banned admin caught mid-impersonation keeps acting as the target until the session naturally expires.

Delete (deleteAdminUser) drops the user's solely-owned teams first, then the user row, in one transaction. FK cascades handle the rest, sessions, accounts, remaining memberships, API keys they created, and notifications all fall away from the two deletes. Teams the user co-owns are left alone. The delete-confirmation dialog previews exactly which teams will be dropped before the operator commits, using the same getSolelyOwnedTeams the delete itself relies on.

Sessions

The detail view's Sessions tab lists a user's active sessions and can revoke them individually (DELETE /api/admin/users/[id]/sessions/[token]), useful for forcing a sign-out on a shared or compromised device without a full ban.

Edit this page on GitHub →Last updated Jul 22, 2026