# Auto-update — contract (issue #16) The app checks for a newer build and installs it. One variant ships, `clean`; the server used to pick between that and an `adult` one from the account's `nsfwEnabled` flag, and no longer does. Server pieces (this repo): - `pb_migrations/1786500000_releases.js` — the `releases` collection. - `pb_hooks/update.pb.js` — the `GET /api/update/manifest` route. - `scripts/publish-release.sh` — uploads a built artifact as a superuser. Client pieces (amber-app): `lib/data/update/update_service.dart` (version check + manifest fetch), the desktop swap-installer, and the Android APK install channel. ## `releases` collection | Field | Type | Notes | |---------------|--------|-------| | `platform` | select | `windows` \| `linux` \| `android`. | | `variant` | select | `clean` \| `adult`. Only `clean` is published now. The field stays because every installed app compares it against its own and refuses a mismatch **silently** — removing it would strand those installs with no error to see. | | `version` | text | Human semver shown to the user, e.g. `1.0.1`. | | `buildNumber` | number | Monotonic int; the comparator the updater actually uses. | | `file` | file | The artifact: windows = `.exe` installer, linux = `.zip` of the release bundle, android = `.apk`. Protected (see rules). | | `sha256` | text | Lowercase hex SHA-256 of the artifact; verified before install. | | `size` | number | Bytes. | | `notes` | text | Optional release notes (shown in the update prompt). | | `created`/`updated` | autodate | | Unique index on `(platform, variant, buildNumber)`. **Access rules.** Read is gated: `@request.auth.id != '' && (variant = 'clean' || @request.auth.nsfwEnabled = true)` — any signed-in account reads **clean** rows; only an `nsfwEnabled` account reads **adult** rows. That rule is deliberately left in place: `adult` rows from before the split are still in this collection, and the rule is what keeps them out of reach. The `file` field is **`protected: true`**, which is what gates the bytes: a protected file is served only with a short-lived file token (`POST /api/files/token`) whose grant **re-checks the view rule above** (and without the flag the file URL would be public regardless of the view rule). `create`/`update`/`delete` are **superuser-only** (null rules); publishing goes through the admin API. ## Endpoint ### `GET /api/update/manifest?platform=windows|linux|android` (auth: users) Returns the latest build for the caller's platform, always `variant: "clean"`. - No release for that platform → `{ "available": false }`. - Otherwise: ```json { "available": true, "platform": "linux", "variant": "clean", "version": "1.0.1", "buildNumber": 2, "notes": "…", "sha256": "<64 hex>", "size": 12345678, "filename": "amber-linux-clean.zip", "downloadPath": "/api/files/releases//" } ``` **Download.** The app mints a file token (`POST /api/files/token`, its user auth) and GETs `{base}{downloadPath}?token=`, then verifies `sha256` before installing. ## Publishing a build Build the artifact, then (on the build machine): ```bash PB_ADMIN_TOKEN= ./scripts/publish-release.sh \ --platform linux --variant clean --version 1.0.1 --build 2 \ --file build/amber-linux-clean.zip --notes "What changed" ``` `PB_ADMIN_EMAIL` + `PB_ADMIN_PASSWORD` work instead of a token. The script computes the SHA-256 + size and uploads via the superuser REST API. Re-publishing the same `(platform, variant, buildNumber)` is rejected by the unique index — bump `buildNumber` for each release. **Windows publishes the installer**, `amber-setup--.exe`, not the zip. One artifact serves both jobs: people download and run it, and the app's updater runs the same file with `/SILENT`, which is why `DesktopInstaller` dispatches on the extension. The zip `release_windows.ps1` still builds beside it is for unpacking by hand — do not publish it. ## Gating summary There is nothing left to gate between: one variant is published, and the manifest hook returns it to everyone. `nsfwEnabled` still exists on the `users` record and the collection rules still read it. Both stay because `adult` rows from before the split are still in the collection, and those rules are what keeps them unreachable. Nothing writes the flag any more: the manifest hook ignores it, `whoami` no longer returns it, and the status dashboard's toggle is gone.