Docs

DocsReference

Settings and health

The settings that you can change, budget mode for the Workers Free plan, and the health endpoint.

Settings#

All settings are optional. Set them as Workers secrets or variables (in your wrangler.<name>.jsonc or in the Cloudflare dashboard).

NameDefaultWhat it does
ADMIN_TOKEN (secret)noneThe bootstrap admin token for /v1. It has all scopes. Use it to create scoped tokens. It needs at least 32 characters, for example openssl rand -hex 32; a shorter one is ignored, and the logs say why.
D1_DAILY_ROW_LIMITnoneTurns on budget mode (see below).
EVENTS_PER_ISSUE100Stored events per issue.
NEW_ISSUES_PER_HOUR100New issues per project per hour. More go into one "flood" issue.
TAG_KEYS_PER_PROJECT200Different tag keys per project.
VERIFY_QUIET_HOURS24Hours without the error, after the deploy, before a fix is verified. See Verify fixes.
GITHUB_TOKEN (secret)noneOptional. A GitHub token that can read the repository contents (a fine-grained token with "Contents: Read-only"). Checkend uses it to get the commits of each deploy, for suspect commits.
GITHUB_WEBHOOK_SECRET (secret)noneThe secret of the GitHub webhook. Without it, the webhook is off.
ENCRYPTION_KEYS (secret)noneThe keys that encrypt stored secrets, for example k2:<base64>,k1:<base64>, the current key first. Webhooks need it. See Encryption keys.
BLOBS (binding)noneAn R2 bucket for source maps and for large raw payloads. Without it, source map uploads give 400 storage_not_configured, and raw payloads above 64 KB (compressed) are not kept.
INGEST_QUEUE (binding)noneOptional overflow queue for events when D1 is overloaded. See Bursts and overload.
EMAIL (binding)noneThe Cloudflare Email Service binding ("send_email": [{"name": "EMAIL"}]) for email alerts.
EMAIL_FROMnoneThe sender of email alerts: an address on a domain that is onboarded to the Email Service. Email alerts need it and EMAIL.
NOTIFY (binding)noneOptional Cloudflare Queue for webhook deliveries. Without it, deliveries work.
RETENTION_DAYS90Days that Checkend keeps stored events and hourly counts (hourly counts at least 8 days). See Data retention.
REQUIRE_APPROVALnoneActions that need the approval of another actor (never the requester, the ADMIN_TOKEN included): a comma-separated list of ignore, merge and delete_project. See Approvals.
PUBLIC_URLnoneThe public origin of Checkend, for example https://errors.example.com. See PUBLIC_URL.
SETUP_CODE (secret)noneThe code of the one-time setup link of the web UI. npm run setup-link sets it. Do not set it by hand.
AI_TRIAGEoffon: triage issues with a Workers AI model. See AI triage.
AI_GATEWAY_IDdefaultThe AI Gateway for all model calls.
AI_GATEWAY_LOGSoffon: the gateway stores the prompts and the responses.
AI_SIMILARoffon: find similar issues. On Cloudflare, it also needs the VECTORIZE binding.
INGEST_RATE_LIMITER (binding)noneOptional: a rate limit for each DSN key (1000 requests a minute in the block below). Without it, Cloudflare has no limit for each key, but quotas and budget mode still work.

Set the secret with Wrangler:

Terminal
npx wrangler secret put ADMIN_TOKEN --config wrangler.<name>.jsonc

For local development, put ADMIN_TOKEN=... in .dev.vars. This file is git-ignored.

On Node (Docker), there are more settings, for example DATABASE_PATH. See Run without Cloudflare.

PUBLIC_URL#

When PUBLIC_URL is set, Checkend uses it, and never the Host of the request, for:

  • the passkey relying party (its ID is the host name of PUBLIC_URL)
  • the CSRF origin check of the web UI
  • each URL that the web UI shows (the setup prompt, the DSN)
  • /llms.txt, /skill.md, /v1/openapi.json, /agent/install, /agent/setup.md and /agent/tasks/*.md
  • the DSNs of /v1/projects and of POST /v1/setup/apps

When it is not set, Checkend uses the origin of the request.

The value must be an http or https origin. A / at the end is accepted. Checkend keeps only the scheme, the host and the port, in lower case. Checkend ignores a value with no scheme, another scheme, a path (https://x/checkend), a query, a fragment, or a user and password. Then it writes one console.error line that names PUBLIC_URL. Checkend cannot run under a path.

Caution: passkeys are bound to the host name. When you change PUBLIC_URL to another host name, the passkeys that exist stop working.

On Workers, the host name is fixed, so PUBLIC_URL is optional. On Node behind a proxy, set it (see Behind a reverse proxy).

Optional Cloudflare bindings#

The root wrangler.jsonc has only what a new Workers Free account can deploy: D1 and one Cron Trigger. Checkend finds the other bindings at runtime and works without them. To add one, add its block to your Worker config (wrangler.<name>.jsonc), then deploy.

A rate limit for each DSN key:

Text
"ratelimits": [
  { "name": "INGEST_RATE_LIMITER", "namespace_id": "1001", "simple": { "limit": 1000, "period": 60 } }
]

R2 for source maps and large raw payloads (see Source maps):

Text
"r2_buckets": [{ "binding": "BLOBS", "bucket_name": "checkend-blobs" }]

The overflow queue (see Bursts and overload):

Text
"queues": {
  "producers": [{ "binding": "INGEST_QUEUE", "queue": "checkend-ingest" }],
  "consumers": [{ "queue": "checkend-ingest" }]
}

Workers AI for AI triage:

Text
"ai": { "binding": "AI" }

A Vectorize index for similar issues (make it with npx wrangler vectorize create checkend-issues --dimensions=1024 --metric=cosine):

Text
"vectorize": [{ "binding": "VECTORIZE", "index_name": "checkend-issues" }]

Email alerts (see Alerts), with the variable EMAIL_FROM:

Text
"send_email": [{ "name": "EMAIL" }]

Budget mode (Workers Free plan)#

The Workers Free plan limits how many rows D1 can write each day. If Checkend writes too many rows, D1 stops all writes until the next day.

To prevent this, set:

Text
D1_DAILY_ROW_LIMIT=100000

Checkend estimates the rows that it writes: 10 rows for a stored event, 6 rows for a counted event, 2 more rows for an event with a user, and 1 row for each new tag key. This is an estimate, not the real count from D1. Each Worker instance reads the count again at most every 30 seconds.

When the estimate reaches 80% of this limit, it goes into budget mode until 00:00 UTC. In budget mode, Checkend still counts events, but it does not store them. /health and the API show the mode as counters_only.

Rows written today (estimate): of the limit fullcounters_only
80%: budget mode
000:00 UTC: back to full100,000
Budget mode. With D1_DAILY_ROW_LIMIT=100000, budget mode starts at 80,000 estimated rows and lasts until 00:00 UTC.

Data retention#

Every 5 minutes, the cron deletes data that is older than RETENTION_DAYS (90 by default):

  • stored events, with their raw payloads (also the ones in BLOBS)
  • hourly counts
  • the records of pushed commits
  • finished webhook deliveries (delivered or failed), after 30 days. Pending deliveries stay.

Checkend keeps the issues (with their total times_seen), the activity timeline (the audit record), and the user hashes behind users_affected.

Hourly counts stay for at least 8 days, also when RETENTION_DAYS is shorter, because events_7d and the priority score use them.

To keep each run small in D1 row writes, Checkend deletes in chunks: 500 rows, at most 4 chunks in each run. A large backlog, for example after you lower RETENTION_DAYS, goes away over several runs. After it deletes events, Checkend updates the stored-event counts of the issues.

Health#

GET /health needs no token. It returns the state of the install:

JSON
{
  "status": "ok",
  "version": "0.8.0",
  "migrations": { "level": 3, "latest": "0003_tokens_activity_public_ids.sql", "applied": 3 },
  "ingest": { "mode": "full", "write_failures_today": 0, "retry_after_seconds": 0 },
  "setup_open": false,
  "time": "2026-10-10T12:00:00.000Z"
}
FieldMeaning
statusok, or error when the database cannot be read.
versionThe Checkend version.
migrationsThe last migration that is applied.
ingestnull before migration 0002, or when the ingest tables cannot be read.
ingest.modefull, or counters_only in budget mode.
ingest.write_failures_todayWrites that failed today, after all tries.
ingest.retry_after_secondsMore than 0 when the database is overloaded and ingest answers 429. Each Worker instance keeps this value in memory, so another instance can show 0.
setup_opentrue until the web UI has its first member. null when the database cannot be read.
timeThe time on the server.

When the database cannot be read, /health returns 503. The response has no error details, because /health is public.

Ingest state#

GET /v1/issues also returns the ingest state in the ingest field. It has more fields than /health:

FieldMeaning
modefull or counters_only.
rows_written_todayThe estimate of rows written to D1 today (UTC). See Budget mode.
daily_row_limitThe value of D1_DAILY_ROW_LIMIT, or null.
events_received_todayEvents that reached the write today, also events that were only counted. Rejected requests (4xx) and failed writes are not counted.
write_failures_todayWrites that failed today.
retry_after_secondsAs in /health.

Edit this page on GitHubSource: guides/settings.md

Esc