Skip to content

Admin dashboard

The admin dashboard at /admin lets you, the app’s operator, manage every account without opening the Convex dashboard: search users, create them, make admins, ban, set passwords, sign users out, impersonate them and delete them. With organizations on, it also manages every organization and its members.

It’s built on Better Auth’s admin plugin. Every admin action is a Convex function in src/convex/admin/ that checks the caller’s role on the server.


Turn it on

1. Add the admin items

A full install of @auth/svelte 0.11 or later (jsrepo add @auth/svelte --all)@auth/nextjs 0.3 or later (jsrepo add @auth/nextjs --all) already has them, so go to step 3. An app installed with an earlier version adds them:

ItemInstalls
admin/convexsrc/convex/admin/queries.ts, src/convex/admin/mutations.ts, src/convex/model/admin/index.ts, src/convex/betterAuth/adminUsers.ts, src/convex/betterAuth/adminOrganizations.ts
admin/libThe components, useIsAppAdmin() and admin.constants.ts: src/lib/admin/src/components/admin/
admin/routesThe /admin page: src/routes/admin/+page.sveltesrc/app/admin/page.tsx

2. Update the items they build on

  • config: auth.ts registers the admin() plugin and blocks its HTTP endpoints. The role helpers are in the new model/admin/roles.ts, and auth.constants.types.ts gains admin?: boolean.
  • base: the generated Better Auth schema moves from betterAuth/schema.ts to betterAuth/generatedSchema.ts and gains the plugin’s fields: role, banned, banReason and banExpires on users, impersonatedBy on sessions. betterAuth/schema.ts builds on it and adds the search indexes the dashboard uses. The root layout mounts <ImpersonationBanner />.
  • auth/lib: the Better Auth client registers adminClient() while admin is on.

Run step 1 first. The updated root layout imports ImpersonationBanner from admin/lib, so base without the admin items doesn’t build. Keep your own auth.constants.ts, and merge the diffs of files you’ve edited.

When you regenerate the Better Auth schema later, write it to generatedSchema.ts so the search indexes in schema.ts survive. Run this in src/convex/betterAuth:

3. Set the flag

src/convex/auth.constants.ts

The admin dashboard doesn’t need sendEmails. With organizations: true as well, it adds an Organizations tab.

4. Make yourself the first admin

Sign up in your app, then look up your user id. Better Auth keeps users in the user table of the betterAuth component, and the id is the document’s _id:

In the Convex dashboard, the same table is under Data once you pick the betterAuth component. Set the id as a Convex environment variable. For several admins, separate the ids with commas:

Open /admin. From there, you promote other users with Make admin, and copy any user’s id from their row menu.

Each Convex deployment has its own users, so the id from your development deployment means nothing on production or on a preview deployment. Set ADMIN_USER_IDS on each deployment with an id from its own user table.

The default layout doesn’t link to /admin. Add <AdminButton /> to your navigation. It renders only for admins.


What admins can do

The Users tab lists users newest first and searches them by name or email. From a user’s row menu or their page, an admin can:

  • create a user who signs in right away: the email counts as verified, and with organizations on the user can join an organization as a member or admin,
  • edit a user’s name and email; a new email counts as verified too,
  • make an admin, or remove another admin’s role,
  • set a password; a user who signed in without one (social, magic link or email code) gets a password account,
  • ban for 1 hour, 1 day, 7 days, 30 days or permanently, with an optional reason, and unban,
  • sign a user out of one session or of all of them,
  • impersonate a user,
  • delete a user.
A user's row menu open in the admin dashboard with Copy user ID, View profile, Impersonate, Make admin, Set password, Sign out everywhere, Ban and Delete user

Banning signs the user out everywhere and refuses new sign-ins. A temporary ban ends at the user’s next sign-in after it expires, and the dashboard already shows an expired ban as no ban. Deleting a user fails while they own an organization that has other members: make another member the owner or delete the organization first.

With organizations on, the Organizations tab lists every organization and searches them by name or slug. On an organization’s page, an admin changes its logo, name and slug, adds and removes members, changes their roles, hands the ownership to another member and deletes the organization, whether or not the admin is a member.

<AdminDashboard /> describes each view.


Roles

The app roles are user, which every new account gets, and admin. An app admin is a user whose role is admin, or whose id is in ADMIN_USER_IDS. These app roles are separate from the owner, admin and member roles inside an organization.

What an admin can do to an account depends on whose it is:

AccountWhat an admin can do
A userEverything listed above.
Their ownEdit their name and email, set their password, sign out sessions. They can’t change their own role, ban or delete themselves.
Another adminOnly Remove admin. After that, the account is a user’s like any other.
An admin from ADMIN_USER_IDSNothing. Change the environment variable instead.

So one admin can’t lock out, take over or quietly sign in as another. The same rule covers organizations: an organization whose owner is another admin can’t be deleted or handed over until that owner loses the admin role. An owner’s role can’t be changed and an owner can’t be removed; hand the ownership to another member first, and choose whether the previous owner stays as an admin or member or leaves.

The roles are Better Auth’s defaults. To add your own, change appRoleValidator and APP_ADMIN_ROLES in src/convex/model/admin/roles.ts together with the admin() options in src/convex/auth.ts.


Impersonation

Impersonate signs you in as the user in your own browser and reloads your app at /. A banner at the top of every page shows who you’re acting as, and Stop impersonating returns you to your own session at /admin.

  • An impersonation session lasts at most 1 hour, Better Auth’s default. Better Auth refreshes sessions it finds in Convex mutations to 7 days, so auth.ts keeps an impersonation session’s original expiry.
  • No one can impersonate an admin, including admins from ADMIN_USER_IDS, whom Better Auth itself would allow.
  • The dashboard doesn’t offer to impersonate a banned user.
  • A user’s session list marks impersonation sessions, so you can sign one out.

Security model

  • Every function in src/convex/admin/ starts with requireAppAdmin from src/convex/model/admin/index.ts. It throws unless admin is on and the caller is an app admin. The Access denied page on /admin and the hidden <AdminButton /> are only the UI.
  • Better Auth’s admin HTTP endpoints under /api/auth/admin/ answer 404, except impersonate-user and stop-impersonating: impersonation swaps the session cookies, so it has to run in the browser. A hook in auth.ts refuses to impersonate admins with You can't impersonate admins. Every other admin action goes through the Convex functions, so the browser can’t skip their checks.
  • Session tokens stay on the server. The session list returns session ids, and signing a session out looks its token up in the mutation.
  • Turning admin off keeps every user’s role in the database, but each admin function then throws The admin feature is disabled and /admin shows Access denied.

Where the backend lives

FileContents
src/convex/admin/queries.tsisCurrentUserAdmin, getImpersonationStatus, listUsers, getUser, listUserSessions, listAllOrganizations, searchOrganizations, getOrganizationWithMembers
src/convex/admin/mutations.tscreateUser, updateUser, setRole, banUser, unbanUser, removeUser, setUserPassword, revokeUserSession, revokeUserSessions, deleteOrganizationAsAdmin, addMember, updateMemberRole, removeMember, updateOrganization, transferOwnership
src/convex/model/admin/index.tsThe guards: requireAppAdmin, assertNotSelf, assertCanManageUser
src/convex/model/admin/roles.tsisAppAdminUser, getAdminUserIds, appRoleValidator
src/convex/betterAuth/adminUsers.tsUser listing and search inside the Better Auth component
src/convex/betterAuth/adminOrganizations.tsOrganization listing, search, memberships and ownership inside the Better Auth component
src/convex/auth.tsThe admin() plugin, the HTTP endpoint block and the impersonation session hook

Your own UI can call these functions too. Each one runs the same guards, so it needs a signed-in admin.