Skip to content
Article System updates
☀️ Light Clean and bright
📜 Sepia Warm and vintage
🌤️ Light Gray Subtle and neutral
🔵 Light Blue Calm and serene
🌙 Dark Easy on the eyes
🌑 Dark Gray Deep and modern
🔷 Dark Blue Professional and sleek
🌿 Forest Calm and natural

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_INSECURE must 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:

OperationEffect
ListEnumerate available backups with their versions and timestamps
RestorePut the files from a backup back where they came from
DeleteRemove one backup
PruneTrim old backups down to a retention limit

SysBackupManager is the UI over these endpoints.

Running an update in production

  1. Take a database backup. File backups are automatic; the database is not.
  2. Apply to staging first, on a copy of production data.
  3. Check the SHA-256 against the digest published with the release.
  4. Apply, and read the result — do not assume success from the absence of an error message.
  5. Verify login, one server-rendered page, and one app window.
  6. 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.