/admin/settings is the way in to the operator console’s configuration, and it holds
three different kinds of thing. Commerce policy — wallet top-up and subscription
concurrency — is stored in the database. Ten declarative installation sections are
stored, versioned, and audited, with their credentials sealed. Two cards are read-only:
the telemetry preview and the backup and restore history.
It is an index rather than a page of forms. Everything used to render in one column —
commerce and its payment providers, all ten sections, the telemetry preview, the backup
history, the diagnostics bundle, and every customer sign-in provider — so finding the
maintenance notice meant scrolling past the payment providers, and opening the screen
fetched all of it whether or not you were looking at any of it.
The diagnostics bundle moved to
/admin/system. Its
route has always been behind system.read rather than settings.read — a bundle
describes the running installation rather than configuring it — and it used to render on
a page an operator holding only system.read could not open.
The distinction that matters is not visible on the screen, so it is stated here first.
Every settings area needs settings.read; every save needs settings.write. The
diagnostics bundle needs system.read and lives on the system screen. See
Admin panel for how those permissions are granted.
Where each value is actually read from
Customer OIDC is the exception worth remembering: those providers are read from the
database on every sign-in, so adding, editing, or disabling one takes effect immediately —
and disabling one ends the sessions it established. Everything else on this page is
either environment-driven or stored and waiting.
Three sections have no environment equivalent at all, and their fields are inert in both
directions:
- Notifications —
failedPaymentThreshold,driftThreshold,outboxBacklogThreshold,quietHoursStart,quietHoursEnd. Nothing reads them. Fulfillment alerting fires after three failed attempts, a compiled-in constant. - Security —
sessionIdleMinutes,sessionAbsoluteHours,requireTotp,allowedOrigins. Operator sessions always use the compiled-in policy: 30-minute idle timeout, 12-hour absolute lifetime, 15-minute token rotation. There is no mechanism anywhere that requires two-factor for an operator account; TOTP is per-account and voluntary. - Required channels —
recheckIntervalMinutes,graceHours,warnBeforeSuspend. The membership re-check runs on a fixed ten-minute loop in the bot.
How a section is saved
Each section is one card with its own Save button. The card footer shows the version it rendered from and when it was last written.1
Open the section
Go to
/admin/settings and pick the area. Every section an area could have is
rendered, not only the ones somebody has already written, so a fresh installation
shows empty cards rather than none.2
Edit the document fields
Text, URL, number, boolean, and textarea fields all belong to the section’s
document. The document never contains a credential — that is enforced by the query
not selecting the secret column, rather than by a filter somebody has to remember to
apply.
3
Type a secret only if you are rotating it
A secret field is always blank, because the API never returns a stored value. Leaving
it blank keeps what is stored. The panel strips blank secrets from the payload before
sending, so re-saving a card cannot clear a credential by accident.
4
Save
The request is
PUT /v1/panel/settings/{section} carrying
{"document": {...}, "version": <n>, "secrets": {...}}. On success the card
re-renders with the new version and updated timestamp.What can refuse the save
What the audit trail records
Every save appendssettings.section.saved in category configuration, targeting
installation_setting:<section>. Its metadata carries the resulting version and the
names of the secrets that were rotated — never their values. “Somebody changed the
Remnawave token on Tuesday” is the auditable fact, and it is the one an incident review
needs.
Secrets are sealed with APP_DATA_ENCRYPTION_KEY under a per-section label, so a
ciphertext moved from one section’s row to another fails to open rather than decrypting
somewhere it does not belong.
The response body of a save reports
secretConfigured from whether this call carried
a secret, so a save that only edited document fields answers false even when a
credential is stored. The panel re-reads the section list immediately afterwards, so
the badge you see on screen is correct; an integration reading the save response
directly should re-read GET /v1/panel/settings rather than trust that field.The section catalogue
Four of these are not fields on the generic renderer.
ai and mcp have their own
screen at /admin/settings/ai — see AI governance and
MCP; theme has its own editor below the branding fields on
/admin/settings/brand, because a colour is not a text field and a logo is not a
settings document; and analytics has one at /admin/settings/analytics, because a
counter identifier has a shape per provider and a verification tag is a name from an
allowlist. All four are still ordinary sections over
GET/PUT /v1/panel/settings/{section}, with the same version guard and the same audit
entry.
The notice wording at /admin/settings/notices is the exception that is not a section
at all. It is a table of overrides rather than one document: a row is the exception and
its absence is the shipped wording, which is what makes reverting a delete and lets a
later upgrade improve a default nobody has touched.
Commerce settings
The two cards at the top of the screen are backed by acommerce_settings row rather
than by a section document, and they are the one part of this screen with real
validation.
The row is seeded from the environment the first time the API starts with an empty table,
so an operator upgrading finds the limits they had already configured rather than schema
defaults. A seed failure is logged and never blocks start-up.
Top-up card. Enabled switch, a read-only currency badge, minimum, maximum, rolling
window in hours, window limit, and a comma-separated preset list — all amounts in minor
units. The currency is not editable because the wallet is per-currency: offering several
top-up currencies would let a customer fund a balance they cannot spend on the plan they
want. Free entry stays available whatever the presets contain, so an empty preset list
means “free entry only” rather than “no top-up”.
Refusals, all 422: a minimum or maximum at or below zero, a minimum above the maximum,
a window at or below zero, and — the one people hit — a preset outside the limits,
because a preset the store would refuse is a button that always fails.
Subscriptions card. A multi-subscription switch and a maximum per customer, which
must be at least 1. Turning concurrency off never closes anything: customers who already
hold several keep them, and the ceiling applies to new purchases only. A plan may narrow
the ceiling through its own limit; neither can widen the other. See
Subscriptions.
Both writes append panel.settings.topup.updated / panel.settings.subscriptions.updated
against settings:commerce.
Telemetry preview
The preview card shows the anonymous installation identifier, whether anything has ever been sent, the total event count, a rendered JSON payload, and a flattened list of its field names. What the card gets right is the shape of the promise: there is no customer count, no revenue figure, and no domain in either the preview or the wire event, because a number small enough to identify an installation is an identifier.Backups and restores
A read-only table of recent backups — when, kind, status, size, verified — with the restore history directly beneath it. A backup nobody has ever restored is a backup nobody knows works, which is why the two sit on one card rather than on separate screens. The panel cannot take or restore a backup. There is no write route for it on/v1/panel
or on /v1/admin; the only two ways to make one happen are the worker’s schedule and the
bot’s /ops surface. See Backup and restore.
The diagnostics bundle
Press Generate and the card fetchesGET /v1/panel/settings/diagnostics, which needs
system.read rather than settings.read — it describes the running installation rather
than its settings, and the people who need it are the people already looking at system
health.
The bundle carries:
- the build version, the Go version, and
GOOS/GOARCH - migration status: applied count, latest version, and whether a migration is marked dirty, read from Atlas’s own revision table
- per section: whether it is configured, whether it holds a secret, and its version
- backup status: total, last taken, last size, last verified, last restore, restore count
- payment provider health and the current maintenance state
- row counts for customers, subscriptions, orders, open tickets, operators, and pending outbox events
- Version is always
dev. No build stamps a version into the binaries, including the released images, so the bundle,/livez, and the telemetry heartbeat all reportdev. Identify a deployment by its image tag instead. - Pending outbox events only grow. Events are written into
outbox_eventsonorder.paid,wallet.credited, andaddon.purchased, and nothing marks them published, so the count rises for the life of the installation and the retention sweep never removes any. It is not a backlog signal, and it is not a sign that something has stalled. See Jobs and queues.
Related
Configuration
Every environment variable that changes behaviour, grouped by concern.
Admin panel
Roles, permissions, sessions, and the audit trail behind this screen.
Anonymous telemetry
The exact event on the wire and how to switch it off.
Backup and restore
Taking, verifying, retaining, and replaying an encrypted dump.