Migration and Upgrade
Plan SOIT upgrades with database migrations, compatibility checks, per-version upgrade notes, and rollback evidence.
SOIT upgrades should preserve runtime data, version records, and workspace isolation. All schema changes ship as reviewed Alembic migrations — raw SQL is never applied directly in production.
Preflight
Run from soit/server before any upgrade:
uv sync
uv run alembic heads
uv run alembic history --verboseExpected before proceeding:
alembic headsreports exactly one release head.- The target release contains no unreviewed migration files.
- Release notes list the migration range and any contract or schema changes.
- Workers can be stopped or drained if required.
Backup
Before upgrading any database with meaningful data:
- Export PostgreSQL with a timestamped dump.
- Record the current
alembic_version. - Export
.envor deployment environment settings. - Snapshot object storage, vector store, and Vault data when the environment contains real data.
- Record the exact SOIT commit, Docker image tag, and operator.
Fresh installation path
Proves a new environment can initialize the schema from scratch:
cd server
uv run alembic upgrade head
uv run alembic current
uv run python scripts/bootstrap_admin.py
uv run python scripts/bootstrap_enterprise_mvp.pyalembic current should report the same revision as alembic heads, and the bootstrap plus demo seed should run cleanly afterwards.
Existing database path
Proves an existing database upgrades in place without destructive reset:
cd server
uv run alembic current # record the pre-upgrade revision
uv run alembic upgrade head
uv run alembic current # must now match heads
uv run pytest tests/integration/test_enterprise_agent_mvp.py -qAfter the upgrade, existing tenant, workspace, and user rows must remain readable and the demo path must still run. No manual data-directory cleanup or destructive reset should be needed.
Version upgrade notes
Each release supports upgrading in place from the previous minor release (its N-1 range). For every version: back up the database and object storage, run alembic upgrade head (the compose migrate service does this), then restart the API, the workers, and the web console on the new images.
v1.7.0
| Item | Value |
|---|---|
| Fresh head | 20261003100000 |
| Migrations added | 20261003100000 |
| N-1 range | 20260927160000..20261003100000: a v1.6.x database (20261002120000) upgrades in place |
| New settings | None; keep APPROVAL_SWEEPER_ENABLED=true on the API service |
20261003100000addsassignee_user_ids,assignee_roles, andexpires_attoapproval_requests, and theapproval_decisionstable. Existing requests stay unassigned and never expire.
The migration has a downgrade. Downgrading to 20261002120000 drops the assignees, the deadlines, and the whole decision history. Restoring the backup taken before the upgrade is the preferred way back.
Behaviour changes to review:
- A decision on a workflow approval resumes the run on the server. A client that resumes such runs itself may now get
409because the run is already resuming; treat it as done. - The approval sweeper also expires overdue requests; an expired request counts as a rejection.
- Owners and Admins are notified of approval requests nobody is assigned to (category
task); members can switch the category off in their notification preferences. - Approval responses carry
can_decide,can_cancel, andassigned_to_me, and approval events carry the request's requester, assignees, and deadline;approval.delegatedis a new event. - Once a ToolSpec declares
approversortimeout_seconds, earlier versions refuse it; do not roll back past v1.7.0 after using them.
v1.7.0 includes the approval fixes of v1.6.1. See Version 1.7.0 and Approvals.
v1.6.1
An approval fix on v1.6.0; upgrade every installation that gates tool calls on approval. It adds no migrations (fresh head and N-1 range as v1.6.0), so restart the API and workers on the new images. Enable the approval sweeper on the API service if you do not use the shipped compose files: APPROVAL_SWEEPER_ENABLED=true (and optionally APPROVAL_SWEEPER_INTERVAL, 30 seconds by default). Its first sweep closes the pending requests of runs that already ended.
Fixed:
- Resuming a workflow run that stopped for approval skipped the tool node's approval gate, so a call a reviewer rejected, or nobody had decided yet, ran once anyone allowed to run the workflow resumed it. Resuming now reads the decision: pending or missing answers
409; rejected, canceled, or expired ends the node asAPPROVAL_REJECTEDwithout calling the tool; only approved runs it. - Two decisions on one request could both be taken. The request is now locked while it is decided: the same decision again returns it unchanged, and a different one on a closed request answers
409(it was400). - Canceling a request now sends
approval.canceled, which releases a waiting task as a rejection does (approval_canceled), and the sweeper closes pending requests whose run has ended.
To find tool calls that ran without an approval before the fix, on PostgreSQL:
SELECT a.run_id, a.id AS approval_id, a.status AS approval_status, t.tool_ref
FROM approval_requests a
JOIN run_step_tool_calls t
ON t.run_id = a.run_id
AND t.tool_call_id = a.details_json ->> 'tool_call_id'
WHERE a.status <> 'approved'
AND t.status = 'succeeded';v1.6.0
| Item | Value |
|---|---|
| Fresh head | 20261002120000 |
| Migrations added | 20261002100000, 20261002110000, 20261002120000 |
| N-1 range | 20260927160000..20261002120000: a v1.5.x database (20261001110000) upgrades in place |
| New settings | None |
20261002100000addsupstream_idandupstream_request_idtorun_cost_entries, nullable and not unique.20261002110000addsunpriced_call_policytoworkspaces; existing workspaces getallow.20261002120000addsknowledge_document_restrictions; nothing is restricted until someone restricts it.
All three migrations have a downgrade. Downgrading to 20261001110000 drops the provider ids, every workspace's unpriced call policy, and every document restriction, so restricted documents become readable to everyone their knowledge base admits. Restoring the backup taken before the upgrade is the preferred way back.
Behaviour changes to review:
- Evidence bundles and ledger exports declare ledger contract
1.1. Cost records may carryupstream_idandupstream_request_id; a consumer that rejects unknown fields must accept them. - Copies kept in runs, responses, threads, and tasks now follow the reader's current access to knowledge, including the visibility rules that existed before: a member who cannot read someone else's private knowledge base, or a base not shared with their workspace, no longer sees its documents in the runs of those who could. Items are dropped from lists, totals recounted, and other copies replaced by a withheld marker.
- The keyword fallback of retrieval answers from the latest version of each document only, as the vector index does.
- Workflow steps record the documents their output named in a
knowledge_documentsmetric. - The Overview spend tile and the dashboard's run costs no longer add currencies together: a run priced in more than one currency has no single dashboard cost, and run detail totals each currency apart. Unpriced entries add nothing, so a run with only those shows a dash, not zero.
v1.6.0 includes the security fixes of v1.5.2. See Version 1.6.0.
v1.5.2
A security patch on v1.5.1; upgrade every v1.5.x installation. It adds no migrations (fresh head and N-1 range as v1.5.0), so restart the API and workers on the new images. Behaviour changes to review:
- Granting and revoking resource access (
POSTandDELETE /api/v1/resource-grants) takes a workspace Owner or Admin. A member with the Developer role who managed grants under Govern › Access now gets403. - A knowledge document's
file_idmust name a file under the same knowledge base's storage prefix; any other is refused with400. A client that uploaded documents with afile_idof its own must upload the file instead, or name a file already stored for the same knowledge base. - Review the audit ledger for grants made by members who were not Owners or Admins (
identity.resource_grant.changed, its actor), and revoke any that should not exist.
v1.5.1
No migrations, and no server change apart from the version: only the command line's distribution changed. The CLI is published to PyPI as soit-cli; a command line installed from the repository can be replaced with uv tool install soit-cli. See SOIT CLI.
v1.5.0
| Item | Value |
|---|---|
| Fresh head | 20261001110000 |
| Migrations added | 20261001100000, 20261001110000 |
| N-1 range | 20260927160000..20261001110000: a v1.4.0 database (20260927190000) upgrades in place |
| New settings | KNOWLEDGE_SYNC_* in server/.env.example; defaults need no change |
20261001100000addsregression_datasetsandregression_dataset_versions, creates one dataset (with one version holding its active cases) for every agent and dataset name that already has cases, and makesregression_cases.source_run_idnullable.20261001110000addsknowledge_sources,knowledge_source_items, andknowledge_sync_runs.
Both migrations have a downgrade. Downgrading to 20260927190000 drops the datasets, their versions, and every knowledge source with its items and sync runs; documents already ingested stay. A case written without a source run gets an empty source_run_id. Restoring the backup taken before the upgrade is the preferred way back.
Knowledge connector syncs run in the knowledge ingest worker and are on by default (KNOWLEDGE_SYNC_WORKER_ENABLED=true). The KNOWLEDGE_SYNC_* settings hold the poll interval, concurrency, schedule spacing, and per-run size caps. A source on a private network needs that network in EGRESS_PRIVATE_NETWORKS.
Behaviour changes to review:
- Retrying a task of a type nothing can re-execute is refused with
TASK_NOT_RERUNNABLE(thetask_typein the details) instead of a generic409 CONFLICT, and the task keeps its terminal status. /api/v1/images/*results name the run artifact asartifact_id;attachment_idstill carries the same id.- A virtual model's image call moves on to its next target when a target cannot carry an option the call asks for, instead of being refused by the first available target.
- Azure AI MAI image edits refuse
nandsizebefore the call. - Passwords are checked with bcrypt 5 directly. Stored hashes verify unchanged, and a password past 72 bytes is cut there as before.
- Every timestamp a model reads is an aware UTC datetime, and a model refuses a naive datetime. Extension packages that write naive datetimes through SOIT's models must pass aware ones.
- The agent detail page no longer shows inputs that saved nothing (trigger, output schema, governance preview, alert threshold, rate limit, retry, and on-failure settings).
- The pre-rebuild console routes (
web/app/routes_old/) are removed.
See Version 1.5.0.
Rollback
Alembic downgrade support is migration-specific and should not be assumed for real data. The supported rollback plan:
- Stop API, worker, and web processes.
- Restore PostgreSQL from the pre-upgrade dump.
- Restore object-storage, vector, and Vault snapshots if the failed migration touched related state.
- Restore the previous image tags or commit.
- Run
uv run alembic currentand the relevant smoke test.
If a migration has a verified downgrade() path, exercise it in a disposable staging environment before relying on it operationally. Treat breaking contract changes as release events that need explicit migration and recovery planning — see Versioning and Release.