Skip to content

Upgrading

Carabase tells you when a newer release is available and lets you choose how aggressively you track updates — but it never auto-applies anything. Applying an update is always a deliberate operator action through the deploy script, which keeps your code, database schema, and running process in lockstep.

Release tags follow semver-prerelease conventions, and the channel for any build is derived from the version string — there’s no separate channel config:

Channel Tag pattern Audience
ga v0.1.0, v0.2.0 Public default. Use this unless you have a reason not to.
beta v0.1.0-beta.N, v0.1.0-rc.N Testers. Release candidates ride the beta channel — the signal that we’re stabilizing toward a release.
alpha v0.1.0-alpha.N Early dogfood. Restricted to alpha-enabled installs; may break, may be reverted.

Wire compatibility: the production channel is ga on the wire everywhere (responses, stored settings). The legacy name stable is still accepted on PUT /api/v1/workspace-settings as an alias and normalized to ga, so older clients keep working across a rolling upgrade — but new integrations should send and expect ga.

GET /api/v1/version reports the running build’s channel directly, alongside version, commit, built_at, and node_version.

The subscribed channel is a per-workspace setting. Change it from the Admin SPA’s Workspace page (a Channel dropdown), or via the API:

Terminal window
curl -X PUT http://localhost:3000/api/v1/workspace-settings \
-H "x-workspace-id: $WORKSPACE_ID" \
-H "content-type: application/json" \
-d '{ "updateChannel": "beta" }'

Switching channels only changes which releases the update checker looks for — it does not pull or apply anything.

GET /api/v1/updates returns the newest release in your subscribed channel, compared against the running version:

{
"current_version": "0.1.0",
"latest_version": "0.1.1",
"channel": "ga",
"update_available": true,
"release_notes_url": "https://docs.carabase.dev/operations/upgrading/",
"published_at": "2026-04-25T10:00:00Z",
"last_checked_at": "2026-04-25T11:00:00Z",
"check_enabled": true
}

The host polls the upstream release feed at most once every 24 hours, with ETag caching so a cached-good response doesn’t count against the upstream rate limit. This is a pull-only flow: the host fetches a public release URL and sends nothing about your install beyond a standard User-Agent. A transient network blip serves the last cached result rather than an error.

The Admin SPA surfaces this as a top-bar “Update available” banner (links to the release notes; dismissible for the session). To force an immediate re-check regardless of the cache, use the SPA’s “Check now” button or:

Terminal window
curl -X POST http://localhost:3000/api/v1/updates/refresh \
-H "x-workspace-id: $WORKSPACE_ID"

To opt out of update checks entirely, set the updateCheckEnabled workspace setting to false (it’s on by default). With checks off, the host makes no outbound request and the endpoint returns update_available: false.

The host never auto-applies. pnpm deploy:prod is the canonical — and only supported — path to change a running prod host. It is the one flow that keeps the checked-out code, the applied migrations, and the running process in lockstep:

Terminal window
# Follow main (default)
pnpm deploy:prod
# Or pin to a specific tagged version
pnpm deploy:prod --tag v0.1.1

Every run does: backup → fetch + checkout the target → install → migrate → build → restart → health check → smoke test, with auto-rollback on any failure after the restart. See Production deploy for the full sequence.

Migrate before restart — and the drift guard

Section titled “Migrate before restart — and the drift guard”

The launchd service runs the host straight from the working tree, so advancing the code without applying its migrations leaves newer code pointed at an older schema — it will 500 on missing columns. Two safeguards enforce the discipline:

  • deploy:prod migrates before it restarts, fail-closed. Never git pull and manually restart on the prod host — that path skips the migration step.
  • A boot-time schema-drift guard. On startup the host compares the migrations the code expects against the migrations actually applied to the database. If the schema is behind, it logs a single SCHEMA_DRIFT: N migration(s) pending and exits before serving any request — rather than crash-looping on per-query 500s. The fix is to run pnpm db:migrate:<env> and restart.

If you rotate the database password, update it in both Postgres and .env.production in the same step — the boot guard reports a credential mismatch as DB_AUTH_FAILED so a half-done rotation is obvious in the log.

Terminal window
pnpm rollback:prod --to v0.1.0

Rollback checks out the target ref and rebuilds, but does not run migrations — a downgrade across a forward-incompatible schema change can’t be undone by checking out older code. If the schema needs to move backward, restore from the pre-deploy backup instead. (deploy:prod takes a backup before every change.)

Tag pushes (v*) run a CI pipeline that gates on tests, lint, dependency audit, and an end-to-end smoke run before publishing — you can’t tag something broken. Each published release ships:

  • A published release with composed notes.
  • Channel-aliased and versioned container images, signed with cosign (keyless).
  • A CycloneDX SBOM, also cosign-signed.

Note that the prod Mac itself runs a local source build restarted via launchd (deploy:prod), not the container image — the images and SBOM are distribution and supply-chain artifacts.