# X5 — Release & Packaging — Session Card

> Module **X5** in the (module × surface) grid of `docs/plan/13-execution-modules-and-sessions.md`.
> Read this card alongside doc 13 (the framework), `03-protocol-engines.md`, `04-backend-togo.md`, and the relevant platform PRD (05 web, 06 chrome, 07 apple, 08 android, 09 desktop, 11 mcp).

## Purpose

X5 ships Health Debug to the world: production deploy of the ToGO backend + built React SPA, and store/packaging submission of every client (Chrome Web Store, App Store Connect, Google Play Console, electron-builder Windows/Linux). It builds **no protocol code** — it packages, signs, deploys, and lists what the engine modules (M0–M8) and cross-cutting modules (X1 notifications, X2 AI proxy, X3 sensors, X4 update-gate) already produced. Because the backend is the Single Source of Truth, this module never smuggles timer/cooldown/window/trigger logic into any shipped binary; a packaged client remains a thin renderer of server state. X5 is **last per platform**: a platform's release session runs only after that platform's engines and its X-modules are done. Per doc 13 §4, the backend/web production deploy must precede every client store submission (each client points at the live production API origin), and the X4 update-gate must exist before the first store submission.

## Relevant fixed parameters and identifiers (exact — do not invent)

These are the load-bearing constants for packaging/deploy. Engine numeric constants are listed only to state the thin-client boundary: they live server-side and must never appear inside a store binary.

**Identity / app ids**
- Android `applicationId`: **`io.threex1.healthdebug`**
- Apple bundle id: **`io.threex1.HealthDebug`** (+ `.widgets` / `.mac` / `.watchkitapp`) — note the case differs from Android *on purpose*
- Electron `appId`: **`com.fadymondy.healthdebug`**

**Signing / release secrets (values in the cabrain `healthdebug` secrets vault — `secret_reveal`, never commit)**
- Android release keystore: **`.secrets/healthdebug-release.jks`**, alias `healthdebug`; password secret `legacy-android-keystore-password`. **LOSING THIS FILE MEANS A BRAND-NEW PLAY LISTING** — confirm the vault/backup copy before signing.
- Apple ASC API: secrets `apple-asc-api-key-id`, `apple-asc-issuer-id`, `apple-developer-team-id`; private key `AuthKey_Z6H456ND57.p8` (on the MacBook at `~/.appstoreconnect/private_keys`, outside the repo).
- Electron publish: **no `GH_TOKEN` stored yet** — must be provisioned before `electron-builder` can publish to GitHub releases (`fadymondy/health-debug`).

**Hosting topology (production deploy)** — Cloudflare (proxied A record → origin `45.129.183.99`) → Nginx Proxy Manager (Proxmox LXC 100, proxy host id 22) → **SSH reverse tunnel** → local host `127.0.0.1:18092`; production PostgreSQL 16 local on `:55437`. One image/process serves both `/api` and the built SPA (index.html fallback, excluding `/api`, `/graphql`, `/events`).

**Update gate (X4, consumed by every client here)** — public **unauthenticated** `GET /api/app/version?platform=&build=` → `up_to_date | update_available | update_required`; server-owned `app_releases` is the SSOT (not the stores' own update signals).

**Non-negotiable rules that gate this module**
- **SECURITY:** never ship `ALLOW_DEV_LOGIN=1` (the legacy prod shipped a passwordless admin backdoor at `/api/auth/dev/login`). The greenfield has **no auth-dev plugin at all** — verify absence, do not merely gate on `APP_ENV`.
- Every PostgreSQL column **NOT NULL with a defined default**; migrations strictly **expand-contract** (verified live before the deploy is "done").
- Auth: session cookie for web, personal access tokens for native/mcp/desktop/extension (TOGO auth).
- i18n: ICU Message Format, en/ar, full Arabic RTL (mirrored progress bars), no string concatenation — applies to store `_locales` and listing copy.
- Color spec for listing assets — Dark `#1A1A1A` bg / `#00FF33` neon-green glow / `#00CCFF` cyan / `#FF3333` red (glow); Light `#FFFFFF` bg / `#20A060` teal / `#0070A0` deep cyan (flat). *(Light-mode alert color is **PENDING DESIGN**.)*
- **Thin-client boundary (server-side only, never packaged into a client):** Hydration 250ml units / max 5000ml/day / 30s cooldown; Caffeine Block 90 min post-wakeup; GERD 4-hour pre-sleep window (water, chamomile, anise only); Medication Grace 60 min; Cycle requires 3 consecutive cycles; Kinetic Pomodoro break registers only on wearable-detected movement; Pomodoro 25/5/15, long break every 4, daily target 8 (**PENDING ratification**, not spec-fixed).

## Session table

Session IDs follow `S-<date>-X5-<surface>-NN` (leave `<date>` and `NN` as placeholders until the session actually runs). Surface order per doc 13: backend → web → mcp → chrome → apple → android → desktop; the production deploy fuses backend+web into one `infra` session that gates all client submissions. There is **no store/packaging session for `mcp`** — the MCP server + CLI rides with the backend deploy (it is not a store artifact); see Notes.

| Session ID | Surface | Scope (what this one session builds) | Entry deps (done first) | Done criteria | Plan docs to read |
|---|---|---|---|---|---|
| `S-<date>-X5-infra-NN` | backend + web (`infra`) | Production deploy of the ToGO API + built React SPA through **Cloudflare → NPM (proxy host 22) → SSH reverse tunnel → `127.0.0.1:18092`**, single process serving `/api` + SPA. Point prod at the **prod** PostgreSQL (not the dev DB), TOGO auth (cookie for web, PATs for native), expand-contract schema applied, `/healthz` green through the tunnel, public `GET /api/app/version` reachable unauthenticated. Fix the canonical **production API origin** (the value every client below points at). Run + verify backups. | M0.1–M0.7 (foundation); all **backend** engine sessions M1–M8 + X1/X2/X3 backend + **X4 update-gate backend**; all **web** engine sessions (each web engine card done). | **(Foundation template, doc 13 §5)** API+SPA serve end-to-end through the full topology; `/healthz` and `GET /api/app/version` verified live; documented manual smoke check (cookie login, one log intent, `/today` renders). **Security gate in place:** grep + live probe confirm no dev-login route (`/api/auth/dev/login` → 404) and `ALLOW_DEV_LOGIN` is unset/absent. Schema-lint CI gate confirms every column NOT NULL+default and migrations expand-contract. Backups run; the **offsite-backup-credential gap logged PENDING**. | 04, 05, 10, 13 §5/§6; ops-deployment memories `5b21ff07`, `611a0896`, `dd1328d2`, `758cfd29`; secret-inventory `17712456`, `97145768`. |
| `S-<date>-X5-chrome-NN` | chrome | Package the MV3 extension; build the **Chrome Web Store** listing with `_locales/en` + `_locales/ar` (RTL-correct message catalogs), minimal permissions (`storage`/`alarms`/`notifications` + a single `host_permissions` = the finalized prod API origin; **no `content_scripts`**), privacy disclosures, listing assets (**PENDING DESIGN**), and submit for review. | `S-<date>-X5-infra-NN` (prod origin fixed); all **chrome** engine sessions (M1–M5 chrome) + relevant X-modules; X4 update-gate. | **(Foundation template + client audit)** Package uploads and clears Web Store review prerequisites; **thin-client/SSOT audit passes** (grep finds no protocol constants in extension code); both `en`/`ar` locales present and RTL renders correctly; documented manual check of popup/badge against prod API; permission-audit gate in place (no content scripts, single host). | 06 (§10 store readiness), 13 §5/§6; secret-inventory `17712456`. |
| `S-<date>-X5-apple-NN` | apple | Archive, **sign, and submit to App Store Connect** the Apple surfaces (iOS + macOS + watchOS + WidgetKit + CarPlay), bundle id `io.threex1.HealthDebug` (+ `.widgets`/`.mac`/`.watchkitapp`). Use the **ASC API key** (`apple-asc-api-key-id` / `apple-asc-issuer-id` / `apple-developer-team-id` from the vault; `AuthKey_Z6H456ND57.p8` on the MacBook). **WORKAROUND:** the legacy Apple Development cert **lost its private key**, so `xcodebuild -allowProvisioningUpdates` cannot finish signing — **mint the signing cert via the ASC API** (the `asc.py` precedent), not local provisioning. App Store deep-link update sheet (X4) wired; watch motion validation for Kinetic Pomodoro functional. | `S-<date>-X5-infra-NN`; all **apple** engine sessions incl. **M8 apple watch motion sub-session** (mandatory — a break must be wearable-validated); X3 HealthKit; X1 APNs; X4 update-gate. | **(Foundation template)** Signed archives for each Apple surface upload to ASC via the **API-minted** cert; TestFlight/submission build accepted; documented manual check against prod API; thin-client audit re-verified; the cert-via-ASC-API workaround documented in the handoff; all secrets pulled from the vault, none committed. | 07, 10, 13 §5/§6; secret-inventory `17712456`, `97145768`. |
| `S-<date>-X5-android-NN` | android | Build a **signed release AAB** (`applicationId io.threex1.healthdebug`), signed with `.secrets/healthdebug-release.jks` (alias `healthdebug`, password from vault `legacy-android-keystore-password`), and submit to **Play Console** (internal testing track at minimum). Wire **Play In-App Updates** (`AppUpdateManager`, IMMEDIATE for mandatory / FLEXIBLE otherwise) as the *downloader* only — the X4 `app_releases` manifest stays the SSOT. WearOS motion validation for Kinetic Pomodoro functional. | `S-<date>-X5-infra-NN`; all **android** engine sessions incl. **M8 android Wear motion sub-session**; X3 Health Connect; X1 FCM; X4 update-gate. | **(Foundation template)** Signed AAB built with the release keystore and uploaded to Play Console; **keystore provenance verified** (jks + password from vault, backup/offsite copy confirmed *before* signing); documented manual check against prod API; thin-client audit re-verified; en/ar string-parity gate passes. | 08, 13 §5/§6; secret-inventory `17712456`. |
| `S-<date>-X5-desktop-NN` | desktop | **electron-builder** producing signed **Windows + Linux** artifacts, `appId com.fadymondy.healthdebug`, publishing to GitHub releases (`fadymondy/health-debug`). `electron-updater` consumes the **same X4 `app_releases` manifest**. **GAP:** no `GH_TOKEN` stored yet — provision it before publish. Code-signing identities (Windows Authenticode; Linux package signing) are **PENDING** credential/design. | `S-<date>-X5-infra-NN` (prod API + web build the desktop shell reuses); all **desktop** engine sessions; X4 update-gate. | **(Foundation template)** Win + Linux artifacts build via electron-builder and (once `GH_TOKEN` is provisioned) publish; electron-updater points at the X4 manifest; documented manual launch check against prod API; theme/i18n/thin-client parity with web re-verified; the `GH_TOKEN` provisioning + code-signing-identity gaps resolved or logged **PENDING**. | 09 (§12 packaging / M6), 13 §5/§6; secret-inventory `17712456`, `97145768`. |

*(A release-session done state maps to doc 13 §5's **Foundation-session** template — "its specific deliverable works end-to-end and is covered by a test or a documented manual check, and the CI/exit gate for its concern is in place" — since these are ops/packaging sessions, not engine sessions. Each client session additionally re-verifies the client-engine thin-client/SSOT + theme + RTL audit before shipping.)*

## Notes & gotchas

**Security (highest priority)**
- The legacy prod shipped `ALLOW_DEV_LOGIN=1` with `DEV_LOGIN_EMAIL=fady@healthdebug.local` — a passwordless admin backdoor at `/api/auth/dev/login` on the live public site. **Never reproduce this.** The greenfield removed the auth-dev plugin *outright* rather than trust an `APP_ENV` gate (precedent memory `223b2069`); confirm the route 404s in production before calling the deploy done.

**Deploy liabilities carried from legacy (avoid)**
- **Prod-ran-against-the-dev-DB trap:** legacy `run-host.ps1` sourced `.env` and served the live site against the *dev* database. Point production explicitly at the prod PostgreSQL (`:55437`) and verify.
- **Supervisor foot-guns** (memory `758cfd29`): a scheduled task starts in `C:\Windows\System32`, where togo can't find `togo.yaml` searching upward (process starts, never binds, logs nothing) — set the working dir. `& api.exe *>> $log` **kills the supervisor** because PowerShell wraps a native command's stderr in a terminating `ErrorRecord`, and the server logs "listening on :18092" to stderr; use `Start-Process` with file redirection instead. Run the tunnel task **AT LOGON, not as SYSTEM**, so it can read `~/.ssh/cabrain_tunnel` from the user profile.
- **DNS 522 trap:** the wildcard `*.fadymondy.com` A record points at a dead host; every new subdomain needs its own explicit A record at `45.129.183.99` or it returns 522 through Cloudflare.
- **Two divergent legacy DB copies** (local PG16 `:55437` vs wedged Docker `:55436`) exist in the legacy tree — irrelevant to greenfield data but a reminder to confirm which DB the greenfield prod points at.

**Reusable precedents**
- **Apple signing:** cert lost its private key → mint via the ASC API (`asc.py` precedent). Use the ASC key ids from the vault.
- **Update gate:** server-owned `app_releases` + unauthenticated `GET /api/app/version` is a proven pattern (X4). Store update signals (Play `updateAvailability`, App Store, electron-updater) are downloaders/fallbacks — they can't express "this build is protocol-incompatible," so the server manifest stays SSOT.
- **NPM origin has no TLS cert** (Cloudflare terminates TLS; NPM hosts use `certificate_id=0`, `ssl_forced=0`). NPM does not regenerate nginx confs from its DB at startup — only via its own API.

**PENDING DESIGN / PENDING credential**
- Store **listing assets** (Web Store, App Store, Play graphics/screenshots, icons) — **PENDING DESIGN**.
- **Light-mode alert color** — PENDING DESIGN (affects any alert-colored listing asset).
- **Electron `GH_TOKEN`** — not stored; provision before desktop publish. Windows/Linux **code-signing identities** and the auto-update format — PENDING.
- **Offsite backup needs a NEW credential** — backups are still same-host on `E:`; the SSH reverse-tunnel key is `command=/bin/false` restricted and **cannot** be reused for rclone/rsync/NAS. Log as PENDING, do not block the deploy on it.
- **Greenfield deploy shape (Docker prod-compose vs bare-metal `api.exe`)** — legacy ran bare-metal on `:18092`; whether the greenfield prod uses the Docker `healthdebug-prod` compose stack or bare-metal is a deploy decision to confirm in the infra session.
- **MCP distribution:** the MCP server + CLI is not a store artifact — it ships with the backend deploy (auth via PAT). If a standalone CLI package (npm/binary) is later wanted, add it as a follow-up X5 session; it is out of scope for the five sessions above.

## Handoff reminder

At the end of **every** X5 session, per doc 13 §6: write **one** `session-handoff` memory to the cabrain `healthdebug` brain — `retain` the content in the doc 13 §6 format, then `memory/edit` the returned id to attach metadata `{project:"healthdebug", category:"session-handoff", era:"greenfield", status:"current", surface:"<surface>", module:"X5", title:"Handoff <session-id>", next:"<module+surface>"}`, with a **unique** `source_ref` = `handoff/X5-release/<surface>/<n>` (never reuse). 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 new architectural decision or open question also gets its own classified memory and, if it changes the plan, an edit to the relevant plan doc.
