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.
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
@colyseus/admin 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/authMounting
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:
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.
Usage
Once mounted, start your server and navigate to http://localhost:2567/admin in your browser. 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. The endpoints are normal createEndpoint(...) routes and respect the same admin.guard() semantics if you wrap them for use elsewhere.
First-run bootstrap
The admin panel includes no built-in admin user. The first visit to /admin checks for an existing admin account and, if none exists, renders a one-time bootstrap form.
After bootstrap:
- A session cookie is set (HttpOnly, SameSite=Strict, JWT-signed).
- You’re redirected into the panel with full
adminrole access. - All sibling routes gated with
admin.guard()(e.g. the Playground or Monitor) now resolve against the same session.
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_SECRETandSESSION_SECRET: see Auth Module → Required Environment Secrets. RotatingJWT_SECRETinvalidates all admin sessions (SESSION_SECRETonly signs OAuth state cookies). -
Wire
auth.settings.onForgotPasswordto deliver password reset emails through your transactional email provider, the same hook that powers your player-side reset flow. -
Tighten the session cookie via the
sessionoption: set an explicitdomain, an appropriatemaxAge, and confirm theSecure/SameSiteflags match your deployment. -
Disable
allowDevHeaderif you don’t need it. It auto-disables whenNODE_ENV === "production", but setting it tofalseexplicitly 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.
-
Gate the Monitor and Playground behind
admin.guard()so you don’t have multiple unauthenticated panels exposed:src/app.config.tsroutes: createRouter({ ...playground({ use: [admin.guard()] }), ...monitor({ use: [admin.guard()] }), ...admin({}), }),
Options reference
admin(options) accepts the following AdminOptions:
| Option | Type | Default | Description |
|---|---|---|---|
database | GameDatabase | GameDatabase.current | The database powering the user store, audit log, and table introspection. Pass explicitly for multi-database setups. |
tables | Record<string, Table> | database.tables | Map of drizzle tables exposed in the panel. Spread database.tables and append custom tables: { ...database.tables, guilds }. |
resources | Record<string, ResourceDefinition> | {} | Per-table UI/UX overrides: labels, visible columns, form fields, actions, policies. See Resources & CRUD. |
uiPath | string | "/admin" | Mount path for the admin SPA. |
apiPath | string | "/admin-api" | Mount path for the admin REST API. |
uiDistDir | string | built-in | Absolute path to the built UI assets. Defaults to the bundled build/ directory inside the package. |
session | SessionConfig | see below | Cookie config: ttlSeconds, cookieDomain, cookieSameSite, cookieSecure. |
resolveUserId | (ctx) => Promise<string | undefined> | session cookie, falling back to the X-User-Id header in dev | Override the identity resolver to integrate with another auth scheme. |
enforceRbac | boolean | true | Set to false to skip RBAC entirely (development only). |
allowDevHeader | boolean | true in dev, false in prod | Permit X-User-Id: <id> as a fallback identity for curl/puppeteer testing. |
minPasswordLength | number | 8 | Minimum password length accepted by the bootstrap and reset endpoints. Plain length check, no complexity rules. |
onResetRequest | (ctx) => void | Promise<void> | logs to stdout | Panel-specific override for the password-reset email. Most projects should configure auth.settings.onForgotPassword instead and reuse their player-side delivery. Receives { email, userId, token, url }. |
rateLimit | { login, bootstrap, requestReset } | in-memory token buckets | Per-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 enabled | Configure the dashboard homepage. See Dashboard & widgets. |
logger | Logger | null | JSON stdout | Pino-compatible logger. 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 and signed with JWT_SECRET. Revoking a session is done via db.auth.bumpTokenVersion(userId). See Database → Authentication.
Next steps
- Authentication & RBAC: sessions, roles, password reset, and
admin.guard(). - Resources & CRUD: exposing custom tables, customizing columns and forms, custom actions, the audit log.
- Dashboard & widgets: configuring presets and appending custom widgets to the homepage.
- Live rooms: the built-in room inspector.