Admin Panel

Admin Panel

@colyseus/admin is a browser-based operations console for projects using @colyseus/database. The panel provides user management (browse, ban, revoke sessions), CRUD over your drizzle tables, a live room inspector, and a customizable dashboard. Everything is gated by session-based login with role-based access control and an append-only audit log.

The admin dashboard: totals, recent users and live rooms widgets

For a lightweight, no-auth dashboard intended for room and process inspection, see the Monitoring Panel instead. The admin panel is the heavier counterpart, built for production operations.

⚠️

@colyseus/admin is in beta: APIs may change between releases as we incorporate community feedback. Share yours on the public roadmap.

Installation

On top of @colyseus/core, the panel has two peer dependencies: @colyseus/database (user store, audit log, drizzle table introspection) and @colyseus/auth (JWT signing, password hashing). Install all three together:

npm install --save @colyseus/admin @colyseus/database @colyseus/auth

Mounting

The admin panel mounts in two ways:

Spread admin({}) into createRouter. The factory returns both the SPA static-serving routes and the REST API in one map:

src/app.config.ts
import { defineServer, createRouter } from "colyseus";
import { GameDatabase } from "@colyseus/database";
import { admin } from "@colyseus/admin";
 
export const db = new GameDatabase({ connectionString: process.env.DATABASE_URL });
 
export default defineServer({
    database: db,
    routes: createRouter({
        ...admin({}),
    }),
});

The database option defaults to GameDatabase.current, set automatically when you construct new GameDatabase(...). Multi-database deployments should pass database explicitly (e.g. admin({ database: db })) to disambiguate which instance the panel drives. admin() throws at startup when neither is available.

Usage

Once mounted, start your server and open http://localhost:2567/admin/ in your browser (the bare /admin redirects to the trailing slash). If you customized uiPath, adjust the URL accordingly.

The REST API mounts in parallel at http://localhost:2567/admin-api and is consumed by the SPA. Every endpoint enforces the same session and role rules as the UI, so you can script the panel with curl or fetch using the session cookie. The sub-pages list the endpoints for each area. GET /admin-api/_health is the one unauthenticated route: it answers 200 { ok, db, latencyMs } or 503 when the database is unreachable, for readiness probes.

First-run bootstrap

The admin panel includes no built-in admin user. The SPA calls GET /admin-api/auth/status on load; while no user has the admin role it reports needsBootstrap: true and the panel renders a one-time setup form.

The first-run setup form asking for an email and password

Submitting the form calls POST /admin-api/auth/bootstrap with { email, password }. If a user with that email already exists (for example, you registered through your game first), that account is promoted instead of created. After bootstrap:

  • A session cookie is set (HttpOnly, SameSite=Strict, JWT-signed).
  • You’re redirected into the panel with full admin role access.
  • All sibling routes gated with admin.guard() (e.g. the Playground or Monitor) now resolve against the same session.
  • The endpoint refuses further calls with 403 as soon as one admin exists.

See Authentication & RBAC for the full session, role, and revocation story.

Hardening for production

The defaults are tuned for local development. Before exposing the panel to the public internet:

  • Set a strong JWT_SECRET and SESSION_SECRET: see Auth Module → Required Environment Secrets. With NODE_ENV=production, admin() throws at startup when no JWT secret is configured; outside production it only warns. Rotating JWT_SECRET invalidates all admin sessions (SESSION_SECRET only signs OAuth state cookies).

  • Keep player accounts out of the panel. POST /admin-api/auth/login accepts any user row with a password. The user role then passes the list/read gate on every resource and on the room inspector. A player with an email and password can therefore read live room state and other players’ emails through the API. Until the package closes this, serve the panel on a separate hostname or behind your own network guard. Keeping admin accounts in a dedicated database also works. See Authentication → Roles.

  • Wire auth.settings.onForgotPassword so password reset links reach your admins. The panel ships no reset page yet; see Password reset for the API flow.

  • Tighten the session cookie via the session option: set an explicit cookieDomain, an appropriate ttlSeconds, and confirm the cookieSecure/cookieSameSite values match your deployment.

  • Disable allowDevHeader if you don’t need it. It auto-disables when NODE_ENV === "production", but setting it to false explicitly avoids surprises in mixed environments.

  • Swap the in-memory rate limiters for a Redis-backed implementation on multi-process deployments. The default token buckets don’t share state across instances. The limiter keys on X-Forwarded-For / X-Real-IP when present, so only expose the panel behind a proxy that overwrites those headers.

  • Gate the Monitor and Playground behind admin.guard() so you don’t have multiple unauthenticated panels exposed:

    src/app.config.ts
    routes: createRouter({
        ...playground({ use: [admin.guard()] }),
        ...monitor({    use: [admin.guard()] }),
        ...admin({}),
    }),
🚫

enforceRbac: false removes every identity check: any anonymous request can create, update, delete, kick, dispose and ban, and audit rows are written without an operatorId. Use it only on a local database.

Options reference

admin(options) accepts the following AdminOptions:

OptionTypeDefaultDescription
databaseGameDatabaseGameDatabase.currentThe database powering the user store, audit log, and table introspection. Pass explicitly for multi-database setups.
tablesRecord<string, Table>database.tablesMap of drizzle tables exposed in the panel. Custom tables must be added here: { ...db.tables, guilds }. See Resources & CRUD.
resourcesRecord<string, ResourceDefinition>built-in defaultsPer-table UI/UX overrides: labels, visible columns, form fields, actions, policies. The audit log and user notes get built-in definitions unless you override them. See Resources & CRUD.
uiPathstring"/admin"Mount path for the admin SPA. Ignored under an Express path mount, where the mount path wins.
apiPathstring"/admin-api"Mount path for the admin REST API. Nested inside the mount under an Express path mount.
uiDistDirstringbuilt-inAbsolute path to the built UI assets. Defaults to the bundled build/ directory inside the package.
sessionSessionConfigsee belowCookie config: ttlSeconds, cookieDomain, cookieSameSite, cookieSecure.
resolveUserId(ctx: { getHeader }) => string | undefined | Promise<…>session cookie, then X-User-Id in devReplace the identity resolver to integrate another auth scheme. Your function receives only getHeader(name). The default also rejects sessions whose tokenVersion was bumped; a replacement has to repeat that check.
enforceRbacbooleantrueSet to false to skip every identity and role check (local development only, see the warning above).
allowDevHeaderbooleantrue in dev, false in prodPermit X-User-Id: <id> as a fallback identity for curl/puppeteer testing.
minPasswordLengthnumber8Minimum password length accepted by the bootstrap and reset endpoints. Plain length check, no complexity rules.
onResetRequest(ctx) => void | Promise<void>forwards to auth.settings.onForgotPassword, else logs the linkDeliver the password reset link. Receives { email, userId, token, url }; url is a site-relative path. See Password reset.
rateLimit{ login, bootstrap, requestReset }in-memory token bucketsPer-endpoint rate limiters. Pass false per slot to disable, or a custom RateLimiter to swap in a Redis-backed implementation.
dashboard{ presets, widgets }all presets enabledConfigure the dashboard homepage. See Dashboard & widgets.
loggerLogger | nullJSON to stdout / stderrPino-compatible logger (info, warn, error, child). warn and error go to stderr; debug only prints with LOG_DEBUG=1. Pass null to silence the panel’s internal logs.

Session defaults

The session config controls the JWT cookie. Defaults:

session: {
    ttlSeconds: 7 * 24 * 60 * 60, // 1 week
    cookieSameSite: "Strict",
    cookieSecure: process.env.NODE_ENV === "production",
    // cookieDomain: undefined  ← host-only; set explicitly when serving from a subdomain
}

The cookie is always HttpOnly, scoped to Path=/, and signed with JWT_SECRET. Revoking a session is done via db.auth.bumpTokenVersion(userId). See Database → Authentication.

Next steps