Skip to main content
/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.
Saving an installation section stores a versioned document, seals any secrets you typed, and appends an audit entry. It does not change how the api, bot, or worker processes behave. Those processes read their configuration from environment variables once at start-up and never re-read it. To change behaviour, change the variable and restart the process.Treat a saved section as a record of intent — useful, auditable, and not yet a control.
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.
The complete list of variables that do take effect is in Configuration.

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 appends settings.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 a commerce_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.
The card’s on/off state and endpoint come from the telemetry section document, which is empty on a fresh installation — so the card reads enabled: false. The sender reads APP_TELEMETRY_ENABLED, which defaults to true. On a default installation the card says off while the API is sending a heartbeat once a day.The card also renders a different field set from the wire event. Treat Anonymous telemetry as the authority on what leaves the installation, and APP_TELEMETRY_ENABLED=false as the way to stop it.
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 fetches GET /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
It is assembled from an allowlist of fields rather than by dumping state and filtering it. A filter has to be updated every time a field is added; an allowlist fails closed when somebody forgets. So the bundle contains no credentials and no section documents — a branding document is harmless and an operator-group document names a chat, and drawing that line per section is exactly the mistake an allowlist avoids. Two numbers in the bundle need interpreting:
  • 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 report dev. Identify a deployment by its image tag instead.
  • Pending outbox events only grow. Events are written into outbox_events on order.paid, wallet.credited, and addon.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.
A migration status that reads as zeros means Atlas’s revision table does not exist, which is the true answer for an installation whose migrator has not run.

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.