# Upgrades and migrations

> How database migrations run, and the order to upgrade in.

Source: https://critlab.app/docs/self-hosting/upgrades

An upgrade is: back up, migrate the database, roll out the new images. The Helm chart does the middle step for you.

## How migrations run

Database migrations are plain SQL files shipped with `packages/db` (`packages/db/drizzle/`), applied in order by the `migrate` image — the same code the new release runs. Each applied migration is recorded in the database, so running the job twice is harmless: it applies only what is missing and exits.

Migrations only move forward. There are no down-migrations, so **a downgrade means restoring the database backup you took before upgrading**.

## Upgrade with Helm

1. Back up Postgres (see [Backups and restore](/docs/self-hosting/backups)).
2. Build and push the new images (or mirror them, for an air-gapped install).
3. Upgrade:

```bash
helm upgrade critlab charts/critlab -n critlab --reuse-values \
  --set web.image.tag=0.3.0 \
  --set sandbox.image.tag=0.3.0 \
  --set realtime.image.tag=0.3.0 \
  --set migrate.image.tag=0.3.0
```

There is no migration hook. The first new `web` pod to start runs the migration in its `migrate` init container — waits for Postgres, takes a lock, applies what's pending — before it serves any traffic, while the old pods keep serving; new `sandbox` pods wait for the schema the same way. If a migration fails, that pod stays `Init:Error`/`CrashLoopBackOff` and the rollout stalls there rather than replacing the still-serving old pods. Check it with:

```bash
kubectl logs -n critlab deploy/critlab-web -c migrate
```

Auto-generated secrets and the bundled Postgres password are read back and kept on upgrade, never regenerated.

Keep all four image tags on the same version: web and sandbox must agree on how prototype URLs are signed, and every service reads the same schema.

## Upgrade a source checkout

```bash
git pull
pnpm install
pnpm db:migrate
```

Then restart web, sandbox and realtime.

## Before you upgrade

- Read the release notes for new required environment variables. The [configuration reference](/docs/self-hosting/configuration) always reflects the current code.
- Upgrade one version at a time if you skipped several; migrations are cumulative, so skipping works, but one step at a time makes a failure easier to pin down.
- The licence key is not in the database and is unaffected by upgrades or restores.
- **`INTERNAL_LOOKUP_SECRET` for `/api/internal/*`.** web now authenticates every `/api/internal/*` route (the sandbox's storage-target lookup, and the `assets/gc` and `notifications/digest` cron endpoints) against the generated `INTERNAL_LOOKUP_SECRET`, not `AUTH_SECRET`. It still falls back to `AUTH_SECRET` when unset, so nothing breaks silently, but any external cron or script calling `POST /api/internal/assets/gc` or `POST /api/internal/notifications/digest` with `AUTH_SECRET` should switch to the `INTERNAL_LOOKUP_SECRET` value in your Secret (`secrets.existingSecret`'s `INTERNAL_LOOKUP_SECRET` key, or the chart-generated one).

> **Open questions**
>
> - **Zero-downtime upgrades.** During a Helm upgrade the old pods keep serving after the migration has run. Whether every migration is written to be compatible with the previous release's code (expand, then contract) is not stated anywhere; plan a short maintenance window until it is.
> - **Release notes and version numbers.** There is no public changelog or tagged release series yet. A curated changelog is planned for the docs.
