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 via POST /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-game
  • admin / wizard (9) — granted by a superuser with @set
  • storyteller (8), builder (7) — granted by an admin with @set
  • player (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:

  1. git pull --ff-only (stashes local edits if needed)
  2. Bump jsr:@ursamu/* app pins to latest + dual-package overrides
  3. deno cache --reload (pre-warm modules for the next process)
  4. 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[].version semver range — each dep entry may set
    "version": "^1.2.0" (or ">=1.0.0 <2.0.0", etc.). The installer
    reads the dep’s own ursamu.plugin.json version and aborts the run
    if it doesn’t satisfy the range. Entries without version install
    unconditionally — backwards compatible.
  • Fail-fast, whole-manifest rollback — if any plugin or transitive
    dep fails to clone, has an unsafe name or URL, violates a version
    range, or has incompatible ranges from multiple requesters, the entire
    install run aborts. Disk and src/plugins/.registry.json are 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):

  1. git pull --ff-only (auto-stash local dirty files, then pop)
  2. Merge config/config.sample.json → live config/config.json
    (adds new server.plugins entries and plugins.* blocks such as
    map — live secrets are preserved via deep-merge)
  3. Pin every app jsr:@ursamu/* import to the exact latest on JSR
  4. Write dual-package import overrides so plugins share one mush/core/help
  5. Delete deno.lock + node_modules (stale graphs cannot survive)
  6. deno cache --reload --minimum-dependency-age=0 entrypoints
  7. 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/fetch blocks 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 wizard flag 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:

  1. Run @resettoken <player> in-game — a UUID token is printed to your session
  2. Pass the token to the player out-of-band (Discord, email, etc.)
  3. The player calls POST /api/v1/auth/reset-password with { token, newPassword }
  4. 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_SECRET environment variable before starting (see below)
  • Use strong passwords — the auth.hash SDK 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: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin

Security Logging

Security events are written to logs/security.log:

  • LOGIN_FAILED — wrong password
  • LOGIN_RATE_LIMITED — IP blocked after too many failures
  • PASSWORD_RESET — password reset token consumed
  • ADMIN_BOOT, ADMIN_TOAD, ADMIN_NEWPASSWORD — admin user management actions
  • ADMIN_SITE_SET, ADMIN_SITE_BLOCKED@site command activity
  • ADMIN_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