internal/remnawave, which exposes exactly thirteen
operations, and everything else in the codebase takes remnawave.Service, remnawave.Provisioner, or
remnawave.Importer as an interface.
What Remnawave owns
What Omniflow owns
The single join between the two
One column joins the systems:subscriptions.remnawave_user_id, a nullable bigint under a unique
partial index, so two Omniflow subscriptions can never claim the same Remnawave user. Nothing else about
the remote user is duplicated.
The remote username is derived rather than stored: omniflow_ plus the first sixteen hex characters of
the subscription UUID. Because it is deterministic, a create that runs after a crash finds the user it
already made instead of making a second one — the worker looks the candidate up by username before
creating, and installations upgraded from an earlier version also try the older customer-derived name so
the existing Remnawave user is adopted rather than duplicated.
Every write to Remnawave is an Omniflow record first: a fulfillment_operations row with a desired state,
an idempotency key, and a correlation ID, applied by a durable job. A panel click that wrote to Remnawave
itself would be the one change reconciliation could not explain.
What Omniflow deliberately does not hold
- The access link. There is no column for it in
database/schema.sql. It is fetched from Remnawave on each request, and the responses that carry it are sent withCache-Control: no-storeandReferrer-Policy: no-referrer, because the link is a bearer credential: whoever holds it has the customer’s access. Rotation is the only remedy once one leaks, and it is gated on an explicitconfirm: trueplus a recent sign-in. - Hardware identifiers. Neither panel ever receives an HWID. A device is addressed by a digest — the
operator panel by
sha256(hwid)truncated to sixteen hex characters, the customer panel by its own handle — and removal resolves the digest back server-side by listing the customer’s devices and matching. One extra Remnawave call is bought in exchange for the identifier never travelling to a browser. - Squad identifiers in history. A
fulfillment_historyrow recordsendsAt, whether a traffic limit and a device limit were configured, and a squad count — never squad identifiers, links, or provider bodies. - Provider payloads in operator views. The raw webhook body is not returned by
GET /v1/admin/webhooksor the panel’s webhook screen: it is the provider’s own payload and can contain payment details. Job listings summarise arguments and redact the last error. - Anything identifying in telemetry. The anonymous heartbeat carries a schema number, an installation
UUID, the event name, the build version, the service name,
GOOS,GOARCH, and a timestamp. Customer identifiers, Telegram IDs, IP addresses, hostnames, domains, URLs, amounts, currencies, traffic figures, access links, tokens, message contents, plan names, and free text are never collected. See Telemetry.
What goes wrong when the boundary is crossed
Editing Remnawave directly. Nothing stops an operator opening the Remnawave panel and changing an expiry, a device limit, or a squad set by hand. Omniflow does not treat that as authoritative: the nextreconcile pass reads the remote user, compares it with the entitlement, records the difference, and
pushes Omniflow’s intent back. The hand edit is undone, and the reason is that the entitlement is what the
customer paid for.
Deleting a Remnawave user. The reconcile pass records a missing_remote drift rather than silently
recreating state, and the subsequent apply recreates the user under the deterministic username. Nothing is
billed twice and nothing is lost.
Treating Omniflow as authoritative about live usage. Traffic and device counts are read live. When
Remnawave does not answer, the customer overview degrades — each subscription is marked live: false and
the payload degraded — instead of showing a stale figure as though it were current.
Trusting one system’s status for the other’s. Suspending a customer in the operator panel writes an
Omniflow lifecycle event and an audit row; it does not enqueue a provisioning change. The panel says why:
suspension is not deletion, and the entitlement lifecycle belongs to the fulfillment pipeline rather than
to a panel click. To stop access now, run the disable subscription operation, which goes through the
same pipeline as everything else.
Drift: the record of disagreement
Areconcile operation compares the Remnawave user with the entitlement across expiry, traffic allowance,
device limit, Internal Squad set, and status, and writes each mismatch as an entitlement_drifts row with
expected and observed JSON. The kinds produced in practice are missing_remote, expiry, traffic,
device_limit, squads, and status. Drift is always evaluated against the Remnawave user that
subscription owns, so a customer’s second subscription is never judged against the first one’s remote
state.
Operators read it at System → Drift in /admin (GET /v1/panel/system/drift, permission
system.read) or through GET /v1/admin/fulfillment/drifts?limit=<1..500> on the token surface.
Detection and correction happen in the same transaction, so a drift opened by a reconcile pass that then
succeeds is closed microseconds later. The drift screen is therefore a view of disagreements that
corrective work is currently failing to fix, not a general log of everything ever changed in Remnawave.
The
external_edit kind exists in the table’s CHECK constraint and in the API enum, but nothing writes
it — an externally modified user is recorded as the specific field drift instead.APP_RETENTION_DRIFT_DAYS (default
90). Entitlements and fulfillment history are never deleted.
Money and access fail independently
Payment state, entitlement state, and Remnawave state are three different things that fail separately, and the schema keeps them separate on purpose. A successful payment survives a Remnawave outage: the money is recorded, the entitlement is written, and provisioning is retried. Nothing rewrites financial history to match a provisioning failure. Maintenance mode is drawn on the same line. It blocks new purchases and holds outbound provisioning, and it never touches money that has already been taken — settlement, refunds, and order history keep working throughout. A held fulfillment job is snoozed rather than failed: the operation staysretrying with
last_error_code = maintenance_active, its attempt count is decremented back so the hold costs no retry
budget, and the job is snoozed for one minute. See Maintenance mode.
Inside Omniflow: durable, ephemeral, and append-only
PostgreSQL is the only durable source of truth. Valkey holds rate-limit counters (omniflow:rate:<scope>:<subject>), single-use replay tokens (omniflow:once:<scope>:<token>), locks, and
cache. Losing Valkey costs those counters and nothing else — which is why the compose stack runs it with
persistence switched off entirely.
Financial, audit, identity, webhook, and outbox records are append-only unless a documented lifecycle
explicitly permits mutation. Retention reflects that: the hourly sweep deletes expired bot sessions,
abandoned checkout sessions, aged webhook intake rows, expired support attachments, published outbox
events, old telemetry events, and resolved drift rows. It never touches financial, audit, consent,
identity, or entitlement history, which is kept for the life of the installation.
A promo code is never deleted, only deactivated, because the redemptions that reference it are financial
records. A revoked payment method keeps its row so a historical charge still resolves to the method that
made it. Deleting a customer OIDC provider keeps the linked identities, because removing them would
silently take a sign-in method away from customers who use it.
Where configuration is authoritative
One boundary worth stating plainly, because it looks like ownership and is not: the operator panel can store installation settings, but the running processes do not read them.installation_settings rows are seeded empty and are read by exactly two callers — the settings screen
itself and the telemetry preview. The Remnawave URL and token, the bot token, the operator group, the
maintenance policy, telemetry enablement, and the backup schedule are all read from environment variables
once at process start. Saving a section changes what the panel displays, not what the process does, and
rotating the Remnawave token means editing the environment and restarting. There is also no Remnawave
connection test: POST /v1/panel/settings/providers/{provider}/test probes payment adapters only and
answers 422 provider_not_loaded for any other name.
Installation settings covers which sections exist and what each one
currently affects.
Related reading
Remnawave integration
The adapter, every call it makes, the supported version, and health.
Fulfillment
Provisioning operations, retries, reconciliation, and drift recovery.
Subscriptions
Entitlements, concurrency, upgrades, grace, and expiry.
Security
Credentials, sessions, secrets, transport, and privacy boundaries.