Skip to main content
Once a day the API process sends one HTTPS request to the project’s collector. It carries an anonymous installation identifier, the build version, the service name, the operating system, and the CPU architecture. Nothing else. It is enabled by default, it never blocks anything, and one environment variable stops every request. Telemetry exists because Omniflow is self-hosted, which means maintainers otherwise have no idea which platforms and versions are actually in use. Knowing that a build is running on linux/arm64 decides whether an architecture keeps getting release images. Knowing which versions are still live decides how long a migration baseline is supported. Neither question needs a customer, an amount, or a domain, which is why none is collected.

The exact event

This is the complete JSON body, field for field:
The wire format also declares optional features and counters maps. No Omniflow process ever populates them, so they are omitted from every request. There is no module inventory and no usage counter in what is sent today; if that changes, this page and the public schema change in the same release.

Never collected

  • Customer identifiers, Telegram IDs, usernames, or email addresses
  • IP addresses, hostnames, domains, or URLs
  • Payment amounts, currencies, wallet balances, revenue, or provider payloads
  • VPN traffic, access links, subscription URLs, nodes, or device identifiers
  • Tokens, secrets, message contents, plan names, ticket text, or any other free-form data
  • Row counts of any kind — a number small enough to identify an installation is an identifier

Timing and failure behaviour

One heartbeat goes out immediately when the API starts, then one every 24 hours. The HTTP client has a 5-second timeout and the sender discards the result: a non-2xx answer, a DNS failure, and an unreachable collector are all treated identically, which is to say ignored. Telemetry can never block start-up, a request, a job, or shutdown, and a sync.Once guard means it starts at most once per process. If the API cannot read or create the installation row — no APP_DATABASE_URL, or the database is unreachable at start — it uses a random UUID for that process lifetime and logs telemetry installation ID is ephemeral.

Switching it off

Restart the API. Nothing re-reads the environment, so the change takes effect on the next start and not before. Confirm it from the API log. On every start the process logs exactly one of:
That log line is the authoritative statement of what this installation is doing.
The panel’s telemetry preview card at /admin/settings/operations reads its on/off state from the telemetry settings section, which is empty on a fresh installation — so the card shows enabled: false while the API is sending heartbeats, because APP_TELEMETRY_ENABLED defaults to true. The card also renders a different field set from the event above. Trust the start-up log and this page, not the card. See Installation settings.
An unparseable value falls back to the default. APP_TELEMETRY_ENABLED=nope means enabled, not disabled — spell it false.

Configuration

Forks that want their own numbers set APP_TELEMETRY_ENDPOINT to an HTTPS collector they control. The endpoint is validated at start-up rather than at send time, so a typo is a refusal to boot rather than a year of silently lost events.

Running a collector

APP_TELEMETRY_COLLECTOR_ENABLED=true mounts POST /v1/telemetry/events on this installation and opens a second database pool for writes. The route does not exist on an ordinary installation, which is why nothing needs to be firewalled off on one. Every submission is validated before it is stored, and an event that fails any check is rejected whole:
  • the body is capped at 64 KiB and unknown JSON fields are refused outright
  • schema must be 1, and installation_id must be exactly 36 characters
  • name must be installation.heartbeat or feature.usage
  • version ≤ 32, service ≤ 16, os ≤ 32, architecture ≤ 32 characters
  • at most 64 features and 64 counters, each key ≤ 64 characters of [a-z0-9._]
  • occurred_at must be non-zero, no more than 7 days old, and no more than 1 hour in the future
The strictness is deliberate: a collector accepts submissions from installations it does not control, so an unbounded map or a free-form string would be an injection surface rather than a data point. feature.usage is accepted by the validator and emitted by nothing, so a collector will only ever see heartbeats from Omniflow itself. A collector deletes its own rows older than 180 days on a daily loop, independently of the worker’s retention sweep. Maintainers use aggregate installation and platform trends for compatibility and roadmap decisions. Raw events are not sold or shared for advertising.

Installation settings

Why the panel’s telemetry card and the running configuration can disagree.

Security

The wider privacy posture: redaction, secrets, and what never leaves.