# RSA app-update-architectuur (OTA, zonder appstore) Canonieke referentie: **https://updates.rsafinance.nl/ARCHITECTURE.md** Dit document beschrijft hoe eigen apps (Android + Windows) zichzelf updaten zonder appstore, en hoe je een **nieuwe app** OTA-klaar maakt. Verwijs hiernaar bij het bouwen van een nieuwe app, dan wordt de update-laag er meteen goed in gebouwd. > Dit bestand staat op een publieke host: **geen geheimen** hierin (geen sleutels, > wachtwoorden, tokens). Toegang tot de host voor publiceren gaat via key-only SSH. --- ## 1. Overzicht - **Update-host:** `https://updates.rsafinance.nl` — nginx op de misc-server (`misc.rsafinance.nl`), TLS via Let's Encrypt. Serveert statische bestanden. - **Register:** `https://updates.rsafinance.nl/apps/index.json` — lijst van alle apps. - **Per app + platform:** een map met de artefacten, een "latest"-manifest voor de zelf-updater, en een `history.json` met alle versies (voor rollback). - **Zelf-update:** elke app checkt bij opstart (en via een knop) zijn eigen manifest en installeert een nieuwere versie. - **RSA Package Manager (app-id `rsapm`):** een deploy-app (Android + Windows) die het register leest en élke app kan installeren / bijwerken / terugzetten / verwijderen. Dit is het vangnet als een app breekt door een foute update. --- ## 2. Host-layout ``` /var/www/updates/ (webroot, = https://updates.rsafinance.nl/) ├── ARCHITECTURE.md dit bestand ├── index.html landingspagina ├── bin/ │ └── register-release.py server-helper: werkt register + history bij └── apps/ ├── index.json het register (zie §3) └── / ├── android/ │ ├── latest.json self-updater manifest (Android) │ ├── history.json alle versies, nieuwste eerst │ ├── -.apk de builds │ └── -latest.apk symlink naar de nieuwste └── windows/ ├── latest.yml electron-updater feed ├── history.json alle versies, nieuwste eerst ├── -Setup-.exe └── -Setup-.exe.blockmap ``` Regels: - **``** = korte kleine-letter-slug (bv. `codespawn`, `rsapm`). - Bestandsnamen bevatten **geen spaties** (spaties breken ssh-args en geven `%20`-URL's). - Manifests (`*.json`, `latest.yml`) worden met `Cache-Control: no-store` geserveerd. --- ## 3. Bestandsformaten ### `apps/index.json` — het register ```json { "apps": [ { "id": "codespawn", "name": "codeSpawn", "description": "Beheer je Claude Code / Codex sessies via SSH", "androidPackage": "nl.rubenharms.codespawn_mobile", "platforms": ["android", "windows"] } ] } ``` - `androidPackage` = de Android application-id; RSA Package Manager gebruikt dit om de geïnstalleerde versie op te vragen. - `platforms` = welke platforms een build hebben. ### `apps//android/latest.json` — Android self-updater manifest ```json { "versionCode": 4, "versionName": "1.0.3", "url": "https://updates.rsafinance.nl/apps/codespawn/android/codespawn-1.0.3.apk", "notes": "Wat er nieuw is." } ``` De app vergelijkt `versionCode` met zijn eigen `buildNumber` (het getal na de `+` in `pubspec.yaml`, bv. `1.0.3+4` → 4). ### `apps///history.json` — versiehistorie (rollback) ```json { "versions": [ { "versionName": "1.0.3", "versionCode": 4, "url": "https://updates.rsafinance.nl/apps/codespawn/android/codespawn-1.0.3.apk", "notes": "…", "date": "2026-07-02" } ] } ``` Nieuwste eerst. Windows-records hebben geen `versionCode`. ### `apps//windows/latest.yml` — electron-updater feed Wordt gegenereerd door electron-builder (`npm run dist`) en 1-op-1 geüpload. Niet met de hand schrijven. --- ## 4. Server-helper: `register-release.py` `/var/www/updates/bin/register-release.py` doet, gegeven één release: 1. **register-upsert** in `apps/index.json` (id, naam, beschrijving, androidPackage, platform); 2. **history-append** in `apps///history.json` (nieuwste eerst, dedupe op versie); 3. voor Android: schrijft `latest.json`. Vrije-tekstvelden (naam/beschrijving/notes) gaan **base64** mee zodat spaties/leestekens de ssh-arg-grens overleven. De publiceer-scripts van de apps roepen deze helper aan; je hoeft hem normaal niet direct te gebruiken. --- ## 5. Nieuwe Android-app (Flutter) OTA-klaar maken **a. Dependencies** (`pubspec.yaml`): ```yaml dependencies: http: ^1.2.0 package_info_plus: ^8.0.0 path_provider: ^2.1.0 open_filex: ^4.4.0 ``` **b. Permissies** (`android/app/src/main/AndroidManifest.xml`): ```xml ``` **c. Updater** — een service die het manifest checkt, de APK downloadt en aan de systeem-installer geeft. Referentie-implementatie: `codespawn_mobile/lib/services/updater.dart`. Kern: `GET .../apps//android/latest.json` → vergelijk `versionCode` met `PackageInfo.buildNumber` → download via `http` → `OpenFilex.open(apk)`. Roep de check aan bij opstart (stil) én via een knop. **d. Manifest-URL** in de updater: `https://updates.rsafinance.nl/apps//android/latest.json` **e. Publiceren** — kopieer `codespawn_mobile/tools/publish-update.sh`, pas bovenin `APP_ID`, `APP_NAME`, `APP_PKG`, `APP_DESC` aan. > **Belangrijk (Android-signing):** bouw altijd de **debug**-APK op **dezelfde machine**. > De debug-keystore moet constant blijven, anders weigert Android de update > (signature mismatch). Voor productie: een vaste release-keystore opzetten en die > consistent gebruiken. --- ## 6. Nieuwe Windows-app (Electron) OTA-klaar maken **a. Dependencies:** `electron-updater` (runtime), `electron-builder` (dev). **b. `package.json` build-config:** ```json { "build": { "appId": "nl.rubenharms.", "productName": "Mijn App", "files": ["**/*", "!dist${/*}"], "win": { "target": "nsis", "icon": "build/icon.ico" }, "nsis": { "oneClick": true, "perMachine": false, "artifactName": "${name}-Setup-${version}.${ext}" }, "publish": [{ "provider": "generic", "url": "https://updates.rsafinance.nl/apps//windows/" }] } } ``` **c. In `main.js`** (na het aanmaken van het venster): registreer electron-updater — `autoUpdater.checkForUpdates()`, download op de achtergrond, installeer bij afsluiten. Alleen als `app.isPackaged` (dev-run = no-op). Referentie: `codeSpawn/main.js` (`setupAutoUpdate` + de `update:check` / `update:install` IPC). **d. Publiceren** — kopieer `codeSpawn/tools/publish-update.sh`, pas `APP_ID`/`APP_NAME` aan. **Packaging-valkuilen (belangrijk):** - Bestanden die je als los proces uitvoert (bv. een gebundelde `.exe`) moeten in `build.asarUnpack`, en het pad resolven via `app.asar` → `app.asar.unpacked` (een `.exe` draait niet vanuit het asar-archief). - Schrijfbare data (config) hoort in `app.getPath('userData')` als `app.isPackaged` (de asar is read-only); sluit die config uit de build (`!config.json`). - Zonder code-signing waarschuwt Windows SmartScreen eenmalig bij de eerste install. --- ## 7. RSA Package Manager (de deploy-app) App-id `rsapm`. Bestaat voor Android (`rsapm/`, Flutter) en Windows (`rsapm_desktop/`, Electron). Leest `apps/index.json`, toont per app *geïnstalleerd vs. nieuwste*, en kan **installeren / bijwerken / terugzetten / verwijderen / openen**. - Android leest geïnstalleerde versies via `QUERY_ALL_PACKAGES` + een MethodChannel (`rsapm/pkg`: `installedVersion`, `launch`, `uninstall`). Rollback vereist eerst verwijderen (Android weigert in-place downgrade), daarna de oude APK installeren. - Gebruik dit als **vangnet**: breekt een app door een foute update, dan zet je hier een eerdere versie terug — zonder kabel. Zelf publiceert RSA Package Manager óók naar het register, dus de app kan zichzelf updaten. --- ## 8. Release-workflow (samengevat) Per platform, één commando-reeks vanuit de app-repo: | Platform | Bouwen | Publiceren | |---|---|---| | Android | bump `pubspec.yaml` → `flutter build apk --debug` | `wsl -e bash tools/publish-update.sh "notes"` | | Windows | bump `package.json` → `npm run dist` | `wsl -e bash tools/publish-update.sh "notes"` | De publiceer-scripts draaien vanuit **WSL** (dat de SSH-sleutel naar de host heeft), uploaden het artefact en roepen `register-release.py` aan. De nieuwe versie verschijnt vanzelf in RSA Package Manager en in de zelf-updater van de app. --- ## 9. Regels & valkuilen (kort) - **Geen geheimen** op deze host. - **Android:** debug-APK, dezelfde machine, constante keystore. - **Bestandsnamen zonder spaties.** - **Manifests niet cachen** (`no-store`; regelt nginx al). - **Manifest als laatste uploaden** (eerst het artefact), zodat een client nooit naar een versie wijst die er nog niet staat. De publiceer-scripts doen dit al. --- ## 10. Zelf publiceren via HTTP (scoped token) — aanbevolen Naast de SSH-publish (voor ruben) is er een **HTTP-publiceer-API** met een **per-project token**. Een project kan daarmee alléén zijn eigen app bijwerken — niet die van een ander, en niet de structuur van de host. Ideaal voor Linux-CI of zelf-publicerende projecten (geen SSH-sleutel nodig). **Endpoint:** `POST https://updates.rsafinance.nl/publish` (multipart/form-data) - Header: `X-Token: ` (token is gebonden aan één app-id) - Velden: `app`, `platform` (`android`|`windows`), `version`, `code` (android versionCode), `name`, `description`, `androidPackage`, `notes` - Bestanden: `file` (de `.apk` of `.exe`); windows ook `yml` (latest.yml) en `blockmap` - Antwoord: `{"ok":true,...}` of `{"error":"…"}` (403 bij fout/onbekend token) De server bewaart alleen de **SHA-256** van het token; het token zelf staat nergens op de host. Bewaar je token als **secret** (password manager / CI-secret), zet het **nooit** in de repo. **Voorbeeld (Android):** ```bash PUBLISH_TOKEN= curl -fsS -X POST https://updates.rsafinance.nl/publish \ -H "X-Token: $PUBLISH_TOKEN" \ -F app= -F platform=android -F version=1.2.3 -F code=5 \ -F name="Mijn App" -F androidPackage=nl.rubenharms.mijnapp \ -F notes="Wat er nieuw is" -F "file=@build/app/outputs/flutter-apk/app-debug.apk" ``` Kant-en-klare scripts: `tools/publish-http.sh` in de rsapm-repos (Android + Windows); het token komt uit `$PUBLISH_TOKEN`. **Nieuw project + token aanmaken via HTTP** (self-service, aanbevolen) — endpoint `POST https://updates.rsafinance.nl/project`. **Geen token nodig:** de toegang is op nginx beperkt tot het IP van de **code-server** (`code.rsafinance.nl`, `83.96.201.135`) via `allow … ; deny all;`. Zo kan de project-agent zelf een nieuwe app bootstrappen zonder SSH. - Velden: `app` (verplicht, `^[a-z0-9][a-z0-9-]{0,40}$`), `rotate` (`1` om een bestaand token te roteren), optioneel `name`, `description`, `androidPackage`, `platforms` (csv `android,windows`) — indien meegegeven wordt de app meteen in `index.json` gezet. - Antwoord: `{"ok":true,"app":…,"token":"","rotated":…}`. Het publish-token verschijnt **één keer** — bewaar het als secret (`$PUBLISH_TOKEN`). - `409` als de app al bestaat (gebruik `rotate=1`); `403` vanaf elk ander IP. ```bash curl -fsS -X POST https://updates.rsafinance.nl/project \ -F app=mijnapp -F name="Mijn App" -F platforms=android \ -F androidPackage=nl.rubenharms.mijnapp # → {"ok":true,"app":"mijnapp","token":"mijnapp.xxxxx", ...} ``` Het endpoint (`bin/create-project.php`) draait in de `updates` php-fpm-pool als **ruben**, dus het weggeschreven token-hashbestand is meteen pool-leesbaar. **Handmatige fallback** (op de host): `/var/www/updates/bin/mint-token.sh `. ⚠️ Draai dit **als ruben, niet als root** — een als root geminte `.sha256` wordt `root:600` en is dan onleesbaar voor de pool (die als ruben draait) → publish geeft `403 "ongeldig token"` terwijl het token klopt. Fix: `chown ruben:ruben .sha256`. --- ## 11. Builds op Linux (code.rsafinance.nl) — de build-server De code-server heeft de volledige build-toolchain, zodat Android + Windows **op Linux** gebouwd worden — geen lokale computer of telefoonkabel nodig: - **Android:** OpenJDK 17 + Flutter (stable) + Android SDK (platforms 34/35/36, build-tools, platform-tools). Bouwen: `flutter build apk --debug`. - **Windows:** Node + electron-builder (+ Wine). Bouwen: `npm run dist` — electron-builder maakt de NSIS-installer op Linux. - Env staat in ruben's `~/.bashrc` (`ANDROID_SDK_ROOT`, Flutter/SDK op `PATH`). - Publiceren daarna via de HTTP-API (§10) met het project-token — volledig zonder SSH. Geverifieerd: zowel een Flutter-APK als een electron NSIS-installer bouwen op deze server. Let op: apps met een **native module** (bv. codeSpawn desktop met `node-pty`) kunnen een extra stap nodig hebben om die module voor Windows te leveren (native cross-compile vanaf Linux is beperkt).