Owner console
The web dashboard: login, projects, records, keys and security.
Every server includes a web admin panel for its single local owner: projects, API keys, project settings and a schema-validated record editor. No website account, signup or SSH tunnel is needed.
/login: password login/login/link: single-use login links (see below)/dashboard: projects, keys and settings/dashboard/projects/{projectId}/data: data browser/dashboard/server: live host and process stats (monitoring)/dashboard/server/alerts: alert rules, notification channels and history (monitoring, alerting)
First login: sbox-ns admin login-link
Run this on the server, for example over SSH or through an agent with shell access:
sbox-ns admin login-link # valid for 15 minutes
sbox-ns admin login-link --minutes 5 # 1–60 minutes
It prints a link like this:
https://ns.example.com/login/link?token=3f9c…
Signs in as owner 'owner'.
Single use; expires 2026-10-08 12:15:00 UTC (15 min). Opening the page does not use it up; confirming does.
Open the link and confirm. If no owner exists yet, the page asks you to choose
the owner username and password (at least 12 characters). Being able to run
commands on the server proves you control it, so this works from any address.
The loopback-only /setup?token=… URL in the server log still works as well.
Until an owner exists, /login shows these steps instead of a password form.
How the link works:
- Base URL. The link uses
server.public_urlwhen it is set. Otherwise it is built from the active listener: HTTPS with the ACME domain when TLS is on, or the bound address. For wildcard binds like0.0.0.0it uses the first routable IPv4 address of the machine. Setserver.public_urlif that guess is wrong, for example behind NAT or a proxy.sbox-ns setupandsbox-ns quickstartuse the same rule, but print<this-host>for wildcard binds instead of guessing. - Token. Each token is 256 bits of randomness. Only its SHA-256 hash and
expiry time are stored, as workspace object
server/identity/login-links/<sha256>.jsonin the configured SQLite/PostgreSQL database. The raw token is printed once and never saved. Minting a new link deletes expired ones. - Opening (GET). Opening the link only shows a confirmation page and does
not use the token. Chat apps and link previewers can fetch the URL without
using it up. The page is sent with
Cache-Control: no-storeandReferrer-Policy: no-referrer. - Confirming (POST). Confirming needs an antiforgery token. The server deletes the token and signs you in in one atomic step, so a second attempt gets a 404. Expired, wrong or malformed tokens also get a 404. Each sign-in through a link is logged as a warning with the client address.
- Rate limit.
/login,/login/linkand/setupshare a limit of 10 requests per minute per client IP and 60 per minute across all addresses, and at most 2 passwords are checked at a time. Requests over the limit get HTTP 429.
Forgot your password? Run sbox-ns admin login-link to sign in, or
sbox-ns admin reset-password to set a new password. A reset ends all
existing sessions.
HTTP vs HTTPS
From another machine, the server refuses your password, login links and the
setup form over plain HTTP, because anyone on the network path could read them.
The refusal page lists the ways in: an SSH port forward
(ssh -L 8080:127.0.0.1:8080 you@your-server, then open
http://localhost:8080/login), sbox-ns tunnel enable, or HTTPS. Loopback
connections are always accepted. On a network you trust you can turn the check
off with sbox-ns config set adminpanel.allow_insecure_http true; the panel then
shows a warning banner, and your password and session cookie travel
unencrypted. To use HTTPS, pick one of these:
- Built-in Let's Encrypt:
sbox-ns config set tls.mode acme, then settls.acme_domain,tls.acme_emailandtls.acme_accept_terms = true(see configuration). - Your own certificate: set
tls.mode = "certificate"withtls.certificate_pathandtls.key_path. - TLS-terminating reverse proxy or tunnel on the same machine, such as
Caddy, nginx or Cloudflare Tunnel. The server trusts
X-Forwarded-ProtoandX-Forwarded-Foronly from loopback proxies. Setserver.public_urlto the HTTPS URL.
Cookie protection:
- Both cookies are always
HttpOnly. The session cookie isSameSite=Lax, so a link to the panel from Discord, an alert email or a bookmark keeps you signed in; cross-site form posts never carry it, and every change is a POST that also needs an antiforgery token. The antiforgery cookie isSameSite=Strict. - Over HTTPS they are sent as
__Host-sbox-ns-ownerand__Host-sbox-ns-csrf. That meansSecure,Path=/and never aDomainattribute. Another subdomain of a shared parent domain therefore cannot plant or overwrite them. Over HTTPS the server ignores unprefixed copies of these cookies. - Plain HTTP cannot carry
__Host-cookies, so plain-HTTP access uses the unprefixed names. - Sessions last eight hours and do not slide. Changing the password ends them.
SSH tunnel alternative
If you would rather not expose the panel at all, bind the server to loopback or firewall its port. Then tunnel to it:
ssh -L 8080:127.0.0.1:8080 user@server
Open http://localhost:8080/login locally. Loopback HTTP does not show the
warning banner. sbox-ns admin login-link still works this way: replace the
printed host with localhost:8080.
Connect a game
All projects is the project hub. Each project card is a single link into
that project and shows its status (Enabled or Disabled), hosting
profile (Player-hosted, Dedicated, Hybrid or Not set), live revision (Not synced before the
first sync) and last game request (No requests yet until the game calls).
Above 8 projects a filter matches name or project ID; without JavaScript it
works as a GET ?q= query. With no projects at all the page shows one empty
state pointing at Create project and Import.
Create project, Import and Export server live in the page header
and open modal dialogs. Each dialog form is a plain form POST, so everything
works with JavaScript disabled (the header links then render the same form on
the page, and a failed validation re-opens the dialog with the entered values
and the error). Create project also creates a public key labeled
Game client unless you clear Create a public game key, then lands on the
new project page. Import takes a portable .tar.gz archive (64 MiB
limit); an error is shown inside the dialog. Export server downloads every
project, record, key and setting as one .tar.gz; including secrets needs
your password (and authenticator code when enabled), exactly as before.
The project overview then opens with a Connect your game card:
- The exact
NetworkStorage.Configure( "<projectId>", "<public key>", "<base URL>" );line, filled with the project ID, the oldest enabled public key and the base URL. Without an enabled public key the line shows<public-api-key>. - The same values as fields for Editor > Network Storage > Setup in the s&box editor. The secret key is only shown once, when you create it under API keys.
- The base URL is
server.public_urlwhen set, otherwise the address you opened the dashboard with. The card warns when that address islocalhost(players on other machines cannot use it) or plain HTTP. - A checklist: a public key exists, a collection or endpoint is defined, the game reached the server (with the last request from the request log), and 401/403 responses in the last 24 hours with what each status usually means.
Copy buttons use the browser clipboard. Browsers only allow that over HTTPS
or on localhost; elsewhere the button selects the text and asks you to
press Ctrl+C.
Every change that redirects (saving settings, keys, records, definitions, rules, webhooks, pages, tests, imports, deletes) shows a one-shot toast naming what changed. It appears once and never reappears on refresh; without JavaScript the same message sits as an inline notice.
Creating a key redirects back to the overview and shows the new raw key there once. Reloading the page does not create another key or show the raw key again. Revoking a key and deleting saved tests, rate limit rules, webhook profiles and pages ask for confirmation in a dialog first; deleting a project or a record still needs its ID or key typed, checked by the server.
All projects keeps a Use with a coding agent card with copyable
commands for connecting Claude Code or another MCP client to sbox-ns mcp
over SSH, a local .mcp.json, the Claude Code plugin, and the three
mcp.allow_* switches with what each allows (coding agents). Each
project overview has a copyable first prompt that names the project ID.
Project settings
The overview keeps name, description, Enabled and Require s&box authentication open. The other settings are folded and closed by default. Project settings also has an optional colour. It tints the project tile on All projects and defaults to Automatic, which keeps the generated colour. Every project gets a generated tile with its initials, using the chosen colour or a generated one when Automatic is set.
Hosting profile
How the game is hosted is Player-hosted (a player's listen server holds
only the public key), Dedicated (your own box holds a secret key) or
Hybrid (both). Create project in the dashboard defaults to
Player-hosted; sbox-ns project create and MCP create projects as Not set
unless you pass --hosting. The profile changes no request behavior. It only
tells the authority check which findings apply (hosting models).
Authority check
The project overview has an Authority check card listing game data a client or untrusted host could still change. It is advisory, runs only when you open the page, and never touches the request path:
- a collection with
accessMode: public(clients read and write it directly); - an enabled endpoint that writes or deletes with no
conditionorassertstep (this heuristic can flag valid endpoints that validate some other way); - an endpoint with
requiresSecretKeyin a Player-hosted project (no trusted caller exists to use it); - a secret API key seen on game routes in the last 200 logged requests of a Player-hosted project (a secret cannot be held safely there).
The same check runs as sbox-ns project authority <projectId> [--json],
sbox-ns doctor --project <projectId> and the read-only MCP tool
project_authority.
The CLI and MCP output also runs a data check that the dashboard card does not:
it scans the live records of every defined collection for documents whose only
field is ops, a list of operation objects. sbox-ns 0.4.0 stored an
update-ops request as the whole document instead of applying it, which
replaced the player's data. Releases after 0.4.0 apply the operations. The
scan reports each affected collection with a count and sample keys
(legacy-ops-record in text, legacyOpsRecords in JSON). The replaced data
is not recoverable from the database: restore a backup taken before the bad
write, or let the player save again. Records in collections that have no
definition are not scanned.
Auth sessions
With Enable auth sessions, the game can trade its s&box token for a signed
session token (/v3/auth-sessions/{projectId}/create). Later calls send the
session token and skip the s&box check until it expires after Session TTL
seconds. Off by default.
Encrypted requests
Enable encrypted requests is reported to the client library in the signed security config, and the library then encrypts endpoint calls (AES-256-GCM with a key derived from the public key, signed with HMAC-SHA256). This server decrypts such calls and rejects replays, but still accepts plain calls, so it does not make encryption mandatory. It accepts request IDs up to 120 seconds old whatever the window setting says; the window value is passed to the client library.
Player key mode
Sets the record key endpoint steps use for the calling player (playerKey).
Player uses the Steam ID. Player save uses {steamId}_{saveId} when
the call input has a saveId, for games with save slots, and the Steam ID
otherwise.
Legacy player projections
A built-in repair and leaderboard projection for endpoint saves into the
players, skills, kills and leaderboard_global collections. Projects
that existed before the setting was introduced keep it on; new projects start
with it off. Leave it off unless your game was built on those collections.
Revision policy
These settings are stored with the project and returned to the client library and the editor Sync Tool. This server never blocks a request because of them. It only answers revision-init with whether the client's game revision is outdated (revision handshake). Any blocking, banner or popup happens in the client library.
Library compatibility
Choose Clients from the project overview. The local, 90-day client-version report shows self-reported library/package and game revisions, requests, approximate distinct players and compatibility status. Preview a minimum before changing it; saving an explicit minimum requires acknowledgement, owner authentication and an antiforgery token, and is audited. Older runtime clients that report a library version are blocked only after that opt-in (or a narrow advisory-linked block); clients that report none are never blocked, and a server update never enables the minimum. Clear the minimum to restore default acceptance. Players need a republished game, not a backend update, to receive a new library. Pull/Test Live remain available.
The optional affected-traffic alert threshold uses the existing Discord/SMTP channels. Reports are batched once per minute and player counts are approximate, not security attestation. See compatibility.md for the limits, statuses, release evidence and deprecation guarantees.
Data browser
On a project page, choose Browse data, or open
/dashboard/projects/{projectId}/data.
- Collections: every synced collection with its type (per-player or global) and live record count. Deleted (tombstoned) player records are not counted. Without collections, the page links to the Collections editor and to the Sync Tool setup.
- Find player: enter a Steam ID to list that player's records in every
per-player collection: the record whose key is the Steam ID and save slots
whose key starts with
{steamId}_(up to 50 per collection; open the collection to see more).?player=takes the same value. - Records: sorted by key, 50 per page by default (
?size=accepts 1–200). Each row shows version, last change, size and a short preview. Key starts with (?q=, up to 256 characters) lists keys that begin with the text; keys are case-sensitive. An empty collection offers Create record and a link to endpoint tests. - Record detail: pretty-printed JSON in an editable payload form. Records
of a player (key is a Steam ID, or starts with
{steamId}_) link to Test an endpoint as this player, which opens Endpoint tests with that Steam ID filled in (/tests?steamId=), and to all records of that player. - Create: choose Create record on a collection. Player keys accept 1–256 letters, numbers, underscores, hyphens or colons; global record IDs accept 1–128 letters, numbers, underscores or hyphens. Global records are shared entries, not player records.
- Save: payloads must be JSON objects within the configured store byte limit and match the collection schema (including required fields, types, enums, nested objects/arrays and supported size/range constraints). Unsupported schema modifiers fail closed with an explicit error rather than being ignored.
- Download:
…/data/{collectionId}/exportreturns the whole collection as JSON. The file containsformat = "sbox-ns.collection-export", the collection type, and each record's key, version, change time and payload.
The browser reads through the storage contract, so it behaves the same on SQLite and PostgreSQL. Counts and pages are queried from the database, so large collections are not loaded into memory; only Download reads the whole collection.
All mutations require the owner session and an antiforgery token. Edit and delete forms carry the version and a protected snapshot of the original payload and change time. The database compares this state and changes the record atomically, including when game writers reuse a version number. A stale form receives HTTP 409 and cannot overwrite or delete changed content. Create uses an atomic missing-record check, so two create forms cannot overwrite one another. Player tombstones can be recreated; owner saves increment the row's version without changing the existing game API's version behavior.
Validation errors (HTTP 400) and conflicts preserve the entered JSON and key. Use Reload current record to compare the latest stored value, then reconcile your changes before submitting again.
Deletion additionally needs the exact record key typed as confirmation:
- Per-player records get a deletion tombstone, just like a delete through the storage API.
- Global records are removed from the global record table.
Creates, edits and deletions are written to the project audit log as
record.create, record.update and record.delete, with the collection, key,
collection type and expected version.
Authoring and runtime diagnostics
The project overview links to collection, endpoint, workflow and query
lists and game values. Each list is a sortable table with one row per
resource: the name opens a detail page, and Quick preview opens the same
summary in a dialog (or the detail page without JavaScript). The detail page
has Overview, Edit and Code tabs as server links (?tab=), so
each tab works without JavaScript. Overview shows stored stats and a
read-only outline with the full source. Edit keeps the definition editor
full width. Code keeps the Use from the game guide full width. Game
values render the detail layout directly, with no list. Old
.../resources/<kind>?id=<id> links redirect to the detail page, and
?draft=<id> redirects to .../resources/<kind>/new?draft=<id>.
Author definitions in YAML; the dashboard stores
your source text and compiles it with the same compiler as editor sync.
Raw JSON is still accepted for older edits but is deprecated. Saving preserves
source metadata and updates only the selected resource, not other definitions.
Saving redirects to the detail page Edit tab of the saved id.
Saved definitions are available to the runtime immediately. Editor sync can
overwrite dashboard edits, so reconcile changes with your checked-in YAML
source. Endpoint expressions use double braces, such as {{steamId}} and
{{player.level}}.
The Edit tab keeps the definition editor with Guided editor, Raw source and Examples. Collections, endpoints, workflows, queries and game values all open in Guided editor. Check definition and Save live definition stay beside the editor. Changes stay local until you save.
Guided editor shows the definition as collapsed blocks. Open a block to edit its options. Fields use typed controls: dropdowns for fixed choices, on/off switches for booleans, and text fields for the rest. Maps and lists nest inside blocks. Expand all opens every block; Collapse all closes them.
Add buttons open a dialog. Pipeline steps come from a searchable palette grouped by purpose: Data, Logic, Values, Random, Flow and Integrations (22 step types). New steps get a unique id based on the step type. Blocks offer Duplicate and Remove, and fields can be renamed. Reorder with the handle: drag it, use the up and down buttons, or press Alt+Up and Alt+Down on the handle.
Guided edits write small targeted changes into the source text. Comments, unknown fields, quoting and formatting survive, and large integers keep their exact digits. Switching tabs never rewrites the source. Raw source is the same authoritative text with highlighting. Without JavaScript the raw textarea still edits and saves the full source.
Saving parses the YAML on the server. A value that reads as a number or
true/false is stored as that type even when it is quoted, so a string value
such as "200" comes back as the number 200. Game values are stored without
their source text: after a save the editor shows the server's rendering of the
stored values, without comments or the original formatting.
Some sources need Raw. Invalid YAML, several documents, a top-level list or
scalar, and definitions that carry authoritative sourceText show a notice
in Guided and leave the source untouched. YAML aliases and anchors also
route to Raw: alias blocks show an Edit in Raw button, and structural
edits through a shared reference are rejected with a message. Failed edits
never half-apply.
The list and detail pages have a New button. It asks for an id (letters, numbers,
underscore or hyphen, 1-128 characters) and opens a pristine
.../resources/<kind>/new?draft=<id> template with that id. Nothing is published until you save. Leaving with
unsaved changes still triggers the navigation warning.
YAML, JSON and C# have local syntax highlighting. Click a source line number to select that line, or use Go to line. The line-number gutter also supports arrow keys, Home and End. Copy source copies the full textarea, not the highlighted markup. Tab inserts two spaces; Shift+Tab leaves the textarea. Ctrl+S / Command+S saves the definition. Unsaved source and unapplied block inputs trigger a navigation warning. Review inserted fields and expressions before saving.
Open the Examples tab to browse complete definitions grouped by category. Each card shows a Preview source view and a Use example button. Replacement asks for confirmation in the shared confirm dialog when the editor is not empty. Saving stays a separate action. Review the replacement before saving.
A new collection starts as accessMode: endpoint: game clients reach it only
through endpoints and queries, and direct document calls answer
403 ENDPOINT_ONLY. Game client access (accessMode: endpoint or
public) and Deletes from game clients (allowRecordDelete) sit with
the storage kind. See
collection access from game clients.
A save that fails validation keeps your text and lists each diagnostic as
Error CODE at path: message, the same wording Check definition uses.
Check definition validates the current text without saving or executing it. It reports server diagnostics, not a guarantee of runtime success. If the text changes during a check, check again. A failed request never counts as successful validation. Catalog failures offer Retry examples while direct editing stays available. Create needed definitions reports created, existing and failed dependencies separately; retrying keeps definitions that already exist.
Analytics, audit/request logs, errors and usage tabs read stored runtime data. Empty panels mean no matching data has been recorded, not synthetic activity. The console does not replace every screen or workflow in the managed dashboard: it covers project settings and keys, records, collection, endpoint, workflow and query definitions, game values, endpoint tests, version history, rate limit rules, webhooks and pages.
Use from the game
The detail page Code tab shows how to use the open collection, endpoint, workflow, query or game values, with a Copy button on every block. The card has tabs for copyable C# and a prepared agent prompt. Workflows add an endpoint-step tab with the YAML step that runs them. Internal endpoints show an endpoint-step tab and an agent prompt instead of game code. New drafts get the same card with skeleton values: it says to save first and shows no Test this endpoint link. The prompt includes the current editor source, with unsaved changes. Every block names the live project and resource. No block ever contains a secret key.
- Setup: the exact
NetworkStorage.Configure( "<projectId>", "<public key>", "<base URL>" );line, with the oldest enabled public key and the dashboard base URL (server.public_url, otherwise the dashboard address). Without an enabled public key the card says to create one under API keys. It warns when the base URL is loopback-only or plain HTTP. - Endpoints:
await NetworkStorage.CallEndpoint( "<slug>", new { ... } );with the input filled from the endpoint'sinputschema (declared defaults, otherwise a value of the declared type), followed by theTryGetLastEndpointErrorcheck. Theendpoint_snippetMCP tool returns the same code. Links open Endpoint tests with the endpoint selected and its Version history. Secret endpoints say to call them from a dedicated server, never from the game client. Internal endpoints never run over HTTP: the card shows the calling route (action: run,flow: <slug>) instead of game code. - Collections with
accessMode: public:GetDocumentandSaveDocumentfor the player's own document (per-player) or a named record (global), with the saved object built from the schema. The card also says when deletes from game clients are off. Other collections name an endpoint that reads or writes them and show its call, because direct calls answer403 ENDPOINT_ONLY. A link opens the collection in the data browser. - Workflows: game code can never run a workflow directly. The card shows the
endpoint step that runs it (
type: workflow,workflow: <id>, params from the workflow'sparams), names the endpoints that run it when any do, and shows the C# that calls the first one. Links open its Version history. - Queries:
await NetworkStorage.RunQuery( "<queryId>" );. The card says results are shared between callers for the cache TTL, and that forced fresh runs need a secret key. Secret queries say to run them from a dedicated server only. - Game values:
await NetworkStorage.GetGameValues();, which reads every group and table in one call. The card lists the saved groups and tables and says values are read-only from game code. - Agent prompt: a prepared prompt that names the project ID and resource, and
tells the agent which
sbox-nsMCP tools to use (definition_get,endpoint_test,logs_requests,errors_recent,data_records,definition_check) before changing anything. Paste it into a session after connecting the agent (see coding agents).
Pages
Pages publishes markdown or key/value pages that games read without a
key. An open page shows its absolute public URL, built like the Connect card's
base URL (server.public_url, otherwise the dashboard address), and a
Fetch it from the game example. The response is JSON with title,
updatedAt and markdown (markdown pages) or data (key/value pages).
Request log and errors
Logs lists recent data-plane requests (/v3, /v1, /api/storage,
/api/network-storage, /pages, /api/pages) with time, method, path,
status and duration:
- Authenticated requests are listed under their project.
- Rejected requests that never authenticated (status 400 or more, such as a wrong API key or failed s&box authentication) are listed under the project ID in the URL, but only when that project exists.
- Successful unauthenticated requests (for example
/v3/server-info) and OPTIONS preflights are not listed.
Errors lists errors the server reported for the project: unhandled exceptions on a project route, endpoint failures and conflicts, storage failures and unconfirmed client saves. Each row shows the error code, message and source, plus up to 4,000 characters of the stack trace when there is one. Messages and stack traces can contain player data. Request-backed errors also show the library version, client context, package revision and game revision. Background errors have legacy/unknown context and blank revisions; identity is self-reported, not an authentication claim.
Both tabs explain common statuses and codes (UNAUTHORIZED,
SBOX_AUTH_FAILED, ENDPOINT_ONLY, RECORD_DELETE_DISABLED, FORBIDDEN
and others) in a What it means column; see
error codes.
To keep the database small, each project records at most 20 successful
requests, 20 rejected or failed requests and 20 errors per minute. Rows are
written within a few seconds and deleted after 7 days. For more detail, read
the server log with sbox-ns logs -f.
Move one project
On the project overview, Download project archive exports that project's
metadata, definitions, records, runtime data and stored API-key material as a
private .tar.gz. It excludes instance configuration and private secrets,
including signing keys. On the destination dashboard, upload it under
Import a portable project.
Import retains the original project ID and assigns its owner membership to the destination instance's owner. An existing project ID is refused rather than overwritten. Limits are 64 MiB compressed and 256 MiB expanded. This is self-hosted SQLite/PostgreSQL portability, not a managed-host export contract.
After moving to an independently configured instance:
- Create a new secret key for editor sync and dedicated servers. Secret-key lookup identifiers depend on the instance's encryption secret; copied hashes alone do not make the old secret key usable on a new instance.
- Point the game client and editor at the destination's base URL.
- Authenticate player sessions again; instance signing/session secrets are not part of the project archive.
Keep the archive private: it contains player data and public runtime credentials. For whole-instance disaster recovery, use server export/import with the required configuration/secrets, not a portable project archive.
Themes and visual editing
The theme selector offers System, Dark, Light, Cool dark, Cool light, Mint, Slate and Warm. Dark (green) is the default; Light is also green, and the Cool themes are the blue Sky palette. System follows your operating system. Your choice is stored in this browser only; it does not change other operators' dashboards. All themes work without external assets. The shared design rules live in UI guidelines.
Record payloads and endpoint-test inputs open in Visual when they can be
represented without loss. Edit strings, numbers, booleans, objects and arrays
without writing JSON, add or remove nested fields, then switch to Raw JSON
or save directly. Unknown fields are kept. Duplicate keys, precision-sensitive
numbers and definitions with authoritative sourceText stay in raw mode.
Both modes use the same server-side schema validation, permission checks and conflict detection.
Collection, record, key, activity, client-version, rate-limit and saved-test
tables have clickable column labels. Click once for ascending order and again
for descending; the arrow and aria-sort state show the current direction.
Sorting only reorders displayed rows. Record pagination, database ordering and
server-side filters remain unchanged. Action/preview columns and ordered
execution steps are not sortable.
Optional admin security
Open Admin security to add a time-based authenticator. On a phone, open
Add to authenticator app: it is an otpauth://totp/ link (issuer
sbox-ns, your owner name, SHA-1, six digits, 30 seconds) that authenticator
apps open directly. Elsewhere, add the displayed secret manually. Then confirm
your current password and a six-digit code. Enrollment expires after ten
minutes. The server does not activate 2FA until confirmation succeeds.
Save the ten recovery codes offline. Each works once. Enrollment and local
reset invalidate existing owner sessions. Password reset does not remove 2FA.
With local server access, recover using sbox-ns admin reset-2fa.
The authenticator is encrypted with a key kept in the data folder, which an
export does not include. After a restore on another machine, sign-in says the
server cannot read your authenticator: use a recovery code, or run
sbox-ns admin reset-2fa and sign in with your password.
Shell-issued login links remain a local-operator recovery capability, so
protect SSH access and the configuration/data directories too.
Disable the web admin surface with:
sbox-ns adminpanel disable
sbox-ns service restart
This denies login, setup, login links and dashboard requests at the server
boundary. It does not disable game APIs. Configuration changes take effect
after restart; sbox-ns adminpanel enable restores access after another restart.
If running in the foreground, stop and start the process instead.
Allow only specific admin IPs or CIDR networks:
sbox-ns config set adminpanel.allowed_ips "192.0.2.10,2001:db8:1234::/48"
sbox-ns service restart
Replace those documentation addresses with yours. An empty list allows all addresses. The policy uses the trusted connection address, not arbitrary forwarding headers. The built-in proxy trust is loopback, one hop. Confirm the observed address and your reverse proxy configuration before restricting access, especially with a Cloudflare tunnel; keep local recovery access available.
Turnstile is optional. Create a widget for your exact public admin hostname
(including your sboxns.com tunnel name when applicable), then configure
adminpanel.turnstile.enabled, adminpanel.turnstile.sitekey, adminpanel.turnstile.secret and adminpanel.turnstile.hostname as described
in configuration. Keep the secret in a private config file
or a protected environment variable, not shell history. The server checks
success, action and hostname with Cloudflare before login/setup/link consumption,
and denies requests when verification fails or Cloudflare is unavailable.
Turnstile does not replace 2FA, passwords, rate limits or IP restrictions.
Public read-only demo
adminpanel.demo_read_only=true is for a dedicated demonstration instance,
not for making a production database public. It requires SQLite and refuses
an existing project database without its demo marker. Start with a new, private
configuration and data directory.
The instance seeds clearly labeled generated player progress, inventory, world state, definitions and diagnostic rows into its real database. Visitors can browse anonymously and explore local editor changes. Every HTTP mutation, owner login/setup/security/export operation and game execution route is denied server-side. Health and server-info remain available. No production credentials or player data should ever be copied into this instance.