System updates
SepoDesk updates itself in place. An update is a ZIP containing the changed files plus a manifest describing what to do with them — not a full replacement of the tree, and not a redeploy.
The package
update-v1.2.4.zip
├── update-manifest.json
└── files/
└── … only the files that changed
update-manifest.json names the target version and lists each file with the
operation to perform on it. SystemUpdater reads it and applies a surgical
file swap: only listed paths are touched, and every path is validated before
anything is written.
Package rules:
- Ship changed files only. The manifest is the contract; anything not listed is not touched.
- Bump the version. Version comparison drives both the availability check and the update history.
- Record the SHA-256. Remote packages are verified against it before a single file is unpacked.
- Paths are relative and validated. Traversal outside the project root is rejected at the manifest-parsing stage, not at write time.
Applying an update
Two routes reach SystemUpdateController:
Upload. An administrator uploads a package through the system app. The package is verified, then applied.
Remote fetch. The controller fetches from a URL held in
tsp_sys_updates_api, verifies the SHA-256 against the expected digest, then
applies it.
Either way the apply step is transactional. Files are backed up before they are swapped, and a failure anywhere in the sequence rolls the whole thing back — you do not get a half-updated install.
Every attempt, successful or not, is recorded in tsp_sys_updates.
UPDATER_ALLOW_INSECUREmust never be set in production. It exists so a local instance can fetch an update over plain HTTP. With it on, an update can be served to you over an unauthenticated channel, and an update is arbitrary code. Leave it absent, and confirm it is absent as part of every release.
The update UI refreshes client-side and emits sp:system-updated on success —
there is no browser reload, so open windows keep their state.
Backups
Each apply writes a backup before touching anything. Backups live under
Storage/Backups, each with its own restore-manifest.json describing exactly
which files it holds and where they came from.
The system app exposes the full lifecycle:
| Operation | Effect |
|---|---|
| List | Enumerate available backups with their versions and timestamps |
| Restore | Put the files from a backup back where they came from |
| Delete | Remove one backup |
| Prune | Trim old backups down to a retention limit |
SysBackupManager is the UI over these endpoints.
Running an update in production
- Take a database backup. File backups are automatic; the database is not.
- Apply to staging first, on a copy of production data.
- Check the SHA-256 against the digest published with the release.
- Apply, and read the result — do not assume success from the absence of an error message.
- Verify login, one server-rendered page, and one app window.
- Rebuild the bundles if the update touched anything under
public/.
If step 5 fails, restore from the backup the updater just created, then work out what went wrong on staging rather than on the live install.
Schema changes in an update
File swaps and schema changes are separate mechanisms. A file backup will not
undo an ALTER TABLE.
Application tables change through schema.json and uploadSchema; the core
migration path is InstallerService, whose versions live in
migration/versions and whose actions are journalled under migration/journal.
Both are gated behind INSTALLER_KEY.
When an update carries a schema change, plan the rollback before you plan the release. The safe shape is additive: add the new column, deploy code that reads either shape, backfill, and only drop the old column in a later release once nothing reads it.