Admin Guide
This guide covers the administrative aspects of running an UrsaMU server,
including user management, configuration, backups, and troubleshooting.
First-Run Setup
UrsaMU handles first-run setup automatically. This only needs to happen once.
How it works
Run the server for the first time:
deno task start
When the database is empty, the engine prints:
Fresh database detected — no players exist yet.
Connect via telnet and run:
create <name> <password>
The first player created is automatically given superuser access.
Connect with a Telnet client (telnet localhost 4201) and create your
account. The first player created on a fresh database is automatically
granted the superuser flag.
Permission levels
UrsaMU uses a flag-based permission system:
| Flag | Level | Who can set it | Description |
|---|---|---|---|
superuser |
10 | First-run prompt only | Full server owner access |
admin |
9 | Superuser (in-game) | Full admin command access |
wizard |
9 | Superuser (in-game) | Same as admin — alternative role name |
storyteller |
8 | Admin (in-game) | Storytelling/moderation access |
builder |
7 | Admin (in-game) | Building permissions |
player |
1 | Automatic on create |
Standard player |
From inside the game, grant admin rights with:
@set TheirName=admin
The superuser flag cannot be granted via @set — it can only be created
through the first-run interactive prompt.
The superuser flag itself cannot be granted via @set from inside the
game — it can only be created via this first-run flow.
User Management
Creating Admin Users
To grant admin privileges to an existing player, use the @set command as a
superuser or existing admin:
@set <username>=admin
Managing User Accounts
As an administrator, you can manage user accounts with these commands:
@pcreate <name>=<password>— Create a player without logging in as them@newpassword <user>=<password>— Reset a user’s password directly@resettoken <user>— Generate a one-time password-reset token (valid 1 hour);
give the token to the player so they can reset viaPOST /api/v1/auth/reset-password@boot <user>— Disconnect a player from the server@toad <user>— Convert a player object into a regular thing (removes player
flag and disconnects)@chown <object>=<player>— Transfer ownership of an object to another player@moniker <player>=<display name>— Set a player’s color-coded display name
(admin or wizard required)
Player targets for @set, @flags, @boot, @toad, and related admin
commands resolve globally (including TinyMUX *Name form), so the
target need not be in your current room:
@pcreate Builder=animefan
@set Builder=superuser
@set *Builder=wizard
User Roles and Permissions
See the Permission levels table in the First-Run Setup
section above. In summary:
superuser(10) — created at first run, cannot be granted in-gameadmin/wizard(9) — granted by a superuser with@setstoryteller(8),builder(7) — granted by an admin with@setplayer(1) — automatic on character creation
To set a flag on a player:
@set <username>=<flag>
To remove a flag (prefix with !):
@set <username>=!<flag>
Bulletin Board Administration
Admins and wizards can create and destroy bulletin boards:
+bbcreate <name>[=<description>] -- Create a board
+bbdestroy <board> -- Destroy a board and all its posts
Board names are slugified automatically (spaces become dashes). Example:
+bbcreate General=General discussion board
+bbcreate Staff=Staff-only discussion
+bbdestroy general
The REST API exposes full board management under /api/v1/boards (see
the REST API section below).
Jobs System
The jobs system lets players submit requests to staff. As staff you have
additional commands:
+job/assign <#>=<player> -- Assign a job to a staff member
+job/status <#>=<status> -- Set status (new/open/pending/in-progress/resolved/closed)
+job/priority <#>=<priority> -- Set priority (low/normal/high/critical)
+job/complete <#>=<resolution> -- Mark resolved with a resolution note
+job/reopen <#> -- Reopen a closed job
+job/staffnote <#>=<text> -- Add a staff-only note (not visible to submitter)
+job/delete <#> -- Delete a job
Job statistics are available via the REST API at GET /api/v1/jobs/stats.
Channel Administration
Admins and wizards can create, configure, and destroy communication channels:
@chancreate <name>[=<header>] -- Create a channel
@chancreate/hidden <name> -- Create a hidden channel
@chancreate/lock <name>=<expr> -- Create a channel with a lock
@chandestroy <name> -- Destroy a channel
@chanset <name>/header=<text> -- Change a channel's header
@chanset <name>/lock=<expr> -- Set a lock expression
@chanset <name>/hidden=true|false -- Toggle channel visibility
@chanset <name>/masking=true|false -- Toggle name masking
Master Room & Zones
Master Room
UrsaMU supports a single designated master room. Commands and $-pattern attributes set on objects in the master room are checked globally — any player anywhere on the game can trigger them, as if those objects were in the same room.
The master room ID is configured via game.masterRoom in config.json:
{
"game": {
"masterRoom": "1"
}
}
ACONNECT and ADISCONNECT attributes fire on both the connected player’s own object and on objects in the master room, allowing global connection hooks without touching the player object.
Zone System
Zones allow a zone master object to share $-pattern commands across all objects assigned to it.
@zone <object>=<zone master> -- assign object to a zone
@zone <object>= -- clear zone assignment
Objects in a zone inherit $-pattern command dispatch from the zone master, making it easy to share a command set across many rooms or items without duplicating attributes on each one.
To see what zone an object belongs to, use examine <object> — the zone master dbref is listed in the output.
Rate Limiting
The server enforces a WebSocket command rate limit of 10 commands per second
per connection. Requests that exceed this are silently dropped and logged to
stderr:
[WS] Rate limit hit for socket <id> (cid: <player-id>)
This is a fixed limit defined in WebSocketService and cannot currently be
configured at runtime.
Server Configuration
Basic Configuration
Run the interactive configuration wizard:
deno task config
Key configuration areas:
- Server ports (Hub WS: 4202, Hub HTTP: 4203, Telnet: 4201)
- Game name and welcome messages
- Starting room ID
Runtime Configuration (@site)
Admins and wizards can change a subset of server configuration values at runtime
without restarting:
@site server.name=My Awesome Game
@site game.loginMessage=Welcome back!
@site game.welcomeMessage=Welcome to the game!
@site server.banner=A roleplay game set in urban fantasy
Allowed keys: server.name, server.description, server.banner,
server.corsOrigins, server.maxConnections, game.maxPlayers,
game.description, game.loginMessage, game.welcomeMessage.
Attempts to set other keys (e.g. server.db, jwt.secret) are blocked and
logged to the security log.
Restarting and Shutting Down
From in-game (admin or wizard required):
@restart/check -- List outdated jsr:@ursamu/* pins (no write, no reboot)
@restart -- Prepare packages online, soft-reboot only if cache OK
@restart/quick -- Soft-reboot only (no git / JSR bump)
@update -- Alias for full @restart
@shutdown -- Shut down the server
Safe package updates — @restart and @update never take the game
down on a bad prepare. While players stay connected they:
git pull --ff-only(stashes local edits if needed)- Bump
jsr:@ursamu/*app pins to latest + dual-package overrides deno cache --reload(pre-warm modules for the next process)- Soft-reboot (exit 75) only if cache succeeded
If pull, pin write, or cache fails, the live process keeps serving the
previous packages. Use @restart/check first to see what would change.
From the terminal (game can stay up during prepare):
bash scripts/safe-update.sh # prepare only (no reboot)
bash scripts/safe-update.sh --reboot # prepare, then soft-reboot if OK
deno task start # Hub + Telnet (foreground)
deno task dev # file-watch development
bash scripts/daemon.sh # supervised production
bash scripts/restart.sh # restart main only (telnet stays)
bash scripts/stop.sh # graceful stop
The supervised scaffold is created automatically by ursamu create. See
Production Deployment for the
signal model (SIGUSR2 = no-disconnect restart; Telnet sidecar persists
across soft-reboot and JWT auto-reauth).
Hot-Reload (@reload)
Without restarting or disconnecting any players, admins and wizards can reload
individual parts of the running server:
@reload -- reload everything (config + text + commands + plugins)
@reload/config -- reload config.json from disk only
@reload/text -- reload text files (motd, etc.) only
@reload/cmds -- reload native commands and system aliases only
@reload/plugins -- hot-reload all installed plugins
@reload/plugin <name> -- hot-reload one specific plugin by name
Example — reload just the jobs plugin after an update:
@reload/plugin jobs
The response shows each subsystem’s status. Partial failures are reported
inline so you can see which component (if any) didn’t reload cleanly.
@reload/plugin <name>is case-insensitive and matches against the plugin’s
registered name. To see what’s loaded:@reload/plugin(no name) prints
the list of currently loaded plugins.
System scripts (
system/scripts/*.ts) are always executed live — the
engine reads and compiles them on each invocation — so they never need a
reload.
Plugin Install Behavior
On startup the engine resolves and installs every plugin declared in
src/plugins/plugins.manifest.json (plus their transitive deps[]). Two
things to know when editing the manifest:
- Optional
deps[].versionsemver range — each dep entry may set
"version": "^1.2.0"(or">=1.0.0 <2.0.0", etc.). The installer
reads the dep’s ownursamu.plugin.jsonversionand aborts the run
if it doesn’t satisfy the range. Entries withoutversioninstall
unconditionally — backwards compatible. - Fail-fast, whole-manifest rollback — if any plugin or transitive
dep fails to clone, has an unsafe name or URL, violates aversion
range, or has incompatible ranges from multiple requesters, the entire
install run aborts. Disk andsrc/plugins/.registry.jsonare left
exactly as they were before the run — your previously installed
plugins are not touched. The error names which entry failed and why.
In practice: after editing the manifest, restart the server. If the run
aborts, fix the offending entry and restart again — there is no partial
state to clean up.
Updating packages without crashing (@restart / @update)
Admins and wizards can refresh git + JSR packages from in-game. The game
stays up through prepare; a soft-reboot happens only after a warm cache.
@restart/check -- read-only: list pins behind JSR latest
@restart -- prepare + soft-reboot if cache OK
@restart <branch> -- pull that branch, then prepare + reboot
@restart/quick -- soft-reboot only (skip git/JSR)
@update / @upgrade -- same as full @restart
Prepare steps (live process keeps serving):
git pull --ff-only(auto-stash local dirty files, then pop)- Merge
config/config.sample.json→ liveconfig/config.json
(adds newserver.pluginsentries andplugins.*blocks such as
map— live secrets are preserved via deep-merge) - Pin every app
jsr:@ursamu/*import to the exact latest on JSR - Write dual-package import overrides so plugins share one mush/core/help
- Delete
deno.lock+node_modules(stale graphs cannot survive) deno cache --reload --minimum-dependency-age=0entrypoints- Print resolved
@ursamu/*versions, then soft-reboot if cache OK
Publish first. @restart only loads code that is already on JSR (or
in the game repo via git / ./vendor/*). Monorepo edits that were never
deno published will not appear after reboot.
On cache success the main process exits 75 (daemon loop restarts it).
On any failure you get %crUpdate failed — game left running%cn and
players are uninterrupted.
Shell equivalent while the daemon is running:
bash scripts/safe-update.sh check # same as @restart/check
bash scripts/safe-update.sh # prepare only
bash scripts/safe-update.sh --reboot # prepare + signal soft-reboot
Telnet survives soft-reboot — the sidecar stays up and JWT reauth
restores sessions without a full disconnect.
Daemon Restart Loop
When started via bash scripts/daemon.sh, the server runs inside a restart
loop (scripts/main-loop.sh) that watches the process exit code:
| Exit code | Meaning | Action |
|---|---|---|
75 |
Restart signal (@reboot, @update) |
Restart after delay |
0 |
Clean shutdown (@shutdown) |
Stop — do not restart |
| Other | Unexpected crash | Stop — check logs |
Rapid-restart protection — if the server exits in under 5 seconds the
restart delay doubles (1 s → 2 s → 4 s → … capped at 60 s). A stable
long-running restart resets the delay back to 1 second.
The deno child PID is written to .ursamu-deno.pid; the loop PID is in
.ursamu.pid. Use bash scripts/stop.sh to gracefully stop both.
Help Administration
The help command is provided by the help-plugin,
which aggregates entries from three sources in priority order:
| Priority | Source | Description |
|---|---|---|
| 100 | Database | Entries created with +help/set — override everything |
| 50 | Files | Markdown files in ./help/ and plugin help dirs |
| 10 | Commands | Inline help: fields from addCmd() registrations |
In-game admin commands
+help/set <topic>=<text> ← create or update a DB entry (Markdown supported)
+help/del <topic> ← delete a DB entry
+help/reload ← bust the file cache after adding/removing files
DB entries immediately override any file or command entry with the same slug.
Use +help/del to restore the underlying file entry.
REST API
GET /api/v1/help ← list all sections and topics
GET /api/v1/help/:topic ← fetch a topic (?format=md for raw Markdown)
POST /api/v1/help/:topic ← create/update (admin token required)
DELETE /api/v1/help/:topic ← delete (admin token required)
Adding help files
Drop .md or .txt files in ./help/ (or a subdirectory) and run
+help/reload in-game. No restart needed. See Writing Help Files
for directory layout and Markdown conventions.
Wiki Administration
The wiki plugin stores articles as Markdown files in ./wiki/ with a
folder-driven URL structure. Admins and wizards manage pages with @wiki
in-game commands.
In-game wiki commands
@wiki/create <path>=<title>/<body>
-- Create a new wiki page. Path mirrors the folder structure.
-- Example: @wiki/create news/patch-notes=Patch Notes/Details here...
@wiki/edit <path>=<new body>
-- Replace the body of an existing page (frontmatter is preserved).
-- Example: @wiki/edit news/patch-notes=Updated content here.
@wiki/fetch <url>=<wiki-path>
-- Download an image or PDF from a public URL into the wiki folder.
-- Example: @wiki/fetch https://example.com/map.png=maps/world.png
-- Allowed types: .jpg .jpeg .png .gif .webp .svg .pdf (max 10 MB)
Security note:
@wiki/fetchblocks private/loopback/link-local IP
ranges (localhost, 127.x, 10.x, 192.168.x, etc.) to prevent SSRF attacks.
Only publicly routable URLs are permitted.
Wiki REST API
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/v1/wiki |
Required | List all pages (title + path) |
GET |
/api/v1/wiki?q=<query> |
Required | Full-text search (title, body, tags) |
GET |
/api/v1/wiki/<path> |
Required | Read a page or list a directory |
GET |
/api/v1/wiki/<path.ext> |
Required | Serve a static asset (image, PDF) |
POST |
/api/v1/wiki |
Staff | Create a page |
PATCH |
/api/v1/wiki/<path> |
Staff | Update body and/or frontmatter |
DELETE |
/api/v1/wiki/<path> |
Staff | Delete a page or asset |
PUT |
/api/v1/wiki/<path.ext> |
Staff | Upload a static asset (binary) |
Example — create a page:
curl -X POST https://yourgame.example.com/api/v1/wiki \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"path": "news/patch-notes",
"title": "Patch Notes",
"date": "2026-03-18",
"body": "## Changes\n- Added new location."
}'
Wiki page frontmatter format:
---
title: My Page Title
date: 2026-03-18
author: Admin
tags: [news, update]
---
Page body in Markdown here.
All metadata keys must match /^[\w-]+$/. Body size limit is 10 MB.
Scene Management
Scenes are collaborative roleplay logs. They can be exported via the HTTP API.
Exporting a Scene
GET /api/v1/scenes/:id/export
Optional query parameter:
format |
Result |
|---|---|
markdown (default) |
A formatted Markdown log with poses and participant list |
json |
Full scene object as JSON |
Example with curl:
# Markdown log
curl -H "Authorization: Bearer <token>" \
https://yourgame.example.com/api/v1/scenes/42/export
# Raw JSON
curl -H "Authorization: Bearer <token>" \
https://yourgame.example.com/api/v1/scenes/42/export?format=json
REST API Reference
All endpoints are served on the Hub’s HTTP port (default 4203). Most require
a Bearer JWT token in the Authorization header, obtained from
POST /api/v1/auth.
Authentication
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/auth |
Returns { token } |
POST |
/api/v1/auth/register |
Create a new character |
POST |
/api/v1/auth/reset-password |
Consume a reset token and set a new password |
GET |
/api/v1/me |
Current user profile |
Players
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/players/online |
List connected players |
GET |
/api/v1/channels |
List channels |
Bulletin Boards
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/boards |
All boards with post + unread counts |
POST |
/api/v1/boards |
Create a board (staff) |
GET |
/api/v1/boards/unread |
Unread summary across all boards |
GET |
/api/v1/boards/:id |
Single board |
PATCH |
/api/v1/boards/:id |
Update board (staff) |
DELETE |
/api/v1/boards/:id |
Delete board + all posts (staff) |
GET |
/api/v1/boards/:id/posts |
Paginated post list (limit, offset) |
POST |
/api/v1/boards/:id/posts |
Create a post |
GET |
/api/v1/boards/:id/posts/:num |
Read a post |
PATCH |
/api/v1/boards/:id/posts/:num |
Edit a post (author or staff) |
DELETE |
/api/v1/boards/:id/posts/:num |
Delete a post (author or staff) |
POST |
/api/v1/boards/:id/read |
Mark board as read |
Jobs
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/jobs |
List jobs (staff see all; players see their own) |
POST |
/api/v1/jobs |
Submit a job |
GET |
/api/v1/jobs/stats |
Counts by status, category, priority (staff) |
GET |
/api/v1/jobs/:id |
Get a job by number or ID |
PATCH |
/api/v1/jobs/:id |
Update status/priority/assignee (staff) |
DELETE |
/api/v1/jobs/:id |
Delete a job (staff) |
POST |
/api/v1/jobs/:id/comment |
Add a comment |
Scenes
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/scenes |
List scenes |
POST |
/api/v1/scenes |
Create a scene |
GET |
/api/v1/scenes/locations |
List accessible rooms |
GET |
/api/v1/scenes/:id |
Scene detail |
PATCH |
/api/v1/scenes/:id |
Update name, desc, status, sceneType |
POST |
/api/v1/scenes/:id/pose |
Add a pose/ooc/set entry |
PATCH |
/api/v1/scenes/:id/pose/:poseId |
Edit a pose |
POST |
/api/v1/scenes/:id/join |
Join a scene |
POST |
/api/v1/scenes/:id/invite |
Invite a player |
GET |
/api/v1/scenes/:id/export |
Export as ?format=markdown or ?format=json |
Wiki
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/wiki |
List all pages |
GET |
/api/v1/wiki?q=<query> |
Full-text search |
GET |
/api/v1/wiki/<path> |
Read page or directory listing |
GET |
/api/v1/wiki/<path.ext> |
Serve static asset |
POST |
/api/v1/wiki |
Create page (staff) |
PATCH |
/api/v1/wiki/<path> |
Update page (staff) |
DELETE |
/api/v1/wiki/<path> |
Delete page or asset (staff) |
PUT |
/api/v1/wiki/<path.ext> |
Upload static asset (staff) |
WebSocket
Connect via ws://host:4202 (or the Hub port). Authenticate either by
sending connect <name> <password> as your first message, or by passing a
JWT at connection time:
ws://host:4203?token=<jwt>&client=web
The client=web parameter enables rich JSON payloads instead of plain text.
Troubleshooting
Common Issues
Server Won’t Start
- Check if a port is already in use (
4201,4202,4203) - Ensure Deno is installed and up to date (
deno upgrade) - Check for errors in configuration output (
deno task config)
Player Can’t Connect
- Confirm the Hub is running (
deno task server) - For Telnet clients, confirm the Telnet sidecar is running (
deno task telnet)
Permission Denied Errors
- Verify the player has the correct flags set (
@set <player>=admin) - The
wizardflag can only be granted by a superuser (@set <player>=wizard)
Logs
UrsaMU logs to stdout/stderr. Redirect output to a file if you need persistent
logs:
deno task server > logs/server.log 2>&1
Password Reset
See the full Password Reset guide for the step-by-step
flow. In brief:
- Run
@resettoken <player>in-game — a UUID token is printed to your session - Pass the token to the player out-of-band (Discord, email, etc.)
- The player calls
POST /api/v1/auth/reset-passwordwith{ token, newPassword } - The token is single-use and expires after 1 hour
Security
Securing Your Server
- Place the Hub behind a reverse proxy (e.g., nginx, Caddy) for TLS termination
- Set up a firewall to limit external access to only the necessary ports (
4201,4202,4203) - Keep Deno and dependencies updated
- Set a strong
JWT_SECRETenvironment variable before starting (see below) - Use strong passwords — the
auth.hashSDK method uses bcrypt
JWT Secret
UrsaMU signs session tokens with a secret key. Set it via environment variable
before starting the server:
export JWT_SECRET="your-long-random-secret-here"
deno task start
If JWT_SECRET is not set, a random secret is generated at startup and a
warning is printed. This means all sessions are invalidated on restart.
Brute-Force Login Protection
The login endpoint (POST /api/v1/auth) enforces a per-IP rate limit of
10 failed attempts per minute. After that threshold is reached the server
returns 429 Too Many Requests and logs the event to logs/security.log.
Security Headers
All HTTP responses include hardening headers:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-origin
Security Logging
Security events are written to logs/security.log:
LOGIN_FAILED— wrong passwordLOGIN_RATE_LIMITED— IP blocked after too many failuresPASSWORD_RESET— password reset token consumedADMIN_BOOT,ADMIN_TOAD,ADMIN_NEWPASSWORD— admin user management actionsADMIN_SITE_SET,ADMIN_SITE_BLOCKED—@sitecommand activityADMIN_RESETTOKEN— reset token generation
The wizard Flag
The wizard flag sits at the same permission level as admin (level 9) but
can only be granted by a superuser. Regular admins cannot elevate another
player to wizard. Grant it with:
@set <player>=wizard