# Family onboarding & distribution site The distribution site (landing + account wizard) lives in `pb_public/` and is served by PocketBase itself at `/` — same origin as the API, no extra service. It exists so a family member can be fully set up **from a browser**: create an account, receive the family addon config (encrypted client-side), and log the TV in via the existing device sign-in flow — without ever installing Amber on a PC or typing addon URLs. ## Pages | Path | What | |--------------------|-------------------------------------------------------------| | `/` | Landing: create-account CTA, TV sign-in steps, PC downloads (login-gated) | | `/onboarding.html` | The wizard: invite + account + profile + template config | | `/device` | Existing device-approve page (issue #12), linked from both | | `/get/tv` | Public direct download of the latest **clean** Android APK | ## Environment | Var | Meaning | |---------------------|-----------------------------------------------------------| | `AMBER_INVITE_CODE` | The single family invite code. **Unset → registration and the template route are disabled (fail closed).** Set it in Coolify. | ## Routes (pb_hooks/onboarding.pb.js) ### users create gate Creating a `users` record requires the invite code — header `X-Amber-Invite` (what the wizard sends) or `?invite=` query fallback. Superusers bypass. **Note:** this also closes the previously-open in-app registration; the app's register screen will refuse without a code until it grows an invite field (family onboarding happens through the site, so this is acceptable). ### GET /api/amber/onboarding-template?code=CODE Returns the admin-maintained plaintext family config: ```json { "addonUrl": "…", "czechAddonUrl": "…", "tmdbKey": "…" } ``` - `403` bad code · `429` after 10 bad codes / 5 min / IP · `404` no template record · `503` env unset. - **Deliberately no adult fields** — the template is family config by definition (mirrors the #14/#20 child stripping). Adult stays per-account. - Plaintext travels once over TLS to the wizard, which encrypts it under the *new user's* password (PBKDF2-HMAC-SHA256 210k → AES-256-GCM, blob = `base64(nonce[12] ‖ ct ‖ tag[16])`, `kdf = pbkdf2-sha256-210000`) and pushes it as the account's `addon_config` row — the server still stores only ciphertext per account. Interop with the app's `AddonConfigCrypto` is verified (a wizard-produced blob decrypts in the app's Dart crypto). ### GET /get/tv Streams the newest `releases` row with `platform=android && variant=clean` as `amber-tv.apk` (attachment). Public by design: the TV-sideload path (the Downloader app needs a plain URL, a fresh TV has no account, and the APK is useless without one). Adult builds remain account-gated, unchanged. ## Template maintenance (admin) One record in the `onboarding_template` collection, edited via the PocketBase admin UI (`/_/`). Fields: `addonUrl`, `czechAddonUrl`, `tmdbKey` (+ free-form `note`). Same value shapes the app's Settings store (base URLs without `/manifest.json`). The hook serves the newest record if several exist — keep just one. No client can read the collection directly (all rules null). ## Wizard flow (onboarding.html) 1. `POST /api/collections/users/records` with `X-Amber-Invite` (email + password). 2. `auth-with-password` → token. 3. `POST /api/collections/profiles/records` — the default profile, named by the user ("Kdo se bude dívat?"). The app adopts the earliest-created profile on first login (`CloudSyncService._ensureProfile`), so this *is* their profile. 4. Template fetch → client-side encrypt → `POST /api/collections/addon_config/records` (`profile`, `blob`, `salt` (fresh 16 B), `kdf`, `updatedAt`). 5. Success: TV instructions (`/get/tv` + "Přihlásit z jiného zařízení" → `/device`), plus login-token-gated Windows/Linux download buttons (existing `/api/update/manifest` + `/api/files/token`). Template application is optional (checkbox, default on); every failure past account creation degrades gracefully — the account still works, config can be entered in-app. ## Coolify deploy 1. DNS: `amber.petruzalekr.cz` → the server. 2. The service's **Domains** field becomes a comma-separated list: `https://pb.petruzalekr.cz,https://amber.petruzalekr.cz` (save, don't "Generate Domain", then restart). The site is host-agnostic (relative API paths), so both domains work; hand the family the `amber.` one. 3. Env: add `AMBER_INVITE_CODE=` and redeploy. 4. Admin UI → `onboarding_template` → create the one record with the real values. 5. Publish releases (`scripts/publish-release.sh`) so `/get/tv` and the download buttons have something to serve. ## Local verification (2026-07-22) Native `pocketbase serve` against the repo dirs + `AMBER_INVITE_CODE`: signup refused without/with wrong code (Czech error) and accepted via header and query · template route 403/200/429(10 per 5 min)/404 paths · full wizard run in a real browser produced the user, the named profile, and an `addon_config` blob that **decrypted with the app's Dart `AddonConfigCrypto`** with every field intact (adult fields null/false) · `/get/tv` streamed a published dummy APK byte-identical with the attachment header · landing's download login handled the no-releases state.