# X4 — Update Gate · Session Card

> Per-module session list for **X4 Update Gate**. Read alongside `docs/plan/13-execution-modules-and-sessions.md` (the framework), `04-backend-togo.md` (resource/migration/REST conventions), `12-legacy-reference-map.md` §5.7 (the proven legacy precedent), and the per-surface PRDs (05 web, 07 apple, 08 android, 09 desktop). A future build session opens this card to know exactly what to build for its cell.

## Purpose

The Update Gate makes release management **server-owned, not store-owned**. A single `app_releases` resource plus a public, **unauthenticated** `GET /api/app/version?platform=&build=` endpoint lets the ToGO backend tell any client whether its installed build is `up_to_date`, `update_available`, or `update_required` — the verdict is computed server-side by comparing the client's reported build against the release manifest, never by the client. Admin CRUD plus a Releases admin page mean a new version is published without a code deploy or a `psql` session. Each client acts on the server verdict in the way its platform allows: Android drives Play In-App Updates (the download mechanism only), Apple deep-links to the App Store (no equivalent API exists), Electron uses electron-updater against the same manifest, and web shows an update banner. This is the module that gates the first store submission (doc 13 §4).

## Engine / feature parameters (exact — do not invent)

This module has **no protocol-engine constants** (it is a backend resource + version-verdict service, not one of the 8 protocol engines). Its fixed contract, carried from the proven legacy pattern (GitHub **issue #4**, brain memory `healthdebug-update-gate-contract`):

- **Resource `app_release`** fields: `platform`, `version`, `build`, `min_supported_build`, `store_url`, `release_notes_en`, `release_notes_ar`, `is_mandatory`, `released_at`.
- **Public endpoint:** `GET /api/app/version?platform=<>&build=<>` → returns one of `up_to_date | update_available | update_required`, plus `store_url`, `release_notes_en/ar`, and `is_mandatory`.
- **Verdict rule (server-computed SSOT):** reported `build` < `min_supported_build` ⇒ `update_required`; reported `build` < latest published `build` for the platform ⇒ `update_available`; otherwise `up_to_date`. `is_mandatory` on the target release also forces `update_required`.
- **Unauthenticated by necessity:** the endpoint must answer for a client whose token was invalidated by the very change that requires the update — so it is on the deny-by-default guard's allowlist as public (no `Authorization` required).
- **Android** uses **Play In-App Updates** (`AppUpdateManager`) — **IMMEDIATE** flow for mandatory, **FLEXIBLE** otherwise — for the download only. Play's own `updateAvailability` is a **fallback, not the source of truth** (it cannot express "this build is protocol-incompatible"); the server manifest verdict decides.
- **Apple** has **no equivalent API** — the update sheet deep-links to the App Store via `store_url`.
- **Desktop** uses **electron-updater** sharing the same manifest.
- **Web** shows an update banner.

## Sessions

Surface order is `backend → web → mcp → chrome → apple → android → desktop` (doc 13 §4); backend must land before any client session. **mcp** and **chrome** get no session in this module: Chrome extensions auto-update through the Web Store and the MCP server/CLI updates through its package manager, so neither needs an in-app update gate (mirrors how M6/M7 skip chrome). Session-ID `<date>` and `NN` are placeholders filled at session start.

| Session ID | Surface | Scope (what this one session builds) | Entry deps | Done criteria (doc 13 §5) | Plan docs to read |
|---|---|---|---|---|---|
| `S-<date>-X4-backend-NN` | backend | `app_releases` table (migration; **every column NOT NULL with a default**; strictly expand-contract) + sqlc queries. Server-side **version-verdict service** implementing the `up_to_date/update_available/update_required` rule. Public **unauthenticated** `GET /api/app/version?platform=&build=` on the guard allowlist, returning verdict + `store_url` + `release_notes_en/ar` + `is_mandatory`. **Admin CRUD** endpoints (authenticated, admin-scoped) so releases are published without a deploy. | M0.1–M0.4 (microkernel, TOGO auth for the admin endpoints, migration harness, error envelope) | Adapted **Backend-engine** template: migration is expand-contract and every column NOT NULL + default; the public endpoint returns **server-computed** verdicts; the verdict logic lives server-side and is covered by **unit tests that encode all three outcomes + the `min_supported_build` / `is_mandatory` edges literally**; integration test against real Postgres (unknown/lower/equal/higher build; admin-created release flips a client from `up_to_date`→`update_required`); admin endpoints reject non-admin (404-not-403 isolation); the version endpoint answers with **no token**. (`plugin/core` clause is N/A — this is a resource+service, not one of the 8 engines — but the verdict comparison is the SSOT and never leaves the server.) | 04, 13, 10 (admin auth), 12 §5.7 |
| `S-<date>-X4-web-NN` | web | End-user **update banner** that renders the server verdict (dismissible for `update_available`, blocking for `update_required`) using `release_notes_en/ar` + `store_url` from the API. Plus the **Releases admin page**: a CRUD form over the admin API so a version is published from the browser (no deploy, no psql). | M0.5–M0.7 (app shell, theme engine, i18n), `S-<date>-X4-backend-NN` | **Client-engine** template: renders `/api/app/version` state and the admin API only; **thin-client/SSOT audit passes** — grep finds no `min_supported_build`/`build` comparison or mandatory logic in client code; both dark-glow and light-flat themes per the strict color spec; Arabic RTL mirrors correctly; banner + admin copy are ICU messages, release notes rendered as server strings (never concatenated). | 05, 13, 01 (colors/i18n), 12 |
| `S-<date>-X4-apple-NN` | apple | Update **sheet** shown on the server verdict; for `update_required` it blocks and deep-links to the App Store via `store_url`, for `update_available` it offers an optional prompt. No local version math. | `S-<date>-X4-backend-NN`, Apple app shell (M1 apple) | **Client-engine** template: renders the server verdict and deep-links only; thin-client audit passes (no build comparison, no client-decided "mandatory"); both themes; Arabic RTL; ICU strings; release notes from `release_notes_en/ar`. | 07, 13, 02 (divergences), 12 |
| `S-<date>-X4-android-NN` | android | Wire **Play In-App Updates** (`AppUpdateManager`): **IMMEDIATE** flow when the server verdict is `update_required`/`is_mandatory`, **FLEXIBLE** otherwise. **Server manifest is the source of truth**; Play `updateAvailability` is only a download fallback. | `S-<date>-X4-backend-NN`, Android app shell (M1 android) | **Client-engine** template: the decision to prompt/force comes from the server verdict, not from Play or client math; thin-client audit passes (grep finds no `min_supported_build` comparison); both themes; Arabic RTL; ICU strings; release notes from the API. | 08, 13, 02, 12 |
| `S-<date>-X4-desktop-NN` | desktop | Configure **electron-updater** to consult the shared manifest / `store_url` feed and surface the server verdict (reuses the web banner components where possible); blocking behavior for `update_required`. | `S-<date>-X4-backend-NN`, Desktop Electron shell (M0.5 web + Electron scaffold) | **Client-engine** template: acts on the server verdict, no local version logic; thin-client audit passes; both themes; Arabic RTL; ICU strings. | 09, 13, 12 |

## Notes & gotchas

- **Legacy precedents worth reusing.** The whole pattern is proven in the legacy tree (GitHub **issue #4**; brain memory `healthdebug-update-gate-contract`, `9d7ee1e2`): legacy Android already shipped an update gate, and legacy `desktop/` already carried `electron-updater` in its scaffold (doc 12 §3). Mine those for the wiring; carry **no code** forward (doc 12 header rule) — reference only what matches this plan.
- **Liability: never let the client decide "mandatory."** The `is_mandatory` / `update_required` verdict is server-owned. Android must treat Play `updateAvailability` as a **fallback**, not the truth, because Play cannot express "this build is protocol-incompatible." A client that computes its own mandatory state re-introduces the duplicated-logic liability the thin-client rule exists to prevent (doc 12 §6).
- **Liability: keep the version endpoint unauthenticated.** It is deliberately on the deny-by-default allowlist. If it ever required a token it could not answer the exact clients (token invalidated by the update) it exists to serve. Do not add auth "for consistency."
- **Do not hardcode store URLs in clients.** `store_url` comes from the manifest so a store move needs no client release.
- **PENDING DESIGN.**
  - Exact banner/sheet **copy and layout** and the Releases admin page UI (colors follow the strict spec; composition is a UI-phase decision).
  - **FLEXIBLE-update UX**: snooze cadence / re-prompt interval for non-mandatory updates.
  - **Theme override behavior** for the banner/sheet (tracks the M0.6 system-vs-manual override decision, still PENDING DESIGN).
  - **Build/version comparison edge policy** to confirm at implementation: `build` is treated as a per-platform monotonic integer; the tie-break and cross-platform semantics of `version` vs `build` should be recorded as an architecture-decision memory when fixed.

## Handoff reminder

At session end, write **one `session-handoff` memory** to the `healthdebug` brain — `retain` the content in the doc 13 §6 structure, then `memory/edit` to attach metadata `{category:"session-handoff", module:"X4", surface:"<surface>", status:"...", next:"<module+surface>", ...}`, with a **unique** `source_ref: handoff/X4-update-gate/<surface>/<n>` (never reuse — a repeated source_ref supersedes the prior handoff). Then **update the single `build-ledger` memory** (`source_ref: build-ledger`, edited in place) so one recall shows the whole frontier and the recommended next session. Any architectural decision (e.g. the build-comparison policy above) also gets its own classified memory and, if it changes the plan, an edit to the relevant plan doc. See doc 13 §6 and `docs/brain/BRAIN-INDEX.md`.
