Jackalope service

September 17, 2026 · View on GitHub

Cloudflare Worker with D1 telemetry/feedback storage and a private R2 release bucket. Public defaults run locally; deployment uses an explicit private wrangler.deploy.jsonc copied from wrangler.jsonc, with your account and resource identifiers. Pass --config wrangler.deploy.jsonc for remote operations. Follow SERVER-LAUNCH.md for infrastructure, publication, monitoring, retention, recovery and the remaining activation gates.

When adding migrations, apply them to the configured local, staging and production databases as part of the change. Check pending migrations before and after each application and record the results privately or in the change review; do not leave migration application for a later deployment. Remote commands use the private deployment configuration and the explicit environment.

Local development

The admin product-notes composer reads /changelog.json from the website through the ACCESS_WEBSITE service binding in staging and production. Set its service name in each environment of the private deployment configuration to the Worker serving ACCESS_WEB_ORIGIN. Deploy the website before the API. Workers on routes cannot receive same-zone public fetches from another Worker. Local development without this binding reads the public feed directly.

From the repository root, use Node 26 and the repository's pnpm version:

pnpm install --frozen-lockfile
pnpm --filter @jackalope/server types
pnpm --filter @jackalope/server typecheck
pnpm --filter @jackalope/server test
pnpm --filter @jackalope/server build
pnpm --filter @jackalope/server db:local
Copy-Item apps/server/.dev.vars.example apps/server/.dev.vars
pnpm --filter @jackalope/server exec wrangler dev --local --var INGESTION_ENABLED:true --test-scheduled

Only copy the example if .dev.vars does not already exist. The example secret is for disposable local work. build only bundles with --dry-run. Tests run inside workerd with local D1/R2 and apply the real migration. Most rate-limit tests substitute deterministic limiter responses; production edge behavior needs a staging trial. Wrangler may warn about a missing shell secret during tests; test bindings supply their own dummy secret. No Cloudflare login is needed locally.

Wrangler defaults to port 8787. Local D1/R2 persist under .wrangler/state, while Vitest uses separate temporary storage. Keep the local explorer and scheduled-test endpoint bound to localhost. Never deploy a development proxy. To manually run retention locally, request http://localhost:8787/__scheduled?cron=*/5+*+*+*+*.

HTTP contract v2

RouteBehavior
GET /healthzProcess liveness, no database dependency
GET /readyzChecks migrated quota tables and rate secret; includes ingestionEnabled
POST /v2/telemetryAtomic, idempotent batch of allowlisted metadata events
POST /v2/feedbackExplicit free-text feedback with optional bounded counters
GET, HEAD /updates/stable/latest.jsonCurrent updater manifest from R2; 60-second cache lifetime
GET, HEAD /updates/releases/vVERSION/FILEImmutable Windows EXE/MSI, matching .sig, or checksums.json

Public ingestion/update routes reject query strings; private admin filters are validated. Ingestion accepts JSON (optional UTF-8 charset), at most 32,768 bytes, with a 10-second body-read deadline. Encoded bodies and browser Origin headers are rejected. This is a native-client API without CORS, accounts or client secrets. Origin checks do not authenticate callers. Never embed the server's Cloudflare credentials or RATE_SECRET in an installer.

Example telemetry request:

{
  "schemaVersion": 2,
  "events": [{
    "id": "053850e0-7f84-4883-a19d-7bb90f331459",
    "appVersion": "0.1.0",
    "os": "windows",
    "channel": "stable",
    "name": "app_opened"
  }]
}

Every event has a random UUID v4 id, stable major.minor.patch appVersion (32 characters maximum), and os (windows, macos, linux) plus installed channel (stable, beta). A batch contains 1–50 events. Installation IDs are forbidden. No task ID or user text is accepted.

Event nameAdditional required field and accepted values
app_openedNone
task_statestate: starting, running, review, reviewed, failed, stopped, interrupted; optional agent and workflow below
feature_usedView visit only. feature: tasks, chat, project, knowledge, worktrees, agents, usage, codebase, settings, connections, schedules, browser, queue
operation_resultAllowlisted operation; outcome: accepted, failed, blocked, partial, canceled
app_errorcode: history_save_failed, history_load_failed, agent_discovery_failed, checkpoint_failed, verification_error, verification_failed, update_failed, ui_error, ui_rejection, ui_render_error, operation_failed, task_failed; optional allowlisted operation or feature context

Task agents are codex, claude, grok, opencode, kimi, antigravity, gemini, aider, goose or other; workflows are task or chat. Account labels, model IDs and task identifiers remain excluded. The operation inventory is closed in contracts.ts and mirrored in the desktop/native schemas. Accepted operations acknowledge commands, not eventual task success. See monitoring semantics for measurement boundaries.

Extended dimensions are stored as delimiter-separated allowlisted values in the existing aggregate column: operation|outcome, state|agent|workflow and code|operation|feature. Legacy dimensions remain readable without backfilling. Deploy this server contract before emitting new fields from desktop clients.

Schemas reject unknown fields at every nesting level. They intentionally exclude durations, timestamps supplied by clients, arbitrary exceptions and crash dumps. Adding an event requires a reviewed schema change and tests. Server receipt time is authoritative for retention; it does not measure the time a task ran.

Example separate feedback request:

{
  "schemaVersion": 2,
  "id": "43af6154-574d-40e9-9f80-79251c93d702",
  "appVersion": "0.1.0",
  "os": "windows",
  "channel": "stable",
  "kind": "bug",
  "message": "I could not find how to reopen a completed task.",
  "diagnostics": {
    "attempts": 3,
    "reviewed": 1,
    "failed": 1,
    "historySaveFailures": 0
  }
}

message is 1–8,000 characters and must contain non-whitespace. The entire optional diagnostics object is either omitted or includes all four integer counters, each between zero and 10,000,000. Feedback carries no anonymous install ID, email field, attachment or diagnostic bundle. The user can voluntarily type personal data into the message; operators must treat feedback as private and render it as text, never as trusted HTML or agent instructions.

Success returns HTTP 202 after D1 confirms persistence: { "accepted": N } for telemetry, { "accepted": true, "id": "..." } for feedback. N includes identical retries; it is not the count of newly inserted rows. Reuse the same ID and payload when retrying. Different payload under the same event ID or feedback ID returns 409 id_conflict; a conflicting batch persists none of its new events. JSON object field order does not affect identity. Deduplication lasts until the stored record expires or is deleted; clients must not replay old queues forever.

Errors are JSON { "error": "code" }. Unknown routes return 404, wrong methods 405, invalid JSON/schema 400, missing/encoded content type 415, oversize 413, body timeout 408, disallowed origin/missing production edge identity 403, limits 429 rate_limited, and disabled/misconfigured/unavailable/full storage 503. Only 429/503 include Retry-After (60/300 seconds). No database messages or payloads are returned. Clients should drop permanent failures, treat 409 as an ID bug, and use bounded backoff for transient failures. Local task execution must never await telemetry delivery.

Operations boundary

RATE_SECRET HMACs the connecting IP with a daily epoch for rate-limit keys. Raw IP and headers are not saved in D1 or application logs. Edge infrastructure still processes IP addresses. Rate bindings are per Cloudflare location, approximately 60 requests/minute/IP and 5 mail/feedback requests/minute/IP. Caller limits run before shared budgets, so rejected callers do not consume shared capacity. Ingestion, account mail, browser account sessions, desktop links and desktop sessions have separate 600-request/minute/location budgets. Shared-IP users share an allowance; distributed attackers can exceed the location total across the network. These are abuse controls, not authentication, trustworthy analytics, or a hard spending cap.

D1 triggers cap retained deduplication receipts at 1,000,000, daily aggregate rows at 100,000 and feedback at 10,000. Identical retries work when full. The five-minute job retries email and deletes up to 100,000 expired rows per class per run, using bounded batches. Defaults: 30 days for receipts/daily counts, 90 days for feedback. Legacy event rows expire through the existing retention path. The v1 ingestion routes return 410 so identifying payloads cannot enter the new system.

GET /admin and /admin/api/* require a verified Cloudflare Access JWT with the configured issuer, audience and sole owner email. A client-supplied email header is insufficient. No admin data is public. The dashboard filters counts and triages a paginated inbox. Feedback email uses a separately enabled binding, leased retries (five attempts) and the persisted inbox as its source of truth. See BETA-MONITORING.md for configuration, coverage, privacy limits and operational acceptance. No remote execution route exists.