Runic Gateway

A website + game bridge for private Ultima Online (ServUO) shards.

Runic Gateway is a self-hostable platform that gives a UO shard a public site, wiki, and admin panel — and wires it to the live in-game world so the site can show shard status, economy, IDOCs, player activity, per-character sheets, a player-vendor marketplace, and a bestiary built from the shard's own spawn tables, while staff push control commands back into the game. Players get the same public content and self-service on the web or a native Android app. Which audience sees which shard data is admin-configurable, and branding is per-instance.

One installer sets up the shard side.


The pieces

Runic Gateway is six repositories that deploy together but build independently:

Repo Language What it is
website JavaScript (Node + React) The full-stack app — Express REST API + MariaDB + a React/Vite SPA (public site, wiki, admin panel). This is the thing players and staff actually visit, and the single backend every client talks to.
link Rust The uo-link sidecar. Runs next to the shard, terminates a loopback link from the game, and exposes the authenticated WebSocket + REST API the website consumes. The only network-facing half of the bridge.
servuo-plugins C# The ServUO plugin — the shard side of the bridge. Compiled by ServUO at boot; dials the sidecar over loopback and emits game events / accepts commands.
installer Rust The installer — one binary per OS that deploys the plugin and the sidecar onto a shard host as a matched, protocol-checked pair, registers the service, and hands you the values the website needs. How you set a shard up.
Android-app Kotlin (Jetpack Compose) The native Android client — public content + player self-service, purely an API client of the website backend. Same features as the browser client minus every admin console. Never touches the sidecar or shard.
docs Markdown All project documentation — design docs, the wire-protocol spec, the integration guide, the Android plan, research. Start here when you want the why.

How they fit together

                                                              ┌─▶ browser       same-origin JSON / SSE
ServUO shard  ──loopback TCP,──▶  uo-link sidecar  ──WS + REST──▶  website backend ─┤
(servuo-plugins) newline-JSON      (link, Rust)      bearer-auth   (website, Node)  └─▶ Android app   REST + push (ntfy)
  • The shard is never exposed to the internet. It only dials 127.0.0.1. The sidecar is the sole network-facing component, and only the website's backend talks to it.
  • The website backend ingests a live event stream from the sidecar (logins, vitals, economy, vendor sales, deaths, IDOC decay, staff audit…) and makes point-in-time REST calls for rosters and character sheets. It then fans that out to browsers over Server-Sent Events — a public channel (safe kinds only) and an admin channel (everything).
  • The Android app is just another client of the website backend — it speaks the same public REST API and never talks to the sidecar or shard. It self-configures its server URL on first run, so one build works against any shard, and receives push notifications through the shard's self-hosted ntfy relay (no Google Play Services required).
  • Players link a game account to a website account with a one-time in-game code, which is what authorizes character reads. Staff can push town-crier messages and control commands back into the game.
  • Accounts are hardened out of the box — TOTP two-factor with trusted devices, optional SSO (Google / Discord / any OIDC provider, link-only: an external identity must already belong to an account), bot scoring with automatic IP bans, and rate limiting.
  • Who sees which shard data is configurable, not hardcoded: an admin panel decides the audience for each shard surface, and the public SSE channel can never carry a sensitive event kind.

The connection between website and sidecar (URL, shared-secret token, protocol version) is admin-managed in the database, not env — the installer prints those values and you paste them once into the site's Admin → Shard panel. If the sidecar is absent or the shard is down, every shard surface degrades gracefully.


Quick start

The core of a live shard is two things running: the website (site + admin), and the shard side — the sidecar plus the plugin, which the installer deploys in a single run on the machine that hosts your ServUO server. The Android-app is an optional client you point at your running website.

1. Website — the site + admin panel

Prereqs: Node.js 20+ and Docker (for MariaDB).

git clone https://gitea.whitlocktech.com/RunicGateway/website.git
cd website

# Start a MariaDB the backend can reach
docker run -d --name rg-db -p 3306:3306 \
  -e MARIADB_DATABASE=runic_gateway -e MARIADB_USER=runic \
  -e MARIADB_PASSWORD=devpass -e MARIADB_ROOT_PASSWORD=rootpass mariadb:11

# Configure + start the backend (terminal 1)
cp server/.env.example server/.env
#   set DB_HOST=127.0.0.1, DB_PORT=3306, DB_USER=runic, DB_PASSWORD=devpass,
#       JWT_SECRET=<anything>, ADMIN_USERNAME=admin, ADMIN_PASSWORD=<your password>
npm run install-server
npm run server            # nodemon → http://localhost:3000

# Start the frontend (terminal 2)
npm run install-client
npm run client            # Vite → http://localhost:5173

Open http://localhost:5173, sign in at /admin/login with the admin credentials you set, then flip Maintenance → Live on the Dashboard. Tables, defaults, and the first admin are created automatically on first boot.

Production (Docker Compose): the website ships a production-shaped docker-compose.yml that pulls prebuilt app + bot images and serves the built SPA from Express — cp .env.example .env, fill it in, then docker compose pull && docker compose up -d. See the website README for the full options.

2. The shard side — one installer run

Prereqs: a working ServUO install, currently stopped, and Administrator/root. Nothing to build and no Gitea account needed. Grab a binary and SHA256SUMS from the installer releases — releases are unsigned, so the checksum is the trust anchor:

sha256sum -c SHA256SUMS --ignore-missing
chmod +x runicgateway-installer-linux-x86_64
sudo ./runicgateway-installer-linux-x86_64 install
# Windows, from an elevated PowerShell
.\runicgateway-installer-windows-x86_64.exe install

It resolves a published bundle — an exact sidecar + plugin pair CI has checked speak the same protocol, rather than "latest of each" — syncs the plugin overlay into your server tree, offers the optional stock-file patch tier, installs the sidecar and registers it as a service under an unprivileged account, and finishes by printing the four values to paste into Admin → Shard:

Base URL          http://<shard-host>:8080
WebSocket URL     ws://<shard-host>:8080/ws
Protocol version  3
Auth token        4f9c…

Keep that binary — it does not install itself, and doctor, update and uninstall are run from it later. Start ServUO, then ./runicgateway-installer-… doctor checks the deployment end to end, through to "has the shard actually dialed in?" — the only check that tells a working bridge from copied files.

Installing by hand is supported too — INSTALL.md Appendix A is the same deployment with curl, tar and systemctl, for a host that cannot run the binary or for developing on the bridge from a source tree. The full operator guide is docs/installer/INSTALL.md.

3. Android-app — the native client (optional)

Prereqs: JDK 17 and the Android SDK. A single build works against any shard — the app prompts for your website's URL on first run.

git clone https://gitea.whitlocktech.com/RunicGateway/Android-app.git
cd Android-app
./gradlew assembleDebug        # debug APK → app/build/outputs/apk/debug/
./gradlew installDebug         # install on a connected device / emulator

Kotlin + Jetpack Compose (Material 3), min SDK Android 10 (API 29). It surfaces the same public content and player self-service as the browser — home/status, news, wiki, the shard hub, account linking, character sheets — and opts into push notifications through the shard's self-hosted ntfy relay. See the Android-app README and the design contract in docs/android/PLAN.md.


Where to go next

  • Setting up a sharddocs/installer/INSTALL.md — the operator guide: what the run asks, where it writes, the patch tier, doctor / update / uninstall, troubleshooting, and the by-hand path.
  • Running / customizing the sitewebsite README (tech stack, API endpoints, env vars, security, branding, Swagger at /api/docs).
  • The Android clientAndroid-app README and docs/android/PLAN.md — the authoritative design contract, milestones, and the push-notification architecture.
  • The bridge internalsdocs — design docs, the canonical wire-protocol spec, and the integration guide.
  • Working on the sidecar or pluginlink README and servuo-plugins — building from source, the loopback protocol, and the compatibility rules.
Runic Gateway · whitlocktech@gmail.com
Description
No description provided
Readme 70 KiB