# Production checklist

> What to set up and verify before pyrula carries real work.

Source: https://docs.pyrula.io/getting-started/production-checklist/

Nothing here is checked for you unless the item says so.

## SQLite (one host)

- **One host, local disk.** A network filesystem or a second host is refused at open
  ([Choosing a store](https://docs.pyrula.io/getting-started/choosing-a-store.md)).
- **Low traffic only.** One writer serves every worker, and `synchronous=full` waits for
  `fsync` on each commit. Use Valkey for production load
  ([why](https://docs.pyrula.io/getting-started/choosing-a-store.md#one-writer-at-a-time)).
- **Linux, not macOS.** macOS `fsync` does not flush the drive cache, so commits can be lost
  on power loss.
- **Back up with `pyrula store backup DEST`** on a schedule, not by copying the `.db` file.
  There is no replication.
- **Disk for retention.** Finished runs are kept 7 days (`retention_seconds`);
  `pyrula store info` shows the size.

## Valkey or Redis

- **Valkey 7.2+ or Redis 6.0+, with `SCRIPT LOAD` and `EVALSHA` allowed.** The store loads one
  Lua script and calls it by its hash. A user whose ACL denies them fails at startup with the
  grants to add ([recipe](https://docs.pyrula.io/getting-started/managed-valkey.md#when-the-server-refuses-scripts)).
- **`maxmemory-policy noeviction`, on an instance dedicated to pyrula.** `allkeys-*` deletes
  live run journals under memory pressure, so `ValkeyStore` refuses it while `maxmemory` is above
  zero (`PYRULA_VALKEY_EVICTION_CHECK=warn|off` accepts the risk,
  [Installation](https://docs.pyrula.io/getting-started/install.md)). `volatile-*` only warns: run locks, leases and
  dedupe keys carry no TTL, so it can drop finished-run history and a few short-lived keys
  ([which](https://docs.pyrula.io/getting-started/managed-valkey.md#what-a-volatile-policy-can-evict)). ElastiCache,
  Memorystore, Azure Managed Redis and Redis Cloud default to `volatile-lru` and Render to
  `allkeys-lru`; [Managed Valkey and Redis](https://docs.pyrula.io/getting-started/managed-valkey.md) gives each one's
  fix. ElastiCache Serverless fixes `volatile-lru`, so alarm on `evicted_keys` there. Checked for
  you: `pyrula dev --check --store URL` and `pyrula db check --store URL` fail on `allkeys-*` and
  warn on `volatile-*` before deploy.
- **Persistence on:** `appendonly yes` or an RDB `save` schedule. Without it a restart loses
  every in-flight run. The store cannot check this; verify on the server.
- **A namespace per environment** when several share an instance (`PYRULA_VALKEY_NAMESPACE`).

## Postgres archive (optional)

Postgres holds completed runs; live state stays in Valkey. Install `"pyrula[workflows,postgres]"`
(`pyrula-agents` already depends on it), run
`pyrula db migrate --postgres-dsn "$PYRULA_POSTGRES_DSN"` once, then
`pyrula db check --store ... --postgres-dsn ...` before starting workers. A database older or
newer than the code supports fails with `ArchiveSchemaMismatch`
([Versioning & stability](https://docs.pyrula.io/getting-started/stability.md#stored-and-wire-formats),
[Workers](https://docs.pyrula.io/workflows/guides/worker.md)).

## Workers

- **Run a worker tier.** Sleeps, signal waits, interrupts, children and schedules need a
  `WorkflowWorker` or `pyrula worker`; nothing else wakes those runs
  ([Deployment modes](https://docs.pyrula.io/workflows/concepts/deployment-modes.md),
  [Deploy a worker](https://docs.pyrula.io/getting-started/deploy-a-worker.md)).
- **Agents: don't ship embedded mode.** An agent under `pyrula dev` or `create_app`'s default
  refuses `ctx.sleep`, `ctx.sleep_until`, `ctx.run_child`, `ctx.spawn_workflow`,
  `ctx.gather_subagents` and waits with a deadline
  ([details](https://docs.pyrula.io/agents/serving/cli.md#what-embedded-mode-cannot-run)).
- **Give each worker a stable id.** `WorkflowWorker(worker_id=...)` defaults to a random id per
  process; use the host or instance name so logs and leases agree across restarts. Do the same
  with `PYRULA_RUNTIME_ID` for Cloud.
- **Know the runbook commands.** `pyrula runs list --status error --since 1h`,
  `pyrula runs show RUN_ID --journal`, and `pyrula runs cancel|terminate|retry` read the store
  `PYRULA_STORE_URL` names and need no running worker ([CLI reference](https://docs.pyrula.io/reference/cli.md)).

## Authentication

Put auth on every HTTP surface a non-loopback address can reach; with no provider, anyone who
can reach the port can submit, stream, cancel and resume runs. The startup refusal for an open
non-loopback bind only sees hosts declared in `AppOptions(host=..., auth=...)`, not a bare
`uvicorn.run`. See [Serving auth](https://docs.pyrula.io/agents/serving/auth.md).

## Schedules

`pyrula schedules create` derives the id from agent, cron and params, so re-running it is a
no-op. `store.schedule_create` with `Schedule.create(...)` mints a random id unless you pass
`schedule_id=`. Pick one path per job, or two schedules fire. Cron runs in the schedule's time
zone (UTC by default) and missed occurrences are not backfilled
([Schedules](https://docs.pyrula.io/agents/serving/schedules.md), [Workers](https://docs.pyrula.io/workflows/guides/worker.md#schedules)).

## Pyrula Cloud

Set `PYRULA_CLOUD_API_KEY` in the worker's environment and install `"pyrula[cloud]"`; a key
without the client is an error, and without a key nothing is sent. A store belongs to one
project and environment. See [Connect to Pyrula Cloud](https://docs.pyrula.io/getting-started/cloud.md) and
[What leaves your runtime](https://docs.pyrula.io/getting-started/what-leaves-your-runtime.md).
