veratown-prototype

Configuration Management Web Service

The optional configuration dashboard runs as a separate Railway service. It has no BC connection and is independent from the bot process. The workspace uses the configured room’s effective stored map as a visual index for locations and doors. Typed configuration forms support administration; writes require explicit deployment opt-in and an authenticated admin. Existing deployments remain read-only by default. The original door, shower, and casino views remain available as read-only summaries. Appearance itemsets have a dedicated BC-catalog-backed editor.

The dashboard uses a responsive app shell: a left sidebar (collapsible to an icon rail on tablets, a slide-in drawer on phones), deep-linkable sections (#/workspace, #/walkthrough, …) and a quick-find palette (Ctrl/Cmd+K) for sections and actions. The theme follows the system setting by default; the Theme button cycles Auto/Light/Dark and stores only that preference (and the sidebar state) in the browser’s local storage. The sign-in and invitation pages share the same light/dark styling and load only same-origin CSS.

The Map tools group (Walkthrough, Map analysis, Image export) replaces the retired standalone map-web service and is available to viewers. See Map tools.

Versioned BC room settings

Administrators can open Room settings in Map & management and use its five-step wizard to review and edit persistent room properties: description, background, space, member limit, game, language, blocked categories, access and visibility roles, admin/whitelist/ban member lists, and the supported custom image/music settings. Custom media URLs must be HTTP(S) and contain no embedded credentials or secret query parameters. The connected bot account is retained as a room admin when settings are applied. The room name remains controlled by BC_ROOM; map data is managed separately through stored map revisions.

Each save appends a room-scoped revision to prototypeRoomConfig; optimistic version checks reject stale edits, and the regular sanitized audit records the mutation without copying room-setting values. The UI lists the latest 50 revisions. Restoring an earlier revision appends a new revision; room-setting history is not deleted or automatically pruned. Delete settings override appends the legacy veratownRooms template as the active revision; previous revisions remain available for restore. Delete is offered only when that legacy template exists. When no version exists yet, New BC room settings opens the wizard with the legacy template as its starting value, and the first save creates revision 1. If no version is created in Admin-Map, the first supervisor startup imports the synchronized BC room as the baseline instead.

The web service still has no BC connection. A supervisor with no saved room-settings revision imports the synchronized BC room properties first, preserving the live admin, whitelist, and ban lists as the initial baseline. On each supervisor process startup, the latest saved revision is applied through the journaled BC room-update action and must be confirmed by a matching inbound room sync; a response packet by itself is not treated as success. Failure to observe the requested settings fails startup rather than reporting a successful apply. Web edits do not change the live room immediately: the supervisor applies them the next time it starts. A room-settings change stream refreshes the missing-room creation template, while existing live room state remains BC-observed.

The wizard manages persistent room configuration, not live moderation operations such as moving or kicking individual occupants. Those actions are not represented by a room-settings revision.

Map-centred workspace

Open Map & management to load the same effective stored-map selection used by room creation: the highest validated revision, or the existing legacy fallback. The source and revision are shown; a corrupt head is not silently replaced by an older map.

Map legend and linked records is collapsed by default; expand it to search and browse the map’s linked records.

This is the stored creation map, not a live BC observation. BC uses it when creating a missing room. Saving configuration, restoring a revision, restarting, or rejoining an existing room does not replace that room’s live map. There is no web BC connection, live-map capture or live door control.

Visual layout editing and integrated analysis

Visual map editor paints the stored tile and object layers using searchable pinned-catalog brushes. Tools include single-cell painting, contiguous flood fill, two-corner rectangle fill, rectangular selection and copy/paste of both layers. Paste beyond the 40-by-40 boundary fails without changing the draft. Arrow keys move focus; Enter or Space applies the selected tool. Undo/redo keeps up to 50 edits. Selection, focus and changed cells have distinct outlines. Drag painting interpolates skipped cells and records the whole stroke as one undo step. The hover outline previews the brush position. Choose tiles-only, objects-only or combined visualization; doors and other objects use distinct markers. Ctrl/Cmd+Z undoes; Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y redoes while the editor has focus (native input editing is not intercepted). Selection movement buttons and Alt+arrow keys move the selection outline only, not map contents; copy/ paste transfers selected contents to a new anchor.

Review shows the changed-cell count and the first 100 before/after catalog styles. Confirm save map revision appends an audited layout_edit revision under the normal expected-version check. Effects, map metadata and CellData are preserved; objects with existing private cell metadata cannot be replaced here. The editor never copies sign text or teleporter metadata. Conflicts retain the draft and lock further writes until refresh; a failed refresh after persistence is reported separately, not retried as another save. Deploy the compatible bot build before the first layout save: older readers that do not recognize layout_edit reject that revision rather than silently loading an older map. No database backfill or live-room change is required.

Both layout and ordinary record reviews include Configuration impact review. Layout review lists locations and doors intersecting changed cells, including their trigger and occupancy geometry. Draft health is compared with the stored configuration: new/existing warnings and notices are distinguished, and resolved findings counted. Lists are bounded to 100 affected records and 100 findings. Overlapping regions are valid and do not produce findings. Geometric warnings do not block an explicitly reviewed save, while invalid input, protected metadata and stale versions remain server-rejected.

Map analysis, candidates & walkthrough reuses the existing map analyzer, including uploaded exports, clickable cells, region/fixture inventories, surfaces, adjacency, findings, labeled/candidate/ isometric images and the keyboard-controlled first-person preview. The analyzer uses separate Walkthrough and Map analysis views; the walkthrough pairs the 3D-style preview with a compact map and configured-door list rather than stacking all analysis content into one long page. The admin and analyzer views share the dashboard’s light palette and responsive panel/button treatment. In the stored-map explorer, EmptySpace / Passable is implicit and has no marker; other catalog objects retain their schematic markers.

Analyze the stored layout or the current unsaved visual draft. When the analyzed geometry matches the current managed map, the walkthrough overlays saved door definitions at their configured coordinates, shows enabled/disabled state, keypad and auto-open triggers, and highlights the selected door’s inside region. Configured locked/unlocked preview modes affect enabled configured doors only; disabled doors retain preview openings. Actual door/gate objects and saved door coordinates cut openings through wall tiles; ordinary EmptySpace / Passable objects do not erase walls. Walls, half-walls, water, solid objects, and all four map edges block preview movement, with a small camera clearance to prevent clipping corners. Door frames and lintels remain visible around openings. Beds, seating, tables, storage, vegetation, windows, and other object families use distinct schematic shapes/materials, not BC image assets. Low furniture does not hide walls behind it. Roof/ceiling planes cover exact analyzed interior-region cells (including gaps correctly), while exterior regions remain open to the sky. Interior classification is heuristic, not stored building metadata or proof of a real roof. Walkthrough camera → Walkable isometric offers a second, camera-following 3D-style view with the same player position, collision rules and door previews. W/S or ↑/↓ moves north/south; A/D or ←/→ moves west/east on the map. The current indoor region’s roof is cut away to keep the player visible; other roofs remain. Exterior tiles and pavement stay roofless even in mixed indoor/outdoor regions, so parks, roads, sidewalks and outdoor gates do not acquire inferred roofs. Mismatches between a door’s configured styles and the map object are flagged, and a selected configured door can be opened in its normal editor. These are configuration previews, not live BC state, movement certification or permission checks; access-code values are not sent to the analyzer. If the analyzed map does not match the stored/current draft geometry, managed-door overlays are hidden. Candidate review and import controls are kept in a separate collapsed panel below the explorer so they do not lengthen the walkthrough view.

Analysis is authenticated, bounded, in-memory and available to viewers; it neither writes configuration nor connects to BC. Exports are limited to 64 KiB; geometry is exactly 1,600 cells per layer. Sign text, effects content and private cell metadata are not returned.

Download reports, analysis JSON, exact row-span candidate manifests and import review drafts. Choose include/exclude decisions and candidate identities in the GUI, select a door’s inside region, and explicitly confirm the target room. Undecided candidates can be explicitly excluded together. Export review decisions or reopen a reviewed draft JSON (maximum 2 MiB). The shared importer validates every decision, source fingerprint/catalog, exact geometry and door defaults. Validation opens included records in the normal editors one at a time, with their normal version checks and explicit review/save; there is no bulk import. New candidates start disabled. Region type, door styles, timing and triggers can be refined in the ordinary editors. Unsupported automatic door defaults must be resolved explicitly, not guessed.

The Graphical candidate geometry grid supports mouse and keyboard selection with exact cell highlighting. Toggle multiple regions to merge them without filling gaps or joining disconnected components. Geometry selection is separate from import include/exclude decisions. Open the selection as one disabled new location, or choose an existing region location/door and Replace existing target geometry draft. Existing locations change only region; door targets change only insideRegion, or, when explicitly selected, only the focused door candidate’s position. Identity, enabled state, timing, appearance rules, triggers and other settings are retained. Replacement is not union with the existing target geometry. The ordinary editor still requires review/save with the target’s original version; no bulk write or implicit activation occurs.

Analyzed source geometry may differ from the stored map; this is labelled and does not replace it automatically. Open candidate as disabled draft is a shortcut with a blank new identity, not approval of a complete import draft. Collision/wall previews are geometric heuristics, not actual BC walkability, live permissions or character observations.

Linked configuration editing

The schematic map uses the pinned catalog and actual stored 40-by-40 tile layout, not upstream BC artwork. Zoom and scroll to inspect it; toggle location, door and trigger overlays. The map stays prominent beside the editor while configuration is grouped under a clear resource and record heading. Click a tile to open its square tooltip with catalog tile/object details and any matching locations, doors, keypad/auto-open triggers or inside regions; matching records link to their editors. Overlapping locations remain separate records.

Select a management category and a record, or create a new one:

The Stored map metadata and history category lists stored revisions and backups and offers audited actions: update map metadata, create a backup, restore a backup, and restore an earlier revision. Restores append a new revision (history is never rewritten) and require the expected current revision, so a concurrent change fails with a conflict. They change only the stored creation map.

Helper bot control plane

The Helper bots shortcut opens the database-backed helper administration categories. Authenticated administrators can create or update helper identities and create, update, or delete reviewed public Chat/Emote templates, edit fleet and task limits and the per-helper action budget, inspect recent task lifecycle and current budget counters, and cancel a task only while it is still queued. Identity edits may include the optional abcl capability; this alone does not enable ABCL, which also requires the room-scoped abcl.rules configuration. Identities are bound to the configured room and runtime target; startup locations must be enabled point bot_position locations, and templates must use an enabled location. Templates may include an optional trigger key; the shopkeeper uses enabled shop.* trigger-key templates in the configured shop region. Startup inserts 24 example shop interactions only when their IDs are absent, preserving administrator changes. See the shopkeeper interaction guide. Optional occupied-region flavor schedules can be configured in the helper settings JSON field; see the regional flavor guide for limits, queue behavior, and helper-first deployment.

Helper records are hidden from viewers. Writes use the workspace’s normal administrator, deployment-opt-in, exact-origin, expected-version, transaction, and sanitized-audit checks. The task view omits message payloads and member identifiers. Claimed, running, dispatched, or uncertain work cannot be edited, cancelled, or replayed; roulette tasks remain event-driven and cannot be created from the dashboard. This module does not expose Railway credentials, deployment variables, or restart controls and does not connect to BC. A saved database change is not proof of runtime adoption; use the existing runtime reports for independently reported consumer receipts.

Edits remain in the current browser tab, not local storage. Use Review changes, then Confirm save; the preview names changed fields without echoing secret values. Editing again invalidates that preview. Navigation warns before discarding drafts. Read-only users can inspect records but cannot edit. Conflicts preserve the draft; refresh and review the newer record rather than blindly retrying. A failed refresh after a successful save is reported separately.

MOTD presets and schedule

The MOTD tab manages reusable private greeting messages and the ordered rules that choose them. Messages have stable generated IDs, an operator label, and text limited to 500 characters. Each rule selects a message and may target one or more weekdays (Sunday is 0 through Saturday 6), a start/end time, or both. Leaving weekdays unchecked means every day; leaving both times blank means the rule is weekday-only. Time ranges use the selected IANA time zone, include the start, exclude the end, and may cross midnight. A cross-midnight rule’s weekday refers to the day it starts. Move rules up or down to control which one wins when multiple rules match.

The schedule is the global motd.schedule key in the prototypeGameConfig collection. Save and delete operations use the workspace’s explicit admin write opt-in, revision conflict check, and transactional audit. A missing, invalid, or unmatched schedule uses the bot’s MOTD_TEXT environment value; deleting the schedule restores that fallback. Updates are picked up by the running bot’s existing game-configuration watcher and apply to subsequent character entries. No live BC state changes.

Guided administration

Health and access summaries are stored configuration previews, not live BC authorization, observed appearance, current role evaluation, or proof of runtime adoption. Health checks are deliberately limited and do not replace feature validation. The web application cannot grant a live access decision.

Review, validation and concurrent edits

Review shows a bounded, nested field-by-field before/after table. Numeric and boolean changes and schema-declared choices are visible; free text, song/prompt contents, private metadata and write-only fields are omitted or redacted. Browser validation highlights invalid inputs, and server validation paths mark matching fields while preserving the draft.

After a stale write, Compare with latest loads current stored configuration without changing the draft. Review both changes since opening and the draft’s differences from the latest value. Use latest as base; keep my draft requires explicit confirmation because keeping the whole draft can overwrite another operator’s changes. It does not merge or save automatically: review and confirm again. If the record was deleted, an update is not offered. A later concurrent write still fails the server’s normal version check.

Refresh stored configuration explicitly reloads the workspace, with the normal unsaved-draft warning. If saving succeeded but refreshing failed, further writes stay disabled until a successful reload; do not blindly repeat the write.

History, recovery and runtime reports

History & recovery is visible only to administrators. All recovery reads are admin-authorized, scoped to ADMIN_WEB_ROOM_KEY and the pinned runtime database, and unavailable to viewers. Reads work without enabling writes; snapshot capture and restore use the existing opt-in, admin, exact-Origin and JSON write gates. No new environment variables or BC connection are required.

Audit history

Browse 25 sanitized audit entries per page with Next audit page. A stable timestamp/ID cursor handles tied timestamps without skipping records. Entries expire after 30 days and show category, operation, hashed record identifier and an anonymized actor identifier. Actor identifiers may change after a service restart. The audit contains no previous values and is not an undo log.

Configuration snapshots

Capture snapshot stores version-1 configuration projections using a snapshot-read MongoDB transaction, paired with a sanitized audit entry:

Snapshots exclude credentials, access codes, hidden/private metadata, map sign content, player submissions, sessions, balances, wallets, action journals and heartbeats. Configuration memberships contain their configured principal IDs; they remain administrator-only. Authored songs/prompts are configuration, not player submissions. Never embed secrets or private player content in authored configuration.

Snapshots are bounded to 8 MiB and 8,000 records, 30 days, and 20 per room, whichever limit is reached first. Capture fails explicitly if any configuration catalog exceeds its existing read bound, is invalid, or the stored map cannot be loaded; it does not silently capture a partial catalog. A per-room serialization document makes count pruning transactional under concurrent captures. The newly captured snapshot survives timestamp ties. Expired snapshots are rejected explicitly even before asynchronous TTL cleanup. Snapshot metadata and content live in prototypeAdminSnapshots; an internal per-room guard has no TTL. The collection and cursor indexes are initialized only on writable service startup. Grant its identity the required collection permissions alongside the existing configuration and audit permissions.

Use Review snapshot, filter by category/record ID, select one record and review the bounded redacted before/after comparison. Then tick the confirmation and choose Restore this record. Each request restores one record in its own transaction, validated through the existing feature mutation path with current expected versions and both configuration/recovery audit entries. There is no automatic bulk restore, automatic retry, or deletion of records absent from the snapshot. Restore dependencies in order: locations, doors, groups, then memberships; itemsets/prompts before dependent game settings.

Restores preserve existing access codes and private fields. Protected metadata containers omitted from the snapshot are retained; incompatible replacements or changes to arrays containing hidden fields fail explicitly rather than losing private values. Existing secret-bearing generic settings can be restored through this protected recovery path, while ordinary generic edits remain disallowed. Missing records are recreated without their excluded secrets; set any required access code separately. A restored song keeps its snapshotted enabled setting, including when recreated.

Global configuration, itemsets and prompt catalogs can affect other rooms. Review their shared-scope warning; shared-prompt protections and game activation readiness/release gates still apply. Truth/Dare prompt restores append a new revision from the current latest prompt rather than overwriting history. Stored-map restore appends a revision marked snapshot_restore, preserves current sign metadata and never changes the live room map.

After success or conflict, Reload preview is required before another restore. A successful transaction means configuration was persisted and audited, not that the running bot adopted it. Snapshots are selective configuration recovery, not disaster-recovery backups or a substitute for database backups.

Read-only runtime visibility

Runtime reports lists at most 50 non-expired process heartbeats for the configured logical room, with anonymized instance IDs, reported state and last heartbeat. A report older than 180 seconds is marked stale; a report over 60 seconds in the future is labelled clock skew. A stale ready report is still only a stored report, never a claim that the bot is connected now.

Bot heartbeats now publish optional roomKey without changing lifecycle or shutdown order. Older bots without this field are omitted until redeployed; there is no inferred room association or account/member identifier exposure. An empty list does not prove the bot is stopped. Instrumented bots include bounded consumer receipts, independent of heartbeat freshness:

No automatic polling, restart, live door action or BC operation is exposed.

Database-managed bot instance profiles

The Bot instances workspace resource creates and edits one typed, versioned profile per stable deployment ID. The profile’s schemaVersion is currently 1; every successful create or edit advances its revision and stores an immutable snapshot in prototypeBotInstanceRevisions. Expected-revision compare-and-swap rejects stale edits. Profile writes and the existing sanitized operator audit are committed in the same transaction. Revision history has no TTL. Retirement is a soft delete: it disables and tombstones the profile, preserving its history and preventing the deployment ID from being reused.

The wizard covers role selection (supervisor or helper), role-specific settings, validation/preflight, review, enable/disable, and retirement. The former abcl role is retired; existing profiles can be edited to supervisor or disabled/retired, but cannot start a runtime. The ABCL feature is managed by the supervisor and uses a separate helper process. Helper profiles must reference a supervisor-owned helper record in the same runtime target and room; an enabled profile requires that helper record to be enabled. Profiles currently support the main runtime room. BOT_INSTANCE_ID on the matching container must exactly match the profile’s stable ID.

The profile can hold the non-secret BC server URL, origin and room, the role, helper identity, appearance qualification, and supported position-observation settings. It never stores BC username/password, Mongo URI/database name, or position-observation bearer tokens. Its runtime-target field is scope metadata for mismatch validation, not a database selector; RUNTIME_TARGET and the fixed database mapping remain deployment settings. Keep credentials and tokens in the relevant deployment’s secret/environment settings. Profile changes are desired configuration only: they do not provision, restart, or stop containers. Every profile-controlled setting currently requires an operator-managed restart; when disabling or retiring an already-running bot, stop its container separately. A missing, disabled, deleted, malformed, or wrong-target profile fails closed at startup before BC connection.

Runtime reports shows the profile revision and role actually loaded by each process beside the latest desired revision, role, enabled state, and current, restart_required, or missing status. This reports configuration adoption only; it does not prove that the container has restarted or that BC state was observed.

Appearance itemsets

The Appearance itemsets section builds sets from the pinned, extracted BC catalog without connecting to BC. Search the full catalog by group, category, item name, or description; results are capped so large matches can be narrowed further. Each set item starts from its catalog blueprint and exposes its typed or modular selections under Extended configuration, color layers, reviewed itemset-compatible locks, and catalog-declared fields under Item parameters. Parameters are shown before color and lock settings; defaults are inherited and only overrides are saved. The pinned catalog does not declare a Tightness baseline property, so there is no independent tightness override; use only the extended options offered for each supported item. Select one or more optional appearance groups to strip from the target’s current observed appearance before applying the listed items; strip-only sets are supported. Required groups are not offered. Property controls show inherited catalog defaults and save only overrides; changing the selected asset resets those overrides and other asset-specific settings. The catalog is cached in page memory; itemsets and access identity are refreshed when the editor opens again. Reload the page to fetch a newly deployed catalog revision. Assets with unresolved or unsupported configuration metadata are omitted; server-side itemset validation remains authoritative. The editor lists every discovered BC lock type, but unsupported or non-itemset locks are disabled with their review/credential reason; only reviewed itemset-compatible types can be saved. A location’s map-workspace link opens this same editor with the referenced set selected (or prefilled for creation). Conditional variants can compare observed body traits or check whether a catalogued item’s BC prerequisites are satisfied for the target. For an extended item, the selected option is included when that item is part of the candidate variant. The editor uses catalog selectors only; it has no live BC connection, and unknown runtime evidence never selects a variant. Extended selectors such as shock levels and modular device options are catalog data, not arbitrary property writes. No stand-alone item difficulty override is offered because the pinned metadata does not define a validated range/semantics. Timer locks save the displayed default duration of 300,000 milliseconds (five minutes) even when the duration field is left untouched. Existing explicit durations are preserved; the configuration write API rejects timer locks without a duration rather than saving an itemset that cannot be applied independently. Door appearance selectors use a separate, unfiltered identifier projection of this same catalog; matching an observed asset does not require mutation support.

Create and update operations use the same authenticated, origin-checked, opt-in workspace write path and sanitized audit log. The set key stays stable, the server advances its revision, and an optional catalog pin can prevent use against a different extracted revision. Deletion is not offered because location-entry behavior and shop rules can refer to shared itemset keys. Saving changes only the prototype database; it never applies an appearance to a BC character.

Authentication and transport

The admin service defaults to local passkey authentication; it does not require Cloudflare Access. Local Admin Web password login is disabled unless the environment-operator recovery option is explicitly enabled. Set ADMIN_WEB_AUTH_MODE=cloudflare only when Cloudflare Access is available and deliberately selected. There is no automatic fallback between authentication modes or to unauthenticated access. Local mode ignores Cloudflare identity headers.

Cloudflare mode (existing deployments)

Use Cloudflare Access as the identity-aware proxy in front of the Railway service. Create an Access application for its public hostname, configure an identity provider, and require MFA in the Access policy. The application must have a stable audience/tag and an issuer URL of the form https://<team>.cloudflareaccess.com.

The service independently verifies the RS256 signature of the Cf-Access-Jwt-Assertion against the issuer’s HTTPS JWKS endpoint. It also checks the exact issuer and audience, expiry and not-before timestamps, and requires the signed email to appear in ADMIN_WEB_USERS. It does not trust identity headers without a valid signature or accept the map analyzer’s shared password. Configure every permitted email individually; do not use a wildcard or organization-wide allow rule. Cloudflare Access terminates public HTTPS, and the app remains protected even if someone reaches the Railway origin directly.

Local passkey login and invitations

Admin Web uses passkeys for local sign-in. New accounts are created by an administrator-issued invitation link; email delivery is not used. The map analyzer continues to use its existing environment-configured password accounts. Both services use separate service-specific accounts, origins, and sessions.

Set these Railway variables on each web service:

Variable Value
WEB_AUTH_ORIGIN Exact public HTTPS origin, no path: https://map-admin.netheril.org for admin, https://map-analyse.netheril.org for analyzer
WEB_AUTH_USERS Admin’s environment recovery operator and the analyzer’s configured accounts: email maps to { "passwordHash": "<generated hash>", "role": "admin" } or viewer; 1-50 accounts
AUTHENTICATION_RECOVERY_METHOD Optional Admin Web local-auth recovery setting. The only accepted value is environment-password; unset means passkey-only.

WEB_AUTH_USERS is required for local Admin Web configuration and remains the Map Analyzer’s active account source. For Admin Web it is not a normal password login. Only when AUTHENTICATION_RECOVERY_METHOD=environment-password is set can the configured environment operator use its password as a break-glass recovery method. Other environment entries are not Admin Web accounts. An operator email that also exists in the database receives that database account’s current role; role changes therefore remain effective. Do not enable this option unless the recovery path is needed.

For first-time setup, temporarily set AUTHENTICATION_RECOVERY_METHOD=environment-password, sign in as the environment operator, and create an administrator invitation. Open the one-time link on the recipient’s device and register a FIDO2/WebAuthn passkey; the first database account must have the admin role. After confirming that the new administrator can sign in with the passkey, unset the recovery variable and redeploy for passkey-only operation. You may keep it set for continued operator recovery. Existing password-backed database accounts cannot use their stored password hashes to sign in; while recovery is enabled the configured operator can register their own passkey, and other users should be re-invited after their legacy account is removed.

The invitation URL is shown once in the Accounts & passkeys tab for manual copying; no email is sent. Its random bearer token is carried in the URL fragment, is never sent in HTTP requests or referrers, and is stored only as a SHA-256 hash. Invitations are bound to an email and role, expire after 24 hours, and are single-use, revocable, and replaceable (creating a new invite for the same email invalidates its previous link). Redemption transactionally consumes the invite and creates a passwordless account plus its passkey. If registration fails, an administrator can issue a fresh invitation.

viewer accounts remain read-only. admin accounts can write configuration only when deployment writes are explicitly enabled; account security administration is separately restricted to authenticated local admins and same-origin requests. These variables belong to the separate web services, not the bot. Do not add database or BC credentials to the analyzer.

Admin account, passkey, and invitation records are stored in prototypeAdminWebAccounts, prototypeAdminWebPasskeys, and prototypeAdminWebInvitations in the pinned prototype database. New account documents contain email, role, and timestamps, with no password hash; the reader accepts legacy password hashes but Admin Web never uses them for authentication. Passkey records contain a WebAuthn credential ID, public key, signature counter, and non-secret device metadata—not an authenticator’s private key or biometric/PIN data. Invitation records contain only the hashed token and invitation metadata and are automatically removed after expiry. These records must remain in the isolated prototype database and protected by the MongoDB provider’s encryption at rest and TLS in transit. This implementation does not add a separate field-level encryption key. Configure the database provider’s encryption-at-rest feature; do not copy these collections to the production/source database.

Account admins can issue/revoke invitations, change roles, and delete accounts; the final database administrator cannot be demoted or deleted, and admins cannot remove their own account. Account deletion revokes sessions. An environment operator account cannot be deleted while environment password recovery is enabled. Users can register up to ten passkeys and remove their own credentials; at least one passkey should remain registered.

The local passkey flow requires a discoverable FIDO2/WebAuthn credential and authenticator user verification; registration and assertions are checked against the exact configured origin and RP ID. Generate hashes locally only for the Map Analyzer’s environment password accounts or the optional Admin Web recovery operator. Use a password manager-generated unique password of at least 16 characters (prefer 24+ random characters). In a Bash terminal, this hides input and avoids putting the password in command arguments or shell history:

read -r -s -p 'New web password: ' WEB_PASSWORD
printf '\n'
printf '%s' "$WEB_PASSWORD" | npm run --silent web:hash-password
unset WEB_PASSWORD

The command outputs only the salted scrypt hash (N=32768, r=8, p=3, 64-byte key). Put the entire hash, including $ separators, in the passwordHash field of the relevant service’s WEB_AUTH_USERS value. Share passwords through a password manager, not chat or logs. Example: {"operator@example.org":{"passwordHash":"<generated hash>","role":"admin"}}.

The Admin Web login page is /auth/login; passkey assertions are submitted only through HTTPS. With recovery disabled, the page has no password form. Cloudflare mode delegates authentication factors to Cloudflare Access and does not let this application enforce passkey-only identity-provider policy. After login the browser receives an opaque random, host-only __Host- cookie with Secure, HttpOnly, SameSite=Strict, and no domain attribute. Only a SHA-256 key of the cookie token is stored, and sessions rotate on login and are deleted on logout. Database account status and roles are read on each request, so deleted or downgraded accounts take effect immediately. With the optional environment recovery method enabled, only configured operator credentials can authenticate by password; an existing database account’s current role remains authoritative.

Service Session storage Idle expiry Absolute expiry
Admin dashboard prototypeAdminWebSessions in the pinned prototype database (TTL index) 15 minutes 1 hour
Map analysis web service Bounded process memory; lost on restart 30 minutes 8 hours

Admin sessions survive Railway sleep, redeploys and the idle database disconnect; a database reconnect never requires a new login. If the session store is temporarily unreachable, requests return 503 and the cookie is kept rather than being treated as signed out. The dashboard checks GET /auth/session (which reports expiresAt/idleExpiresAt and never extends the session) only when expiry is near, and shows a warning 5 minutes before the earlier idle or absolute deadline. Stay signed in (POST /auth/session) extends the idle deadline, never the 1-hour limit. Reauthenticate with passkey verifies a fresh assertion and rotates the cookie without reloading, so unsaved drafts stay in the tab. When environment password recovery is enabled, the configured operator also has a password reauthentication form.

The admin service initializes indexes at startup. The Mongo identity needs createIndex, find, insert, update, and remove on prototypeAdminWebSessions, prototypeAdminWebAccounts, and prototypeAdminWebPasskeys, and prototypeAdminWebInvitations. Account, passkey and invitation collections are required for local mode; failure to initialize them stops startup. Invitation redemption uses a MongoDB transaction. If the session TTL index cannot be created, the service warns and falls back to process-memory sessions that end on restart.

Every UI asset and API route requires authentication except the login page and the invite enrollment page, its script, and the token-protected invitation registration and passkey-login flows. Cookie-backed mutations require an exact matching Origin; forged identity and forwarded-IP headers confer no access. HTTPS is required; HTTP is accepted in configuration only for loopback development.

When enabled for Admin Web recovery, password login requests are capped at 4 KiB, 20 attempts/minute per process, five attempts/15 minutes per configured operator (and a shared unknown-account bucket), and two concurrent scrypt verifications. Passkey assertions share the per-account and global attempt limits. Attempts include successful logins. Limits do not trust client-supplied forwarding headers. Responses report generic credential failures and 429/Retry-After when throttled. Persistent lockout or distributed attack protection is not provided: Cloudflare WAF/rate limiting can add a layer, and an attacker can temporarily exhaust login capacity.

Run exactly one replica per web service; disable horizontal autoscaling. Throttles (and map analysis sessions) are process-local and reset on redeploy/restart. To revoke the environment fallback account, remove it from WEB_AUTH_USERS or unset AUTHENTICATION_RECOVERY_METHOD, then redeploy; the password route is disabled when the recovery variable is unset. Database account deletion revokes its sessions; a changed role is used on the next request. Manage database-backed accounts and invitations in the Accounts & passkeys tab. Passkeys require authenticator user verification and are phishing resistant.

Cloudflare and Railway checklist

The UI and API use a restrictive same-origin content security policy, no-store responses, and safe text rendering. Authentication tokens and configuration values are not logged. The dashboard displays the authenticated email and role.

Railway service

Create a new service in the existing veratown project:

Setting Value
Root directory Repository root
Builder Railpack
Build command npm run build:admin-web
Start command npm run start:admin-web
Node.js 24
PORT Leave unset; Railway supplies it

Attach a public HTTPS hostname. Configure the common variables below plus either the local variables above or Cloudflare mode variables:

Variable Required Notes
RUNTIME_TARGET Yes Start with test; only test/veratown_prototype_test or production/veratown_prototype pairs are accepted.
MONGODB_URI Yes Dedicated identity restricted to the selected prototype database; read-only until enabling reviewed writes.
MONGODB_DATABASE Yes Must match the database pinned by RUNTIME_TARGET.
ADMIN_WEB_AUTH_MODE No local (default), or explicit cloudflare when available.
ADMIN_WEB_ACCESS_ISSUER Cloudflare only Cloudflare Access team issuer URL.
ADMIN_WEB_ACCESS_AUDIENCE Cloudflare only Audience value for this Access application.
ADMIN_WEB_USERS Cloudflare only JSON object mapping each permitted email to viewer or admin.
ADMIN_WEB_ROOM_KEY No Room scope shown by the dashboard; defaults to main.
ADMIN_WEB_WRITES_ENABLED No Defaults to false; only exact true enables authenticated administrator configuration writes.

Writes also require WEB_AUTH_ORIGIN in Cloudflare mode. It must be the deployment’s exact public HTTPS origin; local mode already requires it. Leave writes disabled while first configuring authentication and read access.

For example, the user map has the shape {"operator@example.org":"admin","viewer@example.org":"viewer"}. Use actual addresses only in the service’s secret/environment settings, never in source control or logs. Do not configure BC_SERVER_URL, BC credentials, helper tokens, or the source database on this service.

First deploy against the test database with a MongoDB user that cannot write. Verify only the intended room’s data is visible, the selected login/allowlist works (including MFA for Cloudflare mode), and unauthenticated direct-origin requests fail. Do not point the dashboard at production until that review is complete.

Railway Serverless / idle sleep

Enable Serverless in this separate admin service’s Railway settings, then redeploy: the setting only applies to newly created containers. Do not enable it on the BC bot or helper workers, which require persistent connections. See Railway Serverless.

The dashboard validates its database connection and initializes writable indexes at startup as before. After 15 minutes without active database work, it closes the entire admin MongoDB client, including topology-monitoring sockets. Pool idle timeouts alone would leave monitoring traffic running and prevent sleep. A later authenticated database request opens a new client through the same pinned-database guards. Concurrent requests share a connection; ongoing operations, transactions and session cleanup finish before the idle timer starts. Failed operations are reported normally and mutations are never automatically replayed. Shutdown drains requests and clears the idle timer. The bot’s client and configuration watchers are unaffected.

The web UI does not poll in the background; a parked tab, login session, or unsaved local draft does not generate keepalive traffic. The only automatic request is a single session-status check shortly before expiry (plus one when a hidden tab becomes visible again); after a session ends, no further checks run. Admin session lookups use the same lazy connection as configuration reads, so an authenticated request reopens MongoDB; static assets after login and in-memory analysis add no long-lived traffic. Cloudflare signing keys are fetched on demand only. Avoid external uptime probes, scheduled requests, telemetry, or other traffic to this service, and disable Static Outbound IPs if they keep it awake. Railway normally sleeps a container 5–10 minutes after its last outbound packet, so allow that interval after the database disconnect, not just after the last page interaction. Inspect Railway deployment status and network/connection metrics to verify sleep and reconnection on the deployed service; private traffic also counts even when absent from Railway’s public network chart.

Opening the service wakes it automatically, with cold-start latency; Railway warns the first request may return 502. Reload a failed page, but do not blindly repeat a save: reload stored state to determine whether the write completed. Admin login sessions are stored in MongoDB, so a cold wake within the session lifetime does not require signing in again. Save work before leaving; drafts are not server-persisted by sleeping. This implementation permits sleep but cannot guarantee it in the presence of unrelated network traffic.

Startup troubleshooting

The admin container must run npm run start:admin-web, not npm start (the bot). Successful startup logs Admin web service listening with port and authentication mode. Failures log a safe stage and reason; no MongoDB URI, password hash, raw driver error, or stack is printed.

The npm production/--omit=dev warning is not the application’s startup failure. A generic failure from an older deployment does not identify which variable or connection failed; deploy diagnostics before inferring the cause.

Failed /api/workspace reads still return a generic temporary-unavailability response. The service log now includes a safe read stage, error class, and numeric MongoDB error code when available; it omits driver messages, stacks, connection details, and stored values. Use those fields to narrow the failing read before changing stored configuration or database grants.

Authorization and runtime boundary

The dashboard reuses the feature repositories’ validated reads and the existing casino rules schema. It omits keypad group codes and membership records from the door view; shower song text is shown only to an authenticated, allowlisted user. Appearance itemsets are validated and bounded to 100 entries. The configured room key is fixed by service configuration, not selected by an API request.

Legacy summary endpoints remain read-only. The workspace is the only configuration mutation surface and is scoped to the deployment’s fixed room; it cannot select an arbitrary database or impersonate a BC member.

Adoption is separate from persistence

A successful save means configuration was persisted, not that BC acknowledged anything or that a feature already used the new value. Map/location/door and game-config watchers reload their owned configuration; feature-specific settings can instead be read on the next interaction or round. Editing a game’s settings does not cancel, rewrite or settle an active game. Runtime sessions, wallets, ledgers, player submissions, action journals and heartbeats are not editable configuration.

Web login does not grant a BC role. Dynamic room-admin and room-whitelist access groups remain evaluated by the bot from current BC state; the web interface must not present stored membership grants as authority for those principals. The web editor cannot independently perform a live access check.

Rollout

First qualify administration against the isolated test database, including viewer denials, cross-origin write rejection, stale edits, transaction rollback, access-code redaction and reload behavior. Use a dedicated database identity with only the collection permissions needed by the configured administration surfaces and audit history, never a cluster-wide credential. Existing runtime database isolation stays unchanged. Production writes require a separately reviewed deployment decision; this code change does not enable them. The admin service also needs access to prototypeAdminFindingAcknowledgements, which stores room-scoped, reversible finding confirmations separately from the audit log.

Audit retention

prototypeAdminAudit stores sanitized operation metadata atomically with each successful configuration mutation. It does not contain access codes, song or prompt text, map sign contents, complete configuration values or passwords. Entries expire after 30 days through an expiresAt TTL index. Failed writes must not leave a successful audit entry, and audit failures must roll back the mutation. These transactions require a MongoDB replica set.

prototypeAdminFindingAcknowledgements stores the accepted finding identity, the versions of its related records and a hashed actor identifier. It has a room/key index and no TTL because resolved exceptions remain revocable until an admin removes them. The collection is bounded when read by the workspace.