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:
| Item | Installs |
|---|---|
admin/convex | src/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/lib | The components, useIsAppAdmin() and admin.constants.ts: src/lib/admin/src/components/admin/ |
admin/routes | The /admin page: src/routes/admin/+page.sveltesrc/app/admin/page.tsx |
2. Update the items they build on
config:auth.tsregisters theadmin()plugin and blocks its HTTP endpoints. The role helpers are in the newmodel/admin/roles.ts, andauth.constants.types.tsgainsadmin?: boolean.base: the generated Better Auth schema moves frombetterAuth/schema.tstobetterAuth/generatedSchema.tsand gains the plugin’s fields:role,banned,banReasonandbanExpireson users,impersonatedByon sessions.betterAuth/schema.tsbuilds on it and adds the search indexes the dashboard uses. The root layout mounts<ImpersonationBanner />.auth/lib: the Better Auth client registersadminClient()whileadminis 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
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.
5. Link to it
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.

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:
| Account | What an admin can do |
|---|---|
| A user | Everything listed above. |
| Their own | Edit their name and email, set their password, sign out sessions. They can’t change their own role, ban or delete themselves. |
| Another admin | Only Remove admin. After that, the account is a user’s like any other. |
An admin from ADMIN_USER_IDS | Nothing. 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.tskeeps 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 withrequireAppAdminfromsrc/convex/model/admin/index.ts. It throws unlessadminis on and the caller is an app admin. The Access denied page on/adminand the hidden<AdminButton />are only the UI. - Better Auth’s admin HTTP endpoints under
/api/auth/admin/answer 404, exceptimpersonate-userandstop-impersonating: impersonation swaps the session cookies, so it has to run in the browser. A hook inauth.tsrefuses to impersonate admins withYou 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
adminoff keeps every user’s role in the database, but each admin function then throwsThe admin feature is disabledand/adminshows Access denied.
Where the backend lives
| File | Contents |
|---|---|
src/convex/admin/queries.ts | isCurrentUserAdmin, getImpersonationStatus, listUsers, getUser, listUserSessions, listAllOrganizations, searchOrganizations, getOrganizationWithMembers |
src/convex/admin/mutations.ts | createUser, updateUser, setRole, banUser, unbanUser, removeUser, setUserPassword, revokeUserSession, revokeUserSessions, deleteOrganizationAsAdmin, addMember, updateMemberRole, removeMember, updateOrganization, transferOwnership |
src/convex/model/admin/index.ts | The guards: requireAppAdmin, assertNotSelf, assertCanManageUser |
src/convex/model/admin/roles.ts | isAppAdminUser, getAdminUserIds, appRoleValidator |
src/convex/betterAuth/adminUsers.ts | User listing and search inside the Better Auth component |
src/convex/betterAuth/adminOrganizations.ts | Organization listing, search, memberships and ownership inside the Better Auth component |
src/convex/auth.ts | The 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.
Related
<AdminDashboard />: the tabs, views and URL parameters.<AdminButton />: a link to/adminfor admins.<ImpersonationBanner />: the banner shown while impersonating.- Organizations: turns on the Organizations tab.
- Configure features: every
AUTH_CONSTANTSflag.