> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Installation settings

> What the Settings screen stores, which of it changes behaviour today, and where each running value actually comes from

`/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.

| Area | Holds |
| - | - |
| `/admin/settings/commerce` | Currency, top-up limits, subscription policy, payment providers |
| `/admin/settings/integrations` | Remnawave, the Telegram bot, the operator group, required channels |
| `/admin/settings/operations` | Maintenance, notifications, backups, telemetry — plus the telemetry preview and backup history |
| `/admin/settings/brand` | Branding, [appearance](https://omniflow-vpn.mintlify.site/operations/branding.md), and the security section |
| `/admin/settings/connect` | [Connection guidance](https://omniflow-vpn.mintlify.site/operations/connection-catalogue.md): the platforms and applications customers are told to install |
| `/admin/settings/notices` | [Message wording](https://omniflow-vpn.mintlify.site/operations/message-wording.md): the transactional notices the installation sends on its own |
| `/admin/settings/analytics` | [Advertising measurement](https://omniflow-vpn.mintlify.site/operations/advertising-measurement.md): counters, site verification, and the click identifier carried into an order |
| `/admin/settings/sign-in` | The customer sign-in providers |
| `/admin/settings/ai` | Model providers, per-feature enablement, budgets, MCP connections |

The diagnostics bundle moved to [`/admin/system`](https://omniflow-vpn.mintlify.site/operations/admin-operations). 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.

> [!WARNING]
>
> 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](https://omniflow-vpn.mintlify.site/operations/admin-panel.md) for how those permissions are granted.

## Where each value is actually read from

| Settings card | Stored in | Read at runtime | Change behaviour with |
| - | - | - | - |
| Wallet top-up | `commerce_settings` | No — limits, presets, and the rolling window come from the environment | `APP_WALLET_TOPUP_*` |
| Subscriptions | `commerce_settings` | Partly — the customer web panel reads `multi_subscription_enabled` to decide whether to show the subscription switcher | `APP_MULTI_SUBSCRIPTION_ENABLED`, `APP_MAX_SUBSCRIPTIONS_PER_CUSTOMER` |
| Branding | `installation_settings` | No | `APP_PUBLIC_URL`, `APP_TERMS_URL`, `APP_SUPPORT_URL`, `APP_DEFAULT_CURRENCY` |
| Appearance | `installation_settings`, `branding_assets` | **Yes** — both panels read it on every render, behind a one-minute cache | Save and reload; there is no environment variable |
| Remnawave | `installation_settings` | No | `APP_REMNAWAVE_URL`, `APP_REMNAWAVE_TOKEN` |
| Telegram | `installation_settings` | No | `APP_TELEGRAM_TOKEN`, `APP_TELEGRAM_WEBHOOK_URL`, `APP_TELEGRAM_WEBHOOK_SECRET` |
| Operator group | `installation_settings` | No | `APP_OPERATOR_CHAT_ID`, `APP_OPERATOR_NOTIFICATION_CAP`, `APP_OPERATOR_NOTIFICATION_WINDOW_MINUTES` |
| Required channels | `installation_settings` | No | *(no equivalent; see [Required channels](https://omniflow-vpn.mintlify.site/operations/required-channels.md))* |
| Maintenance | `installation_settings` | No | `APP_MAINTENANCE_AUTO_DETECT`, `APP_MAINTENANCE_FAILURE_STREAK`, `APP_MAINTENANCE_RECOVERY_STREAK` |
| Notifications | `installation_settings` | No | *(no equivalent; thresholds are compiled in)* |
| Telemetry | `installation_settings` | Preview only — see below | `APP_TELEMETRY_ENABLED`, `APP_TELEMETRY_ENDPOINT` |
| Backup | `installation_settings` | No | `APP_BACKUP_ENABLED`, `APP_BACKUP_ENCRYPTION_KEY`, `APP_BACKUP_DIR`, `APP_BACKUP_INTERVAL_HOURS`, `APP_BACKUP_RETENTION_DAYS` |
| Security | `installation_settings` | No | *(no equivalent; session lifetimes are compiled in)* |
| Payment providers | `payment_provider_settings` | No — the adapters the API and worker actually call are built from the environment at start-up | `APP_TELEGRAM_TOKEN`, `APP_CRYPTOBOT_TOKEN`, `APP_CRYPTOBOT_TESTNET`, `APP_YOOKASSA_SHOP_ID`, `APP_YOOKASSA_SECRET` |
| Customer sign-in (OIDC) | `customer_oidc_providers` | Yes | The panel itself |

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](https://omniflow-vpn.mintlify.site/getting-started/configuration.md).

## 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

| Response | Meaning |
| - | - |
| `409 conflict` | The version you sent is not the current one. Somebody else saved this section while your screen was open; the card shows a conflict alert and asks you to reload. Saving again over it would discard whatever their change was, which is why it is refused rather than merged. |
| `422` | An unknown section name, or a document that is not valid JSON. |
| `403` | Your roles do not hold `settings.write`. |

### 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.

> [!NOTE]
>
> 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

| Section | Document fields | Secrets |
| - | - | - |
| `branding` | `serviceName`, `supportContact`, `publicUrl`, `termsUrl`, `defaultLocale`, `timezone` | — |
| `remnawave` | `baseUrl`, `reconcileIntervalMinutes`, `compatibilityChecked` | `token` |
| `telegram` | `botUsername`, `webhookUrl`, `webhookEnabled` | `botToken`, `webhookSecret` |
| `operator_group` | `chatId`, `createTopics`, `notificationCap`, `windowMinutes` | — |
| `required_channels` | `recheckIntervalMinutes`, `graceHours`, `warnBeforeSuspend` | — |
| `maintenance` | `autoEnable`, `failureThreshold`, `noticeEn`, `noticeRu` | — |
| `notifications` | `failedPaymentThreshold`, `driftThreshold`, `outboxBacklogThreshold`, `quietHoursStart`, `quietHoursEnd` | — |
| `telemetry` | `enabled`, `endpoint` | — |
| `backup` | `enabled`, `intervalHours`, `retentionDays`, `directory` | `encryptionKey` |
| `security` | `sessionIdleMinutes`, `sessionAbsoluteHours`, `requireTotp`, `allowedOrigins` | — |
| `theme` | The palette, corner and spacing scales, offered modes, default mode — see [White-label appearance](https://omniflow-vpn.mintlify.site/operations/branding.md) | — |
| `analytics` | `enabled`, counter identifiers per provider, webmaster verification tags — see [Advertising measurement](https://omniflow-vpn.mintlify.site/operations/advertising-measurement.md) | — |

Four of these are not fields on the generic renderer. `ai` and `mcp` have their own
screen at `/admin/settings/ai` — see [AI governance](https://omniflow-vpn.mintlify.site/operations/ai-governance.md) and
[MCP](https://omniflow-vpn.mintlify.site/operations/mcp.md); `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](https://omniflow-vpn.mintlify.site/commerce/subscriptions.md).

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.

> [!WARNING]
>
> 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](https://omniflow-vpn.mintlify.site/operations/telemetry.md) 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](https://omniflow-vpn.mintlify.site/operations/backup-restore.md).

## 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](https://omniflow-vpn.mintlify.site/operations/jobs-and-queues.md).

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.

## Related

> **[Configuration](https://omniflow-vpn.mintlify.site/getting-started/configuration.md)**
>
> Every environment variable that changes behaviour, grouped by concern.

> **[Admin panel](https://omniflow-vpn.mintlify.site/operations/admin-panel.md)**
>
> Roles, permissions, sessions, and the audit trail behind this screen.

> **[Anonymous telemetry](https://omniflow-vpn.mintlify.site/operations/telemetry.md)**
>
> The exact event on the wire and how to switch it off.

> **[Backup and restore](https://omniflow-vpn.mintlify.site/operations/backup-restore.md)**
>
> Taking, verifying, retaining, and replaying an encrypted dump.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.