Heartbeat Protocol
Canopy performs a weekly license health check (heartbeat) to verify that your license is still active — not revoked due to a refund, payment failure, or subscription cancellation. This page documents exactly what is sent, when, and how to inspect or control the behavior.
Air-gapped binaries (compiled with the air_gapped feature) never perform any heartbeat. All network code is compiled out entirely in those builds.
What Is Transmitted
Section titled “What Is Transmitted”The heartbeat sends a single JSON object via HTTP POST:
{ "license_hash": "a3f1b2c4d5e6...", "client_version": "1.3.0", "platform": "linux-x86_64", "team_id": null}| Field | Value | Notes |
|---|---|---|
license_hash | SHA-256 hex of the raw license key string | The license key itself is never sent — only its hash. This is how the server identifies the license without seeing the plaintext. |
client_version | Canopy version string (e.g., "1.3.0") | Used by the server for telemetry on version adoption. |
platform | "linux-x86_64", "macos-aarch64", "windows-x86_64", etc. | Operating system and CPU architecture. |
team_id | Team subscription ID, or null | Included for Team licenses so the admin portal can show aggregate team activity (“4 of 5 seats active this week”). null for Solo and Pro. |
What is never transmitted: source code, file paths, queries, repository names, symbol names, email addresses, or any personal data. Privacy policy: https://canopy.ironpinelabs.com/privacy#heartbeat
Endpoint
Section titled “Endpoint”POST https://canopy-license-webhook.ironpinelabs.workers.dev/heartbeatThe endpoint is a Cloudflare Worker. It logs the hash, version, and platform in a D1 database for support and analytics. It does not store your source code or queries. The endpoint can be overridden for testing via the FORGE_HEARTBEAT_URL environment variable (internal use only).
Frequency and Scheduling
Section titled “Frequency and Scheduling”The heartbeat fires from the background task spawned by canopy serve:
- Base interval: 7 days
- Jitter: up to ±12 hours (uniform random, applied at each scheduling point)
- First run: immediately on
canopy servestartup if no heartbeat has been attempted yet, or if the next scheduled attempt is in the past
The jitter prevents all instances updating simultaneously (“thundering herd”). The exact next attempt time is stored in ~/.canopy/heartbeat.json.
Cache Semantics
Section titled “Cache Semantics”The server responds with a cached_until timestamp, typically last_heartbeat_at + 14 days. Canopy uses this to avoid unnecessary network calls:
| Cache age | Canopy behavior |
|---|---|
Within cached_until | Use cached result; do not attempt a new heartbeat |
cached_until elapsed but < 30 days since last heartbeat | Attempt a new heartbeat on next canopy serve startup |
| > 30 days since last heartbeat | Warn on startup: “License check overdue — please ensure network access” |
| > 60 days since last heartbeat | Degrade to Community Mode regardless of cached license status |
The 60-day grace window ensures legitimate customers are never permanently locked out by a prolonged network outage.
What Triggers Degradation
Section titled “What Triggers Degradation”The heartbeat response includes a status field:
| Status | Canopy behavior |
|---|---|
active | Continue normally |
revoked | Degrade to Community Mode on next canopy serve startup; show revocation message |
expired | Degrade to Community Mode; show renewal prompt |
unknown | Log a warning; continue with cached license (the server may not have the hash if the key was issued before v1.3.0) |
Degradation is never immediate — it takes effect at the next canopy serve startup after the heartbeat response is received and cached.
Transparency Commands
Section titled “Transparency Commands”You can inspect and control the heartbeat at any time:
View current heartbeat status
Section titled “View current heartbeat status”canopy config heartbeat showPrints:
- Last heartbeat timestamp
- Cached result (status +
cached_until) - Next scheduled attempt
- Exact payload that would be transmitted
- Link to the privacy policy
Nothing is sent by running this command.
Force an immediate heartbeat
Section titled “Force an immediate heartbeat”canopy config heartbeat nowBypasses the weekly schedule and sends a heartbeat immediately. Use this after re-subscribing or after a network outage to restore the cache. Exits non-zero if the heartbeat fails.
Heartbeat State File
Section titled “Heartbeat State File”Heartbeat state is persisted to ~/.canopy/heartbeat.json:
{ "last_heartbeat_at": "2026-04-15T10:00:00Z", "last_status": "active", "cached_until": "2026-04-29T10:00:00Z", "last_error": null, "next_attempt_at": "2026-04-22T09:17:00Z"}| Field | Description |
|---|---|
last_heartbeat_at | UTC timestamp of the last successful heartbeat. |
last_status | Status returned by the last successful heartbeat: active, revoked, expired, or unknown. |
cached_until | The server’s suggested cache expiry. |
last_error | Error message from the most recent failed attempt, or null. |
next_attempt_at | Jittered timestamp of the next scheduled attempt. |
All fields are optional. If the file is missing or corrupt, Canopy resets to a clean default state (which triggers an immediate heartbeat attempt on the next canopy serve startup).
Licenses Activated Before v1.3.0
Section titled “Licenses Activated Before v1.3.0”If your ~/.canopy/license.json has key_hash: "<unknown>" (activated before Canopy v1.3.0), the heartbeat is skipped until you re-activate:
canopy activate <your-license-key>Re-activation writes the correct key_hash and enables heartbeat going forward. Your license remains valid in the meantime; this only affects the server-side activity tracking.
Air-Gapped Builds
Section titled “Air-Gapped Builds”Air-gapped binaries are compiled with --features air_gapped. In these builds:
- The
reqwestHTTP client is compiled out entirely (since v1.5.1,reqwestis behind thenetworkfeature flag and excluded from air-gapped builds) canopy config heartbeat showdisplays an air-gapped mode notice instead of the standard payload displaycanopy config heartbeat nowis a no-op- License validation is performed entirely against the embedded Ed25519 signature and the compile-time expiration year
No configuration is needed for air-gapped builds; heartbeat is disabled at compile time.