# How Self-Host Telemetry Works in OpenSEO and How to Disable It

> Learn how OpenSEO self-host telemetry sends anonymous usage data and discover simple steps to disable telemetry using environment variables for enhanced privacy.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-08-19

---

**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`](https://github.com/every-app/open-seo/blob/main/src/server/lib/self-host-telemetry.ts) module.

### Heartbeat Trigger and Initialization

On every server startup and periodic interval, [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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 `isTelemetryOptOutValue` helper defined in [`src/shared/selfhost-checks.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/selfhost-checks.ts) evaluates `OPENSEO_TELEMETRY_DISABLED` and `DO_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`](https://github.com/every-app/open-seo/blob/main/src/db/telemetry.schema.ts) (SQLite) and [`src/db/pg/telemetry.schema.ts`](https://github.com/every-app/open-seo/blob/main/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:

```bash

# .env file or shell export

OPENSEO_TELEMETRY_DISABLED=true

```

Alternatively, use the legacy `DO_NOT_TRACK` variable for backward compatibility:

```bash
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`](https://github.com/every-app/open-seo/blob/main/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.

```yaml

# 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.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/self-host-telemetry.ts) manages a **heartbeat system** that sends anonymous counts to PostHog via the `self_host.heartbeat` event.
- **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 like `users`, `projects`, and `audits`—never individual records or keyword content.
- **Disable telemetry** by setting `OPENSEO_TELEMETRY_DISABLED=true` or `DO_NOT_TRACK=1`, noting that values like `"false"` or `"off"` paradoxically keep telemetry enabled.
- The `telemetry_state` table persists installation identifiers in both SQLite ([`src/db/telemetry.schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/telemetry.schema.ts)) and Postgres ([`src/db/pg/telemetry.schema.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.