Run, 15 minute read
Web administration, status and Lua HTTP extensions
Run rift --config examples/admin.lua, then open http://127.0.0.1:8080.
The admin website shows listeners, backends, routes and live counters, edits the
complete Lua source, validates changes, saves and applies them, and reloads edits
made on disk. Its managed servers panel reports lifecycle state and requests
server starts and stops, creates instances from service groups, and removes
instances with explicit file retention/deletion labels. It also provides live server
logs and console commands, field-based group/template editing, configuration
deployment history, rollback and operator audit records. The status website runs separately at http://127.0.0.1:9090.
The dashboard assets are embedded in the binary; no Node installation or
separate frontend server is needed at runtime.
This guide covers web and status HTTP services. The operational admin
endpoint used by rift admin is a separate loopback JSON-line protocol with an
environment-provided secret and explicit operation permissions; see the
operator guide. Both can run
on distinct ports. admin.permissions do not govern HTTP requests: the web
credential grants the enabled HTTP API capabilities, including configuration
editing and managed server lifecycle operations. Named web.operators can receive
restricted permissions and group scopes. Changes to operational admin and
static managed_servers definitions require a restart. Group/template changes
can apply live when existing instances retain their process definitions and
storage policy; remove affected instances before changing those settings.
Configuration
Add these optional fields to your existing Lua configuration:
web = {
enabled = true,
listen = "127.0.0.1:8080",
ui = true,
api = true,
-- token = "a-long-random-secret-at-least-16-characters",
},
status = {
enabled = true,
listen = "127.0.0.1:9090",
ui = true,
metrics = true,
},
metrics = false, -- Or "127.0.0.1:9092" for a separate metrics-only server.Each server is disabled when omitted or set to false. web and status also
accept enabled = false, which retains the other settings in the source.
An enabled empty table uses the defaults above. web.ui controls the admin
HTML/assets; web.api controls the built-in /api endpoints. Lua /ext routes
remain available with api = false. The bundled admin dashboard needs the API
to display or edit data; disabling the API is useful for custom Lua sites.
status.ui controls the status HTML/assets, and status.metrics controls its
Prometheus endpoint. The status JSON endpoint stays available whenever its
server is enabled. metrics = false disables the standalone metrics listener;
internal traffic counters and authenticated /api/metrics remain available.
The web, status and metrics settings, their addresses and web.token can
change through hot reload.
An unchanged address with port 0 keeps its assigned port; actual bound
addresses appear in logs and status JSON. New service sockets bind before a
change is committed. A failed HTTP save retains the previous file, runtime
configuration and services. A rejected reload preserves runtime and services
without undoing a file already edited externally. Moving a service onto a port
still occupied by another Rift service is rejected; use a spare port or separate
reloads. Gameplay listener names/addresses and the separate operational admin
bind, token-variable and permissions require a restart.
Turning off or moving the admin server may make the current browser connection unavailable. Re-enable it by editing the Lua file and sending SIGHUP (Unix) or Ctrl-Break (Windows), or restart Rift. The save response is allowed to finish when disabling its own server. Accepted proxy sessions retain their existing routes and are never interrupted by an HTTP configuration change.
Authentication and access
Loopback web binds may omit token for local development. A non-loopback bind
requires web.token or at least one named operator token. Tokens contain 16–4096
printable, non-whitespace ASCII characters. When credentials are configured,
every /api and /ext request requires:
Authorization: Bearer your-tokenThe HTML shell and static assets contain no private configuration and load
without authentication. Enter the token in the website's connection form; it is
held in memory and forgotten on page reload. API responses never include a
separate token field, but authenticated /api/config returns the original Lua
source, including any credentials it contains. The source is administrative data.
Tokenless access accepts only loopback IP or localhost Host headers. Requests
with mismatched Origin headers or cross-site Fetch Metadata are rejected, and
mutations require JSON. There is no CORS allowance or cookie-based login. Use
HTTPS at a trusted reverse proxy or an SSH tunnel for remote administration;
the built-in listener speaks HTTP. When proxying, preserve the browser's Host
header and use a token.
Named operators use independent tokens and permission lists:
rift.config.web = {
listen = "127.0.0.1:8080",
token = "replace-with-a-random-administrator-secret",
records_directory = "operator-records", -- Relative to the config directory.
operators = {
lobby_staff = {
token = "replace-with-a-different-random-staff-secret",
permissions = { "read", "logs", "console", "servers", "audit" },
groups = { "lobby" },
},
observer = {
token = "replace-with-an-independent-observer-secret",
permissions = { "read", "logs" },
},
},
}| Permission | Access |
|---|---|
read |
Status, metrics and scoped group/server listings; include it for dashboard use |
logs |
Read managed server logs within the operator's scope |
console |
Send commands with the managed server's console privileges |
servers |
Start/stop servers and create/remove instances within scope |
config |
Read, validate, edit and reload complete Lua source and structured definitions |
deploy |
View configuration deployment metadata and restore retained deployments |
audit |
View operator records; group-scoped operators see only their own records |
extensions |
Invoke configured Lua /ext handlers |
Omitting groups permits all groups and static managed servers. A list restricts
server actions and logs to instances of those groups; an empty list permits no
server targets. Scoped listings omit other groups/servers, backend routing and
configuration paths. Global counters and listener information remain visible.
config, deploy and extensions require unrestricted scope because Lua and
configuration changes can affect the whole proxy. config exposes any secrets
in the Lua source and can change credentials. The legacy web.token and
credential-free loopback access retain full privileges. Once any named operator
is configured, loopback requests also require a valid token. Tokens must be
unique, names must be distinct from reserved system/admin audit identities,
and permission/credential changes apply on the next request after reload.
GET /api/access reports the current identity and grants without credentials.
The separate status and metrics servers are read-only and unauthenticated. Their
JSON includes backend/listener addresses, health eligibility, counters and
service settings, but no Lua source, token or configuration filesystem path.
Bind these servers to the monitoring interface appropriate for that information.
A healthy backend may still be administratively draining and reject new
attachments. With health checks disabled, an up value does not imply a successful
probe. health_checks_enabled reports the policy; the operational CLI and
rift_backend_draining/rift_maintenance_mode metrics expose effective maintenance
state, including runtime overrides.
API
All bodies and errors are JSON except Lua responses and Prometheus metrics.
Errors use {"error":"description"}. GET /api provides endpoint discovery.
| Method and path | Purpose |
|---|---|
GET /api/access |
Current operator name, permissions (null for full access) and group scopes |
GET /api/status |
Runtime version, uptime, revision, actual listeners, backends, health, managed server state, service groups, routes, fallbacks, limits, counters and enabled services |
GET /api/servers |
{servers: [...]} with managed server lifecycle state and usage |
POST /api/servers/{name}/start |
Request a managed server start with {}; returns 202 when accepted |
POST /api/servers/{name}/stop |
Request an unused managed server stop and pause automatic wake with {}; returns 202 when accepted |
GET /api/servers/{name}/logs?cursor={byte_offset} |
Up to 64 KiB of log text with the next byte cursor, truncated and has_more; omit cursor to tail the log |
POST /api/servers/{name}/console |
Write one {command: "list"} to a running managed server; commands are limited to 4096 bytes without control characters |
GET /api/definitions |
Structured templates, service_groups and the active revision; includes private paths and executable arguments |
PUT /api/definitions/{section}/{name} |
Apply {revision, definition}; section is templates or service_groups, and null removes a definition |
GET /api/deployments |
Retained deployment metadata, active revision, retention limit and durability flag; never includes source |
POST /api/deployments/{id}/rollback |
Validate and restore a retained configuration with {revision} |
GET /api/audit |
Latest operator records and durability flag |
GET /api/groups |
{groups: [...]} with group names, inclusive port ranges, instance names, template and storage policy |
POST /api/groups/{name}/instances |
Submit provisioning with {}; returns 202 with {operation_id, status: "pending", poll} (autostart follows group policy) |
DELETE /api/instances/{name} |
Submit removal of an unused instance with {}; returns 202 with {operation_id, status: "pending", poll} |
GET /api/operations/{id} |
Retrieve instance operation state and its completed result; requires current servers permission and the operation's group scope |
GET /api/metrics |
Counter values as JSON |
GET /api/config |
{source, revision, writable} for the active configuration |
POST /api/config/validate |
Validate {source} including live listener restrictions; does not save or bind sockets |
PUT /api/config |
Validate, atomically save and apply {source, revision} |
POST /api/reload |
Reload the selected file, with an empty JSON object {} |
GET /status |
Public JSON on the separate status server |
GET /metrics |
Prometheus text on the status server when enabled, or the standalone metrics server |
Managed lifecycle requests return promptly; acceptance does not mean startup or
shutdown has completed. Poll GET /api/servers for state, pid, players,
reservations, automatic_start and last_error. automatic_enabled reports
whether an administrator has paused automatic starts. restart_attempts counts
automatic restarts since the last explicit Start, and restart_exhausted
identifies failed services requiring operator intervention. The same array is available
as managed_servers on authenticated /api/status. Listings include address,
port, group (null for static servers), template (null without one),
and storage (persistent or disposable; static managed servers are
persistent). Authenticated /api/status also includes service_groups with
the same objects as /api/groups, including the optional scaling policy.
Commands, arguments and
working directories are omitted. The public /status and Lua HTTP context omit
managed lifecycle details entirely. Authenticated /api/config still contains
the complete trusted configuration source.
A manual stop is rejected while players or backend attachments use the server.
The reservations count includes both pending and established attachments;
it overlaps the tracked player count rather than adding more players to it.
Once accepted, it pauses automatic wake until an explicit start or proxy restart;
idle shutdown preserves automatic wake. Unknown backend names return 404,
existing unmanaged backends return 400, conflicting lifecycle requests return
409, and an unavailable/full supervisor queue returns 503. Start failures
after acceptance appear in last_error. See managed servers
for configuration and shutdown behavior. Managed definitions and their backend
addresses require a proxy restart; the configuration API cannot change them live.
Service-group operations allocate loopback ports and register/remove instance
backends without a proxy restart. Creation and removal return 202 promptly,
with an operation ID and polling path (also in the Location header).
Poll that path once per second using the same authorization header. A 200
poll response has operation_id, operation, target and status:
pending, succeeded or failed. Successful operations include result with
the instance details or removal outcome and http_status (201 for creation,
200 for removal). Failed operations include error and http_status:
port exhaustion or an occupied instance is 409; unknown groups/instances
are 404. A failed operation is distinct from a failed polling request.
Authentication, authorization, malformed bodies and queue/tracking capacity
errors still reject the submission immediately.
Tracking permits at most 32 pending operations and 256 total records;
capacity exhaustion returns 503 without submitting work. Completed results
are retained for 10 minutes, then polls return 404. Records are in memory
and do not survive proxy restarts. Pending operations retain their slots
until completion, regardless of HTTP disconnections. Each poll checks current
permissions and the group saved at submission, including after removal.
The dashboard polls through long operations, displays operation failures and
cleanup warnings, and identifies unresolved operations if polling fails.
Instance registration
survives configuration reloads and ends when the proxy restarts. Instance
removal runs asynchronously through its shutdown deadline. Persistent instances retain their world
and other files; disposable instances delete their generated directory after
the child exits. Stop/start preserves files for both storage policies. Successful
file deletion returns files_removed: true; persistent removal returns false.
If deletion fails after the child is reaped, the backend is still removed and
the operation result includes files_removed: false and cleanup_error; inspect the
remaining directory. Failed provisioning does not register an instance.
Template source assets are read-only inputs, and no EULA acceptance is generated.
See templates and storage for asset
layout and persistent directory reuse across proxy restarts.
Configuration saves preserve the source exactly, including comments, functions,
computed values and formatting. The source is the single editable configuration;
there is no generated JSON override file. Send the opaque revision from the last
GET /api/config with every save. A stale revision or a detected external file
change returns 409; fetch/reload and review before retrying. Changes on disk
are applied only by an explicit reload or signal, not by a file watcher. The
website polls active state and preserves unsaved editor changes when another
client applies a revision.
The structured editor writes a single generated Lua wrapper around the original
script, preserving its comments, hooks and computed values. Field overrides are
ordinary Lua source and remain visible in /api/config. Definitions use the
same fields as Lua configuration. Structured reads return resolved absolute
paths; optional fields are omitted. Updates replace one complete definition,
validate all references and live-instance restrictions, then use the same save
transaction as source edits. Templates affect future provisioning; existing
instance files are preserved. Scaling policies can change while instances run.
The dashboard follows selected server logs every second, retaining a bounded
128 KiB view. Logs contain server output and may include private information;
grant logs accordingly. Console requests write exactly one line to the owned
process's stdin, with a bounded write deadline. A successful response confirms
the write; observe server logs for the command's result. Console commands have
the server's full console powers, including commands affecting players or files.
Deployment history retains the latest 64 successful source configurations, including startup, HTTP saves, CLI/signal reloads and rollbacks. Rollback checks the supplied active revision and external disk edits, validates the candidate, reserves sockets, then atomically saves/applies it. Rejection preserves the working file/runtime. Restore uses the retained entry Lua source and current local module/plugin files; it does not restore asset contents, executable files, runtime instances or worlds. Restoring creates a new history entry.
Records default to a private <config filename>.operators directory beside the
configuration. Set web.records_directory to writable storage when the config
mount is read-only; changing that directory requires a restart. Unix directories
use mode 0700, files 0600. Retained deployment sources may contain credentials;
protect and back up this directory as administrative data. With no file-backed
configuration, or an unwritable default directory, records stay in memory and
the APIs report durable: false. An explicitly configured unwritable records
directory fails startup. A post-commit history write failure is reported in Rift's
stderr; the already-applied configuration remains active.
HTTP mutations record the named actor, operation, target, timestamp and admission
result. Authentication/scope denials, local CLI commands and signal reload results
are also recorded. Bodies, console text, authorization headers and query strings
are excluded. Lifecycle accepted means queued; follow server state for completion.
An intent record precedes HTTP/CLI mutation execution; a timeout can leave an
intent without a final result. HTTP/CLI operations are refused if that intent
cannot be written. The API retains the latest 1024 audit records; the journal
compacts at 8 MiB. These are local records, not a tamper-proof external audit log.
Validation errors and failed socket reservations return 400. Missing or
invalid tokens return 401, denied browser origins return 403, unknown or
disabled endpoints return 404, oversized bodies return 413, and incorrect
mutation content types return 415. Busy HTTP/configuration/Lua capacity returns
503. Lua execution errors return 500, and an async Lua deadline returns 504.
For example, using Python's standard library (set RIFT_WEB_TOKEN to the
configured web.token value if needed; the server does not read this environment
variable automatically):
import json, os, urllib.request
base = "http://127.0.0.1:8080"
headers = {"Content-Type": "application/json"}
if os.environ.get("RIFT_WEB_TOKEN"):
headers["Authorization"] = "Bearer " + os.environ["RIFT_WEB_TOKEN"]
def api(method, path, value=None):
body = None if value is None else json.dumps(value).encode()
request = urllib.request.Request(base + path, body, headers, method=method)
with urllib.request.urlopen(request) as response:
return json.load(response)
current = api("GET", "/api/config")
# In a real script, make an intentional change to current["source"].
updated = current["source"] + "\n-- Reviewed through the HTTP API\n"
api("POST", "/api/config/validate", {"source": updated})
print(api("PUT", "/api/config", {
"source": updated, "revision": current["revision"]
}))A save needs write access to the configuration file and its parent directory.
The supplied systemd and Compose examples default to read-only configuration;
see the operator guide before enabling
browser saves in those environments. A save writes a new sibling file, preserves the original access permissions,
flushes it and atomically replaces the selected file. Configuration evaluation
and disk work run on blocking workers, outside gameplay I/O. The selected path
is canonicalized at startup, so a symlink selects its target. Source size is
limited to 256 KiB; the JSON envelope allows escaped source bytes. Saves and
signal, HTTP API and rift admin reload requests share one serialized
transaction path with a bounded queue.
Each HTTP server admits at most 64 open connections, with a 10-second socket
lifetime covering headers, request bodies and response writes. Retiring servers
allow up to three seconds for existing responses to finish. Gameplay admission
and Lua routing capacity remain independent of these HTTP limits.
External file conflict checks are best-effort: an editor writing during the final check-and-rename interval can race a save. Use the admin API for competing automated writers, or finish an external edit and reload it before editing in the website. File replacement is atomic, but does not promise power-loss durability of the directory entry.
Lua websites and endpoints
The optional on_http callback handles every HTTP method at /ext and /ext/*.
It has the same web bearer authentication and browser-origin protections as the
admin API. Use it for network dashboards, maintenance information, deployment
metadata, diagnostic pages and application-specific API responses. Paths outside
this namespace remain owned by Rift.
on_http = function(request)
if request.method == "GET" and request.path == "/ext/connections" then
return {
status = 200,
content_type = "text/plain; charset=utf-8",
body = tostring(request.context.metrics.active),
}
end
if request.method == "GET" and request.path == "/ext/help" then
return {
content_type = "text/html; charset=utf-8",
body = "<!doctype html><title>Support</title><h1>Contact the network operator</h1>",
}
end
return nil -- 404 for paths this script does not handle.
end,The request table contains method, path, query (raw query string without
?), UTF-8 body, lowercase headers, and context (the public status object
as a Lua table). Authorization, cookie and proxy authorization headers are
removed. JSON arrays become one-based Lua arrays. The context is an isolated
snapshot; modifying it cannot change the live runtime.
Return nil, or {body = "...", status = 200, content_type = "text/plain"}.
Only body is required. Status must be an integer from 200 to 599; 204, 205 and
304 require an empty body. The MIME value is validated, arbitrary response
headers are unavailable, and both request and response bodies are bounded to
256 KiB. HTML from a custom handler inherits the service's Content Security
Policy: external resources and inline scripts/styles are blocked. Escape any
request-derived text before putting it into HTML.
HTTP hooks reuse Rift's restricted Lua VM: fresh state per invocation, 8 MiB memory limit, 100,000-instruction budget and 50 ms deadline. Four HTTP script workers are shared across reloads, independently of routing hook capacity. Scripts can import local modules and folder plugins from their saved snapshot (see Lua API); they have no filesystem, sockets, dynamic/native module loading, OS or debug access. They cannot call arbitrary URLs or mutate the running configuration; configuration writes use the revision-checked admin API. Hook/source changes become visible on the next successful reload. The bundled admin extension console can exercise methods, paths and request bodies without leaving the dashboard.
For Rust integrations, rift::http_script::{HttpScript, HttpRequest, HttpResponse, HttpError} provides an owned, thread-safe interface. Obtain the
script from Config::from_lua(...).on_http; async hosts call
script.execute(request).await. The synchronous evaluate entry point accepts
an explicit deadline and is intended for callers that own their worker policy.
Development checks
Run cargo test --locked --all-targets and
cargo clippy --locked --all-targets -- -D warnings for Rust coverage, including
real-socket HTTP and proxy integration tests. node src/web_assets/tests.js
runs the frontend behavior harness using only Node's standard library; Node is
needed only for this optional local test command, never for building or running
Rift. CI runs both suites. The frontend harness exercises API interactions and
editor state transitions; it is not a browser rendering test.