# Admin Panel

The admin panel provides a user management dashboard for administrators. It's part of the Account UI SPA — when enabled, the `/admin` route is included in the build.

## Enabling Admin

Set `auth.admin` to `true` in your account config:

```typescript title="quickback/quickback.config.ts"
account: {
  auth: {
    admin: true,
  },
},
```

This does two things:
1. Sets `enableAdmin: true` on the SPA's runtime config, which unlocks the `/admin` routes (they guard themselves and redirect away when the flag is off)
2. Ensures the Better Auth `admin` plugin is enabled on the backend

When `auth.admin` is `false` (the default), the admin surface does not exist for your project: `/admin` redirects away for every user — including operators — and the nav entry is hidden. The compiler always emits the flag explicitly, so an unset value is a hard `false`.

## What It Can Do

**Users** — every account in the app, independent of organization:

- **View all users** — Paginated list with search
- **Create users** — Manually create accounts with email/password
- **Ban/unban users** — Suspend or reactivate user accounts
- **Reset passwords** — Generate password reset for any user
- **View sessions** — See active sessions for any user

**Organizations** — every tenant, whether or not you belong to it:

- **List all organizations** — cross-tenant, not just your memberships
- **Create an organization** — you name the **owner**; the operator creating it is deliberately not seated (see below)
- **Seat, re-role and remove members** — manage a tenant's roster without joining it

These run on `/auth/v1/admin/*`, emitted by Quickback because Better Auth's organization plugin cannot serve them: every one of its endpoints resolves through the *caller's* membership, and an operator administers organizations it does not belong to.

> **Users sign themselves up.** Self-serve signup stays on — a new account simply has no organization, and lands on a screen saying so until someone invites or seats them. What operators control is *organizations*, not accounts.


## Access Control

The admin panel is restricted to platform roles on the Better Auth user record: **`appmanager`** (the control plane) and **`sysadmin`** (which adds cross-tenant data access on top). Everyone else is redirected away from `/admin`.

> **`role: "admin"` is not a platform role.** The compiler seeds the admin plugin with `adminRoles: ["appmanager", "sysadmin"]`, so a user set to `"admin"` passed the UI guard and then got `403` from every admin API call — an operator who looked promoted and could not act. Removed in v0.62; migrate those rows to `"appmanager"`.


To grant operator access to a user:

```bash
curl -X POST https://api.example.com/auth/v1/admin/set-role \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "user_123", "role": "appmanager" }'
```

### Why creating an organization names an owner

Better Auth's `organization.create` seats whoever calls it. For an operator that is an escalation rather than a convenience: the membership it grants is tenant-data access — under `cms.access: "member"`, access to that organization's records — which is exactly what the `appmanager`/`sysadmin` split withholds.

So organization creation asks for an `ownerUserId`, seats that user, and never seats the caller. Self-serve creation (`POST /auth/v1/organization/create`) is closed by default for the same reason; a project that wants it sets `plugins.organization.allowUserToCreateOrganization: true`.

An operator who genuinely wants their own organization creates one naming themselves as owner — explicit, and visible in the audit trail rather than implied by whoever happened to make the call.

## Custom Admin Domain

Give the admin panel its own subdomain with `adminDomain`:

```typescript title="quickback/quickback.config.ts"
account: {
  domain: "auth.example.com",
  adminDomain: "admin.example.com",
  auth: { password: true, admin: true },
},
trustedOrigins: [
  "https://auth.example.com",
  "https://admin.example.com",
],
```

The admin domain serves the same Account SPA at root (`/`). The SPA's client-side router handles showing the admin page at `admin.example.com/admin`. Authentication cookies are shared across subdomains automatically via [cross-subdomain cookie configuration](/configure/domains#cross-subdomain-authentication).

## CMS Access Control

When both CMS and Account UI are enabled, the admin panel's profile page shows a "Go to CMS" button. You can restrict this to admin users only:

```typescript
cms: { domain: "cms.example.com", access: "sysadmin" },
```

With `access: "sysadmin"`, only the cross-tenant `sysadmin` tier reaches the CMS: the link is hidden in Account UI, `/api/v1/schema` returns `403`, and `custom_view` CRUD is locked to `userRole: ["sysadmin"]`. Note that an **appmanager does not get in** — `/account/admin` (this page) is the appmanager's surface, and the CMS is not. See [CMS access gate](/ui/admin#access-gate) for the full model.

## Related

- [Configuration](/configure) — `auth.admin` and `adminDomain` options
- [Multi-Domain Architecture](/configure/domains) — Custom domains and hostname routing
- [Access Control](/define/access) — Role-based permissions in your API
