amber-backend/docs/auto-update-contract.md
Claude 785a621c49 Auto-update backend: releases collection + gated manifest (#16)
Adds the server side of auto-update (issue #16, was epic #6's [H]).

- pb_migrations/1786500000_releases.js: a superuser-only 'releases' collection
  (platform, variant, version, buildNumber, file, sha256, size, notes). Read is
  gated in the rules — any signed-in account sees clean builds; only an
  nsfwEnabled account sees adult ones — so PocketBase's native protected-file
  serving hands adult bytes only to flagged accounts (no custom streaming).

- pb_hooks/update.pb.js: GET /api/update/manifest?platform=… (auth). Picks the
  variant server-side from the caller's nsfwEnabled (adult) vs clean — the client
  can't request adult — and returns the latest build's version/buildNumber/notes/
  sha256/size + the protected downloadPath.

- scripts/publish-release.sh: uploads a built artifact as a superuser (computes
  sha256 + size, multipart POST). Token or email+password via env.

- docs/auto-update-contract.md: the collection, endpoint, download flow, gating.

Migration + hook + script syntax-checked. Live verification pends deploying this
to the PB (collection auto-applies on boot, hook loads from pb_hooks/).
2026-07-20 11:36:03 +02:00

3.5 KiB

Auto-update — contract (issue #16)

The app checks for a newer build and installs it, receiving the adult or clean variant according to its account's admin-set nsfwEnabled flag.

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.
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: desktop = .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. Because the view rule needs auth, the attached file is served as a protected file: downloading needs a short-lived file token (POST /api/files/token), and minting/using it re-checks the same rule — so adult bytes never reach a non-flagged account. 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. The variant is chosen server-side from nsfwEnabled — the client cannot request adult.

  • No release for that platform/variant → { "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.

Gating summary

nsfwEnabled (admin-set on the users record) is the single source of truth: the manifest hook reads it to pick the variant, and the collection rules enforce it independently at read/download time. A no-flag account only ever sees and downloads clean builds.