# Configuration reference

cronpad reads settings from built-in defaults, then an optional TOML file (`cronpad.toml` in the working directory, or `--config <path>`), then environment variables, then command-line flags; later sources win. Any setting can be set by environment variable: prefix `CRONPAD_`, uppercase the key, and join nested sections with a double underscore. Invalid configuration stops the server at startup with a list of every problem.

```toml
[database]
path = "/data/cronpad.db"
```
is the same as
```sh
CRONPAD_DATABASE__PATH=/data/cronpad.db
```

**Durations** use Go units (`ms`, `s`, `m`, `h`) plus `d` for days: `"500ms"`, `"30s"`, `"1h30m"`, `"30d"`. **Lists** in environment variables are comma-separated: `CRONPAD_AUTH__ALLOWED_EMAIL_DOMAINS=example.com,example.org`.

## Top level

| Key | Default | Description |
|---|---|---|
| `mode` | `"selfhosted"` | `selfhosted` or `hosted`. `hosted` applies safe defaults for a public, multi-tenant server; see [Security](../platform/security.md#hosted-mode). |

## server

| Key | Default | Description |
|---|---|---|
| `addr` | `":8080"` | Address the process listens on. |
| `public_url` | `"http://localhost:8080"` | The URL users reach cronpad at, e.g. `https://cron.example.com`. Used for OAuth callbacks, links in alert emails, and to mark cookies `Secure` when it's `https`. Set it whenever cronpad is behind a proxy or on a domain. |
| `read_timeout` | `"15s"` | Maximum time to read a request. |
| `write_timeout` | `"30s"` | Maximum time to write a response. |
| `shutdown_grace` | `"30s"` | How long `SIGTERM` waits for in-flight runs. Make your orchestrator's stop timeout longer than this. |
| `trusted_proxies` | `[]` | CIDRs whose `X-Forwarded-For` is trusted. |

## ui

| Key | Default | Description |
|---|---|---|
| `dashboard` | `true` | Serve the dashboard at `/`. `false` disables the dashboard; the API, `/auth`, `/mcp` and health endpoints keep working. |
| `cors_origins` | `[]` | Exact origins (no wildcards, no paths; for example `https://www.example.com`) allowed to call `/api/` from a browser. Leave empty for normal self-hosting: the dashboard and API share an origin. Set it only when another site's browser JavaScript calls your API, such as a page embedding the schedule preview. Cookies are never sent cross-origin, so those calls reach only public endpoints or use an API key. Env: `CRONPAD_UI__CORS_ORIGINS` (comma-separated). |

## database

| Key | Default | Description |
|---|---|---|
| `path` | `"./data/cronpad.db"` | Control database: users, workspaces, jobs, keys, secrets, alert config. |
| `history_path` | `"./data/history.db"` | History database: runs, attempts, audit log, alert outbox, stats. |
| `busy_timeout` | `"5s"` | How long SQLite waits on a lock before failing. |
| `run_migrations` | `true` | Apply migrations on startup. |

Both files must be on a local or block-storage filesystem, never NFS/SMB.

## queue

| Key | Default | Description |
|---|---|---|
| `path` | `""` | The SQLite queue file. Empty means `<name>-queue.db` beside `database.path` (`./data/cronpad-queue.db` by default), so two servers with differently named databases in one directory never share a queue. It is a rebuildable index: never back it up or restore it. Must differ from both database paths. |

Releases that used Redis read a `[redis]` section; it is now ignored with a warning at startup. See [Upgrading from a Redis-based version](installation.md#upgrading-from-a-redis-based-version).

## scheduler

See [Scheduling engine](../concepts/scheduling-engine.md) for what these control.

| Key | Default | Description |
|---|---|---|
| `tick_interval` | `"100ms"` | Longest the claim loop sleeps when nothing is due sooner. |
| `results_flush_interval` | `"0s"` | `0s` = group commit: the result writer commits as soon as it is free and batches whatever arrived meanwhile. A positive value (1ms through 1s) instead waits for a fixed ticker to grow batches. |
| `results_max_batch` | `500` | Commit immediately at this command count; 1 through 10000. |
| `delivery_mode` | `"durable"` | `durable` waits for admission commits; opt-in `fast` overlaps delivery and commits. Requires receiver idempotency; allowed in hosted mode. |
| `claim_batch` | `500` | Maximum occurrences claimed at once. |
| `lease_duration` | `"60s"` | In-flight lease. Renewed while a sink runs, so it only expires when a worker crashes or hangs. |
| `reaper_interval` | `"5s"` | How often expired leases are returned to the queue. |
| `lock_ttl` | `"15s"` | Dispatcher lock lifetime; renewed every third of it. |
| `reconcile_interval` | `"10s"` | How often overdue jobs are checked for lost queue entries. |
| `overdue_grace` | `"30s"` | How far past its fire time a job must be before it counts as overdue. |
| `full_reconcile_interval` | `"6h"` | Full database ↔ queue sweep (also at startup and when queue data loss is detected). |
| `sla_threshold` | `"5s"` | Default start lag above which a run breaches its SLA. Jobs can override it. |
| `default_misfire_policy` | `"fire_once"` | `fire_once`, `fire_all` or `skip`. See [Misfires](../schedules/misfires-and-overlap.md). |
| `misfire_fire_all_cap` | `10` | Maximum missed occurrences replayed by `fire_all`. |

## workers

| Key | Default | Description |
|---|---|---|
| `count` | `64` | Concurrent sink executions. |
| `queue_size` | `1024` | Hand-off buffer between the claim loop and workers. |

## sinks.webhook

| Key | Default | Description |
|---|---|---|
| `per_host_rate_limit` | `20` | Hosted-only requests/second per destination host across workspaces; token bucket delays excess. `0` disables; self-hosted mode leaves it off. |
| `enabled` | `true` | |
| `default_timeout` | `"30s"` | Per-attempt timeout when a job doesn't set one. |
| `max_timeout` | `"120s"` | Largest timeout a job may set. |
| `max_response_bytes` | `65536` | Response bytes read and stored per attempt. |
| `allow_private_networks` | `true` (`false` in hosted mode) | Allow webhooks to loopback, private and link-local addresses. |
| `user_agent` | `"cronpad/{version}"` | |

## sinks.command

| Key | Default | Description |
|---|---|---|
| `enabled` | `true` (`false` in hosted mode) | Allow command jobs. |
| `shell` | `"/bin/sh"` | Shell used when a job sets `shell: true`. |
| `allowed_commands` | `[]` | If non-empty, only these executables may run, and `shell: true` is rejected. |
| `default_timeout` | `"60s"` | |
| `max_output_bytes` | `65536` | Captured stdout+stderr per attempt. |
| `env_passthrough` | `[]` | Server environment variables passed to commands, besides `PATH`, `HOME` and `LANG`. |

## retry.defaults

| Key | Default | Description |
|---|---|---|
| `max_attempts` | `3` | Including the first attempt. |
| `backoff` | `"exponential"` | `exponential` or `fixed`. |
| `initial_interval` | `"10s"` | |
| `max_interval` | `"10m"` | |
| `multiplier` | `2.0` | |
| `jitter` | `0.2` | ±fraction applied to each wait. |

## auto_disable

| Key | Default | Description |
|---|---|---|
| `enabled` | `true` | Disable jobs after too many consecutive failed runs. |
| `consecutive_failures` | `20` | Default threshold. |
| `notify` | `true` | Email workspace owners when a job is disabled. |

## limits

`0` means unlimited. Self-hosted defaults are unlimited; cronpad Cloud sets [its own values](../platform/limits.md).

| Key | Default | Description |
|---|---|---|
| `max_jobs_per_workspace` | `0` | |
| `min_interval` | `"1s"` | Shortest allowed time between a job's occurrences. |
| `max_runs_per_month_per_workspace` | `0` | UTC calendar-month cap; automatic retries do not count. |
| `over_quota` | `"block"` | `block` records excess occurrences as skipped; `allow` keeps delivering and counting. |
| `max_concurrent_runs_per_workspace` | `0` | Maximum concurrent admissions per workspace; excess claims wait 250ms. |
| `max_alert_emails_per_day` | `0` | Ordinary alert emails per workspace per UTC day; excess is suppressed. |
| `max_runs_per_day_per_workspace` | deprecated | Converted ×30 to monthly with a startup warning; explicit monthly wins. |
| `max_api_keys_per_workspace` | `0` | Unlimited on self-hosted; hosted default 20. |
| `max_retry_attempts` | `0` | Upper bound for a job's `max_attempts`. |
| `api_rate_limit_per_minute` | `600` | Per API key or session. |

## retention

See [Data retention](../platform/data-retention.md). `"0"` keeps data forever.

| Key | Default | Description |
|---|---|---|
| `runs` | `"30d"` | Runs and their attempts. |
| `attempt_bodies` | `"7d"` | Response bodies and command output only. |
| `store_success_bodies` | `false` | Also store bodies of successful attempts. |
| `audit_log` | `"30d"` | |
| `alert_outbox` | `"30d"` | Sent, failed and suppressed alerts. Pending alerts are never deleted. |
| `job_stats_daily` | `"400d"` | Daily rollups behind dashboard charts. |
| `janitor_interval` | `"5m"` | |
| `janitor_batch` | `1000` | Rows deleted per transaction. |

## auth

See [Authentication](authentication.md).

| Key | Default | Description |
|---|---|---|
| `session_lifetime` | `"720h"` | |
| `allowed_email_domains` | `[]` | If set, only these email domains may sign in. |
| `allow_signup` | `false` | `true` lets anyone who can sign in create an account. When `false`, only existing users sign in, except the first user of an empty server ([First sign-in](authentication.md#first-sign-in)). |
| `dev_login` | `false` | Passwordless local login for development. Never enable on a reachable server. |
| `google.client_id`, `google.client_secret` | `""` | |
| `github.client_id`, `github.client_secret` | `""` | |

## secrets

| Key | Default | Description |
|---|---|---|
| `encryption_key` | `""` | 32 random bytes, base64. Required to use [secrets](../secrets/index.md). Generate with `openssl rand -base64 32`. Keep a copy: backups are useless without it. |

## alerts.email

| Key | Default | Description |
|---|---|---|
| `enabled` | `false` | |
| `smtp_host`, `smtp_port` | `""`, `587` | STARTTLS on 587, implicit TLS on 465. |
| `username`, `password` | `""` | |
| `from` | `"cronpad <alerts@example.com>"` | |

## telemetry

See [Observability](../observability/index.md).

| Key | Default | Description |
|---|---|---|
| `service_name` | `"cronpad"` | |
| `otlp_endpoint` | `""` | OTLP collector endpoint. Empty disables OTLP export. |
| `otlp_protocol` | `"grpc"` | `grpc` or `http`. |
| `otlp_logs` | `false` | Also export logs over OTLP. |
| `prometheus_addr` | `""` | e.g. `":9090"` to serve `/metrics` on a separate port. |
| `traces_sample_ratio` | `0.1` | Fraction of runs traced. |

## logging

See [Logging](../observability/logging.md).

| Key | Default | Description |
|---|---|---|
| `level` | `"info"` | `debug`, `info`, `warn`, `error`. |
| `format` | `"json"` | `json` or `logfmt`. |
| `add_source` | `false` | Include `source=file.go:123`. |

## mcp

| Key | Default | Description |
|---|---|---|
| `enabled` | `true` | Serve the [MCP server](../ai/mcp-server.md). |
| `path` | `"/mcp"` | |

## Example

```toml
[server]
public_url = "https://cron.example.com"
trusted_proxies = ["10.0.0.0/8"]

[auth.github]
client_id = "Iv1.0123456789"
# client_secret from CRONPAD_AUTH__GITHUB__CLIENT_SECRET

[alerts.email]
enabled = true
smtp_host = "smtp.postmarkapp.com"
from = "cronpad <cron@example.com>"

[logging]
format = "logfmt"

[retention]
runs = "90d"
```

## auth.oidc

One optional instance-wide OIDC login provider; see [Authentication](authentication.md#generic-oidc) for Authentik and Keycloak setup. Environment variables use the prefix `CRONPAD_AUTH__OIDC__`.

| Key | Default | Description |
|---|---|---|
| `issuer` | `""` | Issuer URL, used for discovery; empty disables the provider. |
| `client_id` | `""` | OIDC client ID, required when issuer is configured. |
| `client_secret` | `""` | Confidential client secret; prefer an environment variable. |
| `display_name` | `"SSO"` | Label displayed on the dashboard sign-in button. |
| `scopes` | `["openid", "email", "profile"]` | Requested scopes; must include `openid`. |
| `require_email_verified` | `true` | Reject identities without verified email; disabling trusts the provider's email for linking. |

Register `<public_url>/auth/oidc:default/callback` as the provider's exact redirect URI. Discovery and token verification errors reject login. This provider counts toward the startup OAuth-provider requirement.