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.
Channels
Section titled “Channels”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.
Switching channels
Section titled “Switching channels”The subscribed channel is a per-workspace setting. Change it from the Admin SPA’s Workspace page (a Channel dropdown), or via the API:
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.
Checking for updates
Section titled “Checking for updates”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:
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.
Applying an update
Section titled “Applying an update”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:
# Follow main (default)pnpm deploy:prod
# Or pin to a specific tagged versionpnpm deploy:prod --tag v0.1.1Every 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:prodmigrates before it restarts, fail-closed. Nevergit pulland 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) pendingand exits before serving any request — rather than crash-looping on per-query 500s. The fix is to runpnpm 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.
Rolling back
Section titled “Rolling back”pnpm rollback:prod --to v0.1.0Rollback 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.)
Release artifacts
Section titled “Release artifacts”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.