# How cronpad works

cronpad is a single Go binary that serves the dashboard, the API, the MCP server, and also runs the scheduler. It keeps durable state in two SQLite files and a third, rebuildable SQLite file as the queue of what to run next. Nothing else needs to run. A single dispatcher goroutine pulls due work out of the queue and hands it to a pool of workers that deliver webhooks or run commands, recording every attempt.

The public website and documentation are a separate deployment at [cronpad.com](https://cronpad.com). The hosted dashboard runs at [app.cronpad.com](https://app.cronpad.com/); self-hosted installations serve it at the root of their configured public URL.

## Components

```
                ┌──────────────────────────── cronpad (one binary, one port) ─────────────────────────┐
 Browser ─────▶ │ /     dashboard                                                                      │
 API / CLI ───▶ │ /api/v1  REST API             /auth  Google, GitHub sign-in                         │
 AI agents ───▶ │ /mcp  MCP server                                                                    │
                │                                                                                     │
                │  claim loop ──claim──▶ queue: due, inflight (SQLite)  ──▶ workers ──▶ webhook/command│
                │     ▲                                                       │                       │
                │  reconciler, reaper                                  result writer, notifier ──▶ alerts
                └─────┬───────────────────────────────────────────────────────┬──────────────────────┘
                      │                                                       │
             cronpad.db (jobs, users, config)                     history.db (runs, attempts, audit)
                      └─────────── Litestream ──▶ S3 / GCS / Azure / SFTP ─────┘
```

| Component | What it does |
|---|---|
| HTTP server | Dashboard, REST API, MCP server, OAuth sign-in and health checks. Works even while the scheduler is starting. |
| Claim loop | One goroutine. Atomically claims due occurrences from the queue and hands them to workers. Only runs while it holds the dispatcher lock. |
| Workers | A pool (64 by default) that executes sinks, classifies results, schedules retries and records attempts. |
| Result writer | Commits database writes as soon as it is free, grouping whatever arrives meanwhile (up to 500 commands) into one transaction, so the hot path never waits on many small transactions. |
| Reaper | Returns occurrences whose worker stopped renewing its lease (crash, hang) to the queue. |
| Reconciler | Makes sure the queue matches the databases: repairs lost entries, rebuilds a deleted or replaced queue file, handles missed runs after downtime. |
| Notifier | Evaluates alert rules and sends email, Slack, Discord or webhook alerts from a durable outbox. |
| Janitor | Deletes old history according to the [retention policy](../platform/data-retention.md) and maintains daily rollups. |

## The life of one run

Take a job `nightly-report` scheduled `every day at 9am` in `Asia/Kolkata`.

1. **Create.** The API validates the schedule, computes the next fire time (09:00 IST = 03:30 UTC), saves the job in `cronpad.db` with `next_run_at`, and adds an entry to the queue's `due` table at 03:30 UTC.
2. **Claim.** At 03:30:00 the claim loop wakes (it sleeps exactly until the earliest due time), and one transaction moves the entry from `due` to `inflight` with a 60-second lease.
3. **Schedule the next one.** In durable mode, before delivering anything, the dispatcher computes tomorrow's 09:00, writes it to `cronpad.db` and adds it to `due`. A slow webhook can never delay tomorrow's run.
4. **Deliver.** A worker creates the run record (fast mode can start while its commit is pending; see [Delivery guarantees](delivery-guarantees.md#durable-and-fast-delivery)), sends the signed `POST`, and renews the lease while waiting.
5. **Record.** The response is classified as success, retryable or fatal. History commits before job counters, and both commit before acknowledging the occurrence. The attempt is stored; on a retryable failure, a retry entry is added to `due` in the same atomic step that removes the in-flight entry.
6. **Measure.** Start lag (03:30:00.006 − 03:30:00.000 = 6 ms), duration and end-to-end time go into metrics and the run record. If start lag exceeds the SLA threshold, the run is flagged and an alert can fire.

## Source of truth and index

- **`cronpad.db` and `history.db` are the source of truth.** Jobs, their next fire time, users and history live there. Litestream backs them up continuously.
- **The queue file is an index.** It holds only "what runs when" and "what is running now". It is never backed up: if it is lost, deleted or corrupt, cronpad rebuilds it from `cronpad.db` automatically. See [Scheduling engine](scheduling-engine.md#recovery).

Read [Storage](storage.md) for why SQLite was chosen and its limits.

## One binary, one entry point

There is no separate frontend to deploy. API-only mode is a setting (`[ui] dashboard = false`), not a different build. See [Editions](../platform/editions.md) for how the open-source and cloud editions differ.