amber-backend/docs/auto-update-contract.md
Claude 55444ee55e Stop choosing an update variant, and drop the 18+ controls
Amber ships one build. The manifest hook returned `adult` or `clean`
according to the account's nsfwEnabled flag; it now returns `clean` to
everyone.

The `variant` field itself stays in the response, and that is the
important part: every Amber already installed compares it against its
own and refuses a mismatch SILENTLY - no error, no prompt, the update
simply never appears. Removing the field, or sending anything else,
would strand every one of those installs with nothing to see.

whoami stops returning nsfwEnabled. It was there in case some future
amber-api surface wanted to scope results by it; none was built, and an
account flag nobody reads is what a token-introspection endpoint should
not be handing out. amber-api's copy goes with it - it was parsed into
the user object and never once branched on.

The dashboard loses the 18+ account tile, the variant column, the
per-flavour error table, the 18+ pill, the per-user enable/disable
button and the new-account checkbox. A control that sets a flag nothing
reads is worse than no control.

Releases are now read as newest-per-platform filtered to variant='clean',
rather than newest-per-platform-and-variant. Leftover 18+ rows are still
in the collection and would otherwise have been reported as what the
family is being served.

Those rows and the collection rule that hides them are deliberately left
alone: the rule is what keeps them unreachable, and deleting published
artifacts is not something to do as a side effect of a cleanup.

check-flavor.py is gone. It proved which of two builds an artifact was,
by a marker compiled into the Dart snapshot, because a clean-named
Windows installer once carried the 18+ payload. With one build there is
nothing to tell apart.
2026-08-30 04:09:10 +02:00

4.5 KiB

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:
    {
      "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/<recordId>/<filename>"
    }
    

Download. The app mints a file token (POST /api/files/token, its user auth) and GETs {base}{downloadPath}?token=<fileToken>, then verifies sha256 before installing.

Publishing a build

Build the artifact, then (on the build machine):

PB_ADMIN_TOKEN=<superuser 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-<variant>-<version>.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.