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 startup script pauses and prompts you:
No players found in the database.
Welcome! Let's set up your superuser account.
Enter email address: you@example.com
Enter username: Admin
Enter password: ••••••••
Superuser 'Admin' created successfully!
After setup completes, the Hub and Telnet sidecar start automatically.
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.
Non-interactive environments: If you run
deno task startwithout a TTY,
the prompt is skipped. Rundeno task serverdirectly in an interactive
terminal to complete first-run setup, then switch back todeno task start
for normal operation.
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:
@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)
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
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):
@reboot -- Gracefully restart the server
@shutdown -- Shut down the server
@update -- Pull latest code from git and restart (see below)
From the terminal, use Ctrl+C to stop a running process, then restart with:
deno task start # Hub only (no watch)
deno task dev # Hub + Telnet with file watching (development)
deno task daemon # Background service with restart loop (production)
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.
Updating from Git (@update)
Admins and wizards can update the running server from in-game without touching
the terminal:
@update -- git pull from the default branch (origin/main)
@update main -- pull a specific branch
@upgrade -- alias for @update
The command:
- Broadcasts
%chGame>%cn Updating from git...to all connected players - Runs
git pull origin <branch> - Exits with code 75, which tells the daemon restart loop to reboot the server
Telnet connections survive the reboot — the Telnet sidecar stays up and
reconnects to the Hub automatically.
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/login.
Authentication
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/auth/login |
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/login) 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