Skip to content

ADR 31: Personal kit updates and in-app update checks

Status: Proposed
Date: 2026-06-02
Last Updated: 2026-06-02

Context

Self-hosted Substratum ships as a kit of versioned artifacts, not a single binary:

ComponentRole todayTypical install location
GatewayAPI, OAuth, drives{install_root}/bin/
Edge (Caddy)Static UI + /api proxy{install_root}/edge/, serves {install_root}/web/
File explorer UIMithril SPA (built assets){install_root}/web/
Substratum desktop appTauri shell (apps/substratum-explorer)~/Applications/Substratum.app (macOS)
InstallerOrchestrator wizard (apps/installer-gui)User-installed .app / package

ADR 23 names an orchestrator and a versioned kit manifest but leaves update mechanics as “in-app updater, store updates, or replacement bundles.” The alpha implementation already refreshes pieces only through the installer:

  • Express install re-stages gateway, edge, and web/ from the installer’s bundled resources.
  • Open Substratum calls ensure_desktop_app_installed, comparing embedded stage-manifest.json (bundle_version, staged_at) with the staged explorer under resources/bundled/explorer/.
  • Substratum.app does not check for updates on launch; window.open and other shell behavior require a newer desktop bundle the user must obtain via the installer.

Operators with an already installed kit therefore depend on remembering to open the installer after each release. That does not scale for family users, security patches, or mobile parity (ADR 24). We need a long-term, consistent update model with built-in checks in the applications users actually run, while preserving the installer as the privileged orchestrator for service restarts and elevated steps.

Constraints:

  • Split trust domains: gateway runs as a background service; the desktop shell is a separate signed bundle; the installer may require elevation (packages, hosts, LaunchAgents).
  • Same-origin OAuth (operations runbook): updating web/ or edge URLs must not break public_base_url / CORS without a coordinated manifest repair step.
  • Offline-first home servers: update checks must degrade gracefully (no blocking startup on network failure).
  • Hosted SaaS (hosting_topology: cloud_connected) will use deployment pipelines, not the personal kit channel—this ADR focuses on self_hosted and desktop installer distribution.

Decision

1. Single source of truth: Kit Release Manifest

Introduce a Kit Release Manifest (KRM), versioned JSON, describing the expected versions of every installable component for a given release channel.

Local authoritative copy (written by install/upgrade):

{install_root}/substratum-kit-release.json

Optional remote catalog (fetched on check):

https://<release-host>/personal-kit/<channel>/latest.json
(or Tangled/GitHub Releases asset URL—host is a release-pipeline detail)

Minimum fields per component:

json
{
  "schema_version": 1,
  "release_id": "2026.06.02-alpha.3",
  "channel": "stable",
  "components": {
    "gateway": { "version": "1.0.0", "artifact_digest": "sha256:…" },
    "web": { "version": "1.0.0", "artifact_digest": "sha256:…" },
    "edge": { "version": "1.0.0" },
    "explorer_shell": {
      "bundle_version": "0.2.0",
      "staged_at": 1780400000
    }
  },
  "min_installer_version": "0.3.0"
}

Rules:

  • Express install and successful upgrades atomically write this file after staging artifacts.
  • Staging scripts (stage-manifest.json for explorer) remain the build-time input; KRM is the runtime contract across components.
  • release_id is opaque to users; UI shows friendly copy (“Update available: June 2026 security patch”).

2. Update Coordinator (orchestrator module)

All apply operations go through one Rust module (proposed: apps/installer-gui/src-tauri/src/self_hosted/kit_update.rs, later extractable to a shared crate if Android/desktop agents need it).

Responsibilities:

ResponsibilityOwner
Read local KRM + installed digestsUpdate Coordinator
Compare to remote catalog / bundled “shipping” manifestUpdate Coordinator
Plan ordered upgrade steps (web → gateway → edge reload → explorer shell)Update Coordinator
Execute file copies, manifest repairs (repair_gateway_oauth_manifest), service reloadExisting platform installers + manifests
Elevated OS packages (Postgres major bumps)Installer wizard only

Apply order (default) minimizes downtime and OAuth breakage:

  1. Stage web/ (static UI).
  2. Replace gateway binary; run DB migrations if release_id requires it.
  3. Rewrite edge Caddyfile if needed; reload edge (not full uninstall).
  4. Replace desktop shell if explorer_shell changed.
  5. Write new KRM; append line to {install_root}/logs/installer.log.

Gateway restart and edge reload use existing platform::*Installer hooks (ADR 24).

3. Built-in update checks (not necessarily silent apply)

Every user-facing application performs a non-blocking check on startup (and on a manual “Check for updates” action). Checks never require network for cold start; they schedule after first paint / tray idle.

ApplicationCheck behaviorApply behavior
Installer (installer-gui)Compare bundled shipping manifest vs local KRM; show Step 0 / Step 4 banner if behindFull Express install or new “Update home server” command (thin wrapper over Coordinator)
Substratum (substratum-explorer)Invoke Tauri command kit_check_updates → returns { status, components[] }Phase 1: deep-link or modal: “Open Installer to update.” Phase 2: shell-only updates via tauri-plugin-updater when signed feed exists
File explorer (browser or webview)GET /api/v1/system/kit-status (read-only) from gatewayDisplay banner; link opens installer custom URL scheme or OS default handler
Installer (self)Compare own CFBundleVersion / crate version to min_installer_version in remote catalogPrompt to download new installer bundle (separate channel)

Check outcomes (shared enum in API):

  • up_to_date
  • update_available (informational)
  • update_required (breaking schema / security; block sensitive actions only)
  • check_skipped (offline, throttled, or user disabled)
  • check_failed (logged; no user-blocking modal)

Throttle: at most one remote check per app per 24 hours unless user clicks “Check now.” Persist last_check_at in {install_root}/substratum-kit-release.json or a small sidecar file.

4. Channels and distribution

ChannelAudienceRemote catalog
stableDefault self-hostedSigned manifest on release host
betaOpt-in (SUBSTRATUM_UPDATE_CHANNEL env or profile flag)Same host, different path
devEngineersBundled manifest only; no remote fetch

Installer remains the only component that may apply full kit updates without extra consent in Phase 1. Substratum.app may apply shell-only updates in Phase 2 via Tauri updater only when artifacts are code-signed and the feed URL is pinned in tauri.conf.json.

5. Security and integrity

  • Remote KRM and artifact bundles must be served over HTTPS.
  • Phase 1: digests in KRM compared after download; shipping manifest embedded in installer at build time (tamper-evident via signed installer bundle).
  • Phase 2: minisign or platform codesign on each artifact; gateway refuses to advertise update_required unless signature verifies.
  • No auto-execute of scripts from the network; Coordinator copies known artifact types into fixed paths under install_root.
  • Log every check and apply: {install_root}/logs/installer.log (existing) + optional kit-update.log.

6. UX principles

  • Inform, don’t nag: one banner per session; snooze 7 days.
  • Explicit apply: default requires user confirmation for service restart (“Updates need ~30s downtime”).
  • Transparent components: show what will change (Gateway, App, Web UI)—not a single opaque version number.
  • Repair stays automatic: manifest repairs (OAuth loopback base URL, PDS provider) run as migration steps inside apply, not separate operator rituals.

7. Phased delivery

PhaseScopeUser-visible result
0 (today)Installer-only refresh via Express install / Open SubstratumNo in-app check
1Local KRM + kit_check_updates + installer/explorer banners + GET /api/v1/system/kit-status“Update available” with Open Installer
2Remote catalog + signed downloads + Coordinator apply_kit_update commandOne-click update from installer; explorer still defers shell to installer unless Tauri feed ready
3Background check (LaunchAgent / scheduled task optional) + update_required for CVEsAutomatic notify; optional auto-apply for shell-only

Explicitly out of scope for Phase 1–2: updating the installer app itself in place (replace bundle download only); Android/iOS store update flows (separate ADR alignment with ADR 23); mesh triangle rotation via update channel.

8. API sketch (gateway ingress)

Read-only status for the file explorer (DTOs live in crates/ingress/src/models per global AGENTS rules):

http
GET /api/v1/system/kit-status
→ { release_id, channel, components: [{ id, installed_version, status }], update_available: bool }

Optional authenticated endpoint for apply remains installer-local (Tauri IPC), not public HTTP, to avoid remote drive-by upgrades.

Consequences

  • Positive: Operators get a predictable upgrade path; engineering aligns releases to one manifest; explorer and installer share vocabulary; OAuth/CORS repairs become migrations in the Coordinator; path to signed auto-update for the desktop shell without forked logic.
  • Negative: Release CI must publish KRM + digests; more integration tests (install vN → check → apply vN+1); dual paths during Phase 1–2 (installer vs future Tauri updater) require clear docs.
  • Neutral: Alpha users keep using Open Substratum until Phase 1 ships; behavior is backward compatible if KRM is absent (treat as “unknown / check skipped”).