How Self-Host Telemetry Works in OpenSEO and How to Disable It
OpenSEO's self-host telemetry transmits anonymous usage metrics to PostHog via a periodic heartbeat system that can be disabled by setting OPENSEO_TELEMETRY_DISABLED or DO_NOT_TRACK to any truthy value except the literal strings "0", "false", "no", or "off".
OpenSEO is an open-source SEO management platform that includes a privacy-first telemetry system for self-hosted instances. The every-app/open-seo repository implements this system to collect low-cardinality aggregate counts—such as the number of projects and audits—without tracking individual users, keywords, or sensitive content.
How OpenSEO Self-Host Telemetry Works
The telemetry system follows a strict seven-step lifecycle governed by the src/server/lib/self-host-telemetry.ts module.
Heartbeat Trigger and Initialization
On every server startup and periodic interval, src/server.ts imports and invokes maybeSendSelfHostHeartbeat. This function serves as the entry point for the entire telemetry pipeline, checking whether the current instance should participate in data collection before proceeding.
Eligibility Validation
Before transmitting any data, telemetryIsDisabled() performs three critical checks defined in src/server/lib/self-host-telemetry.ts:
- Hosted Mode Detection: If
isHostedServerAuthMode()detects Cloudflare Access (AUTH_MODE=cloudflare), telemetry is automatically disabled. - Environment Opt-Out: The
isTelemetryOptOutValuehelper defined insrc/shared/selfhost-checks.tsevaluatesOPENSEO_TELEMETRY_DISABLEDandDO_NOT_TRACK. Any value disables telemetry except the explicit consent strings"0","false","no", or"off". - Build Environment: The
isNonProductionBuild()guard prevents telemetry during development, testing, or preview builds.
Database State Management
If eligibility passes, claimHeartbeat(now) queries the telemetry_state table. This table is defined in both src/db/telemetry.schema.ts (SQLite) and src/db/pg/telemetry.schema.ts (Postgres). The function updates lastHeartbeatAt only if the previous heartbeat exceeds the current cadence, returning stored installation identifiers for consistent tracking across restarts.
Metrics Collection
The collectCounts() function executes COUNT(*) queries against core tables to build an anonymous snapshot. Specifically, it aggregates totals for:
- Users
- Projects
- Audits
- Rank-tracking keywords
- Saved keywords
- Google Search Console (GSC) connections
- SAM sessions
Data Transmission to PostHog
The system POSTs a self_host.heartbeat event to PostHog containing the aggregated counts, deployment target (cloudflare or docker), database backend (d1 or postgres), current application version, optional previous version, and any unhealthy setup checks. This transmission occurs only if the eligibility checks pass and the heartbeat slot is successfully claimed.
State Persistence and MCP Tracking
After successful transmission, markHeartbeatSent records the current version in telemetry_state and resets the mcpToolCallCount counter. Separately, incrementSelfHostMcpToolCallCount() tracks MCP (Model Context Protocol) tool invocations throughout the application lifecycle, including this count in subsequent heartbeats.
Disabling OpenSEO Telemetry
You can disable telemetry through multiple mechanisms, all respecting a privacy-first default.
Environment Variable Configuration
Set OPENSEO_TELEMETRY_DISABLED to any truthy value to disable telemetry:
# .env file or shell export
OPENSEO_TELEMETRY_DISABLED=true
Alternatively, use the legacy DO_NOT_TRACK variable for backward compatibility:
DO_NOT_TRACK=1
Important nuance: The literal strings "0", "false", "no", and "off" are interpreted as explicit consent to enable telemetry. If the variable is present but not clearly "off", the system assumes the operator wants telemetry disabled.
Hosted Mode Deployment
When deploying via Cloudflare Access (detected via isHostedServerAuthMode()), telemetry is automatically disabled without requiring environment variables. This applies when AUTH_MODE is configured for Cloudflare-hosted environments.
Docker Pre-flight Validation
The scripts/selfhost-preflight.ts script runs the same telemetryDisabled() check before the container starts. If telemetry is explicitly disabled via environment variables, the pre-flight script aborts container initialization, ensuring no data is transmitted even during the boot sequence.
# docker-compose.yml example
services:
open-seo:
image: everyapp/open-seo:latest
environment:
- OPENSEO_TELEMETRY_DISABLED=true
Non-Production Builds
Running the application via vite dev, vitest, or vite build --mode preview triggers the isNonProductionBuild() guard, which suppresses all telemetry transmissions regardless of environment variables.
Summary
- OpenSEO's
src/server/lib/self-host-telemetry.tsmanages a heartbeat system that sends anonymous counts to PostHog via theself_host.heartbeatevent. - Eligibility checks in
telemetryIsDisabled()respect Cloudflare Access mode, explicit opt-out variables, and non-production builds. - Metrics are aggregated through
collectCounts()and include only totals from tables likeusers,projects, andaudits—never individual records or keyword content. - Disable telemetry by setting
OPENSEO_TELEMETRY_DISABLED=trueorDO_NOT_TRACK=1, noting that values like"false"or"off"paradoxically keep telemetry enabled. - The
telemetry_statetable persists installation identifiers in both SQLite (src/db/telemetry.schema.ts) and Postgres (src/db/pg/telemetry.schema.ts) backends.
Frequently Asked Questions
What specific data does OpenSEO collect via self-host telemetry?
OpenSEO collects only low-cardinality aggregate counts. The collectCounts() function queries totals for users, projects, audits, rank-tracking keywords, saved keywords, GSC connections, and SAM sessions. It also captures deployment metadata (Docker vs. Cloudflare, D1 vs. Postgres), application version, and MCP tool invocation counts. Individual keyword data, search queries, and personally identifiable information are never transmitted.
How do I verify that telemetry is actually disabled in my Docker container?
The scripts/selfhost-preflight.ts pre-flight script validates the opt-out status before the Node process starts. If isTelemetryOptOutValue returns true for your environment variables, the container will exit immediately with a clear message, guaranteeing no heartbeat is sent. You can also check server logs for the absence of "self_host.heartbeat" events or set OPENSEO_TELEMETRY_DISABLED=true explicitly in your Docker compose environment section.
Why does setting OPENSEO_TELEMETRY_DISABLED=false enable telemetry instead of disabling it?
The isTelemetryOptOutValue helper in src/shared/selfhost-checks.ts treats the strings "0", "false", "no", and "off" as explicit consent to keep telemetry enabled. This privacy-first design assumes that if an operator bothers to set the variable to any value other than these specific "off" indicators, they likely intend to disable tracking. Only these four literal strings permit telemetry; all other values disable it.
Does running OpenSEO in Cloudflare Access mode automatically disable telemetry?
Yes. When isHostedServerAuthMode() detects a Cloudflare Access configuration (via AUTH_MODE=cloudflare), the telemetryIsDisabled() function returns true immediately, bypassing all telemetry logic. This ensures that managed/hosted deployments never transmit metrics, respecting the boundary between the open-source self-host codebase and proprietary hosted infrastructure.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →