Distribution site served by PocketBase itself (pb_public/, same origin): - Landing (/): create-account CTA, TV sign-in steps, login-gated PC downloads via the existing update manifest + file tokens - Wizard (/onboarding.html): invite code -> account -> named default profile -> family template fetched and encrypted IN THE BROWSER under the new user's password (PBKDF2-210k + AES-GCM, byte-compatible with the app's AddonConfigCrypto - a wizard blob decrypts in Dart, verified) -> pushed as their addon_config ciphertext. Adult fields never included. - /get/tv: public direct download of the latest clean Android APK (Downloader-friendly; adult builds stay account-gated) Backend: - onboarding_template collection (admin-only, maintained via admin UI) - AMBER_INVITE_CODE env gates BOTH users creation (X-Amber-Invite header or ?invite=) and the template route (per-IP rate limit 10/5min); fail-closed when unset. Note: this closes the previously-open in-app registration too. - Dockerfile ships pb_public + --publicDir; compose passes the env var Contract + Coolify steps (second domain amber.petruzalekr.cz) in docs/onboarding-contract.md. Verified locally end-to-end against pocketbase 0.39.6 (real-browser wizard run + Dart decrypt interop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
107 lines
5.4 KiB
Markdown
107 lines
5.4 KiB
Markdown
# 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=<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.
|