# OpenSEO Self-Hosting Monitoring and Telemetry: Complete Implementation Guide

> Implement OpenSEO self-hosting monitoring and telemetry for daily health and usage metrics. This guide details privacy-preserving reporting to PostHog with automatic opt-out.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-08-14

---

**OpenSEO provides a lightweight, privacy-preserving telemetry system for self-hosted deployments that reports daily health and usage metrics to PostHog, with automatic opt-out support and no personal data collection.**

OpenSEO monitors self-hosted installations through an automated heartbeat system designed to give maintainers visibility into deployment health without compromising user privacy. This telemetry implementation runs exclusively in production environments and can be completely disabled through environment variables. The system tracks installation metrics, database backends, and feature usage while strictly avoiding any personal data collection.

## How the OpenSEO Telemetry System Works

The core telemetry engine lives in [`src/server/lib/self-host-telemetry.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/self-host-telemetry.ts). Every incoming request triggers `maybeSendSelfHostHeartbeat`, which manages the scheduling and transmission of heartbeat events.

The heartbeat follows an adaptive schedule:

- **First 2 hours after installation**: Every 5 minutes to capture onboarding behavior
- **After initial period**: Once per day for ongoing health monitoring

This approach ensures install-time issues are detected quickly while minimizing long-term network overhead.

## Telemetry Payload: What OpenSEO Reports

Each heartbeat event (`self_host.heartbeat`) transmits a structured payload to PostHog at `https://us.i.posthog.com` with key `phc_xaXj4vE4LikxfvR7q6EHemAYNBSZW4hQkqor7fpf8aGT`:

| Property | Purpose |
|----------|---------|
| `installId` | Random UUID uniquely identifying the installation |
| `installedAt` | Timestamp of first heartbeat (installation time) |
| `lastHeartbeatAt` | Timestamp of previous successful heartbeat |
| `version` / `prevVersion` | Current and previous OpenSEO versions |
| `firstRun` | Boolean flag for initial heartbeat |
| `minutesSinceInstall` | Time elapsed since installation (optional) |
| `deployTarget` | `"docker"` for local/no-auth, `"cloudflare"` otherwise |
| `dbBackend` | `"d1"` or `"postgres"` |
| `userCount`, `projectCount`, `siteAuditCount`, `rankTrackingKeywordCount`, `savedKeywordCount` | Core entity counts |
| `gscConnected` | Google Search Console connection status |
| `samChatUsed` | SAM chat tool usage flag |
| `mcpToolCalls` | MCP tool-call counter |
| `setupIssues` | Compact list of unhealthy setup checks |
| `$process_person_profile` | Always `false` — no personal profiling |

The `$process_person_profile: false` flag ensures PostHog processes this as anonymous event data only.

## Telemetry State Storage

OpenSEO persists telemetry metadata in a dedicated table defined in two schema files:

- **SQLite**: [`src/db/telemetry.schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/telemetry.schema.ts)
- **PostgreSQL**: [`src/db/pg/telemetry.schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/pg/telemetry.schema.ts)

The `telemetry_state` table stores:

```sql
-- Core fields (SQLite variant)
install_id TEXT PRIMARY KEY,
installed_at INTEGER,
last_heartbeat_at INTEGER,
version TEXT,
prev_version TEXT,
mcp_tool_calls INTEGER DEFAULT 0

```

This minimal schema ensures the heartbeat can resume correctly after restarts without leaking operational data.

## Disabling OpenSEO Telemetry

Telemetry automatically disables when any of these conditions are met (checked by `telemetryIsDisabled` 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**: `isHostedServerAuthMode` returns true
- **Explicit opt-out**: `OPENSEO_TELEMETRY_DISABLED` set to `"off"`, `"0"`, `"false"`, or `"no"`
- **Global DNT**: `DO_NOT_TRACK` set to any truthy opt-out value

Environment variable configuration:

```bash

# Disable via OpenSEO-specific variable

OPENSEO_TELEMETRY_DISABLED=off

# Or use the global Do Not Track standard

DO_NOT_TRACK=true

```

The [`src/shared/selfhost-checks.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/selfhost-checks.ts) module parses these values consistently across the codebase.

## Production-Only Operation

OpenSEO telemetry never runs in development environments. The heartbeat guard in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) checks:

```typescript
if (import.meta.env.MODE !== "production") {
  // Telemetry skipped — dev, Vitest, or preview build
}

```

This prevents accidental data pollution from local development and CI pipelines.

## MCP Tool-Call Tracking

OpenSEO increments a dedicated counter whenever self-hosted MCP tools are invoked. Call `incrementSelfHostMcpToolCallCount` from any MCP tool handler:

```typescript
import { incrementSelfHostMcpToolCallCount } from '@/server/lib/self-host-telemetry';

// Inside your MCP tool implementation
await incrementSelfHostMcpToolCallCount();

```

This counter is batched into the next heartbeat, providing aggregate usage metrics without per-call network overhead.

## Debugging and Manual Control

Force an immediate heartbeat for troubleshooting:

```typescript
import { maybeSendSelfHostHeartbeat } from '@/server/lib/self-host-telemetry';

// Bypass the 5-minute/24-hour throttle
await maybeSendSelfHostHeartbeat({ skipMemoryThrottle: true });

```

The pre-flight script at [`scripts/selfhost-preflight.ts`](https://github.com/every-app/open-seo/blob/main/scripts/selfhost-preflight.ts) validates telemetry configuration during startup and aborts with a clear message if telemetry is explicitly disabled.

## Summary

- **OpenSEO telemetry** runs via `maybeSendSelfHostHeartbeat` on every request, with intelligent throttling to once daily (or every 5 minutes during onboarding)
- **Payload includes** deployment target, database backend, entity counts, and feature flags — **never personal data**
- **Storage** uses a minimal `telemetry_state` table with SQLite and PostgreSQL variants
- **Opt-out** works through `OPENSEO_TELEMETRY_DISABLED` or `DO_NOT_TRACK` environment variables
- **Production-only** execution prevents dev environment leakage
- **MCP tracking** aggregates tool usage through `incrementSelfHostMcpToolCallCount`

## Frequently Asked Questions

### Does OpenSEO telemetry collect any personal or identifiable information?

No. The `$process_person_profile` property is hardcoded to `false`, and the payload contains only aggregate counts, installation UUIDs, and configuration flags. No user emails, names, IP addresses, or content data are transmitted.

### How do I completely disable telemetry in my self-hosted OpenSEO instance?

Set either `OPENSEO_TELEMETRY_DISABLED=off` or `DO_NOT_TRACK=true` in your environment. 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 checks these via `telemetryIsDisabled` and suppresses all heartbeat activity when detected.

### Why does OpenSEO send heartbeats every 5 minutes initially?

The accelerated cadence during the first two hours captures onboarding friction and setup issues. After `minutesSinceInstall` exceeds 120, the throttle relaxes to daily intervals. This is implemented in `maybeSendSelfHostHeartbeat` using the stored `installedAt` timestamp.