# Siftlode — Public Deployment Plan (v1) Status: **approved (planning)** — 2026-06-13. Goal: share the current Siftlode build with one trusted tester (both users in Hungary), on safe public infrastructure, with a clean and trustworthy Google sign-in experience. This is a planning document. No implementation is started until each phase is explicitly greenlit. --- ## 1. Confirmed decisions | Topic | Decision | |---|---| | Google verification level | **Production (unverified)** — not full verification for now | | Sign-in / scopes | **Incremental authorization via an onboarding wizard** (see §2) | | Hosting | **All on the asgard VPS** (UI + backend + DB) | | Cloudflare in front | **No** for now (invite-only, unadvertised URL → tiny attack surface) | | Repo visibility to tester | **Private repo + read access** for the friend | | Security review timing | **Hybrid**: baseline audit early (folded into Phase A), focused review before publish | ### Why not full verification (yet) The "Google hasn't verified this app" warning is removed **only** by full verification (homepage on a verified domain, privacy policy + ToS, app logo, demo video, scope justification, ~10-day review). For a 2-person test that is overkill. **Production-unverified** keeps a single one-time click-through warning per user but removes the painful Testing-mode behaviour (7-day refresh token expiry → weekly re-login). Full verification stays as an optional later phase (§6). Sources: Google "Sensitive scope verification", "Verification requirements" (support 13464321), "Manage App Audience" (15549945, the 7-day rule), "Unverified apps" (7454865). --- ## 2. Onboarding wizard — incremental scopes (core design) **Key insight:** `openid email profile` are **non-sensitive** scopes. An app that requests only these shows **no unverified warning at all**, and the grant **does not expire after 7 days even in Testing mode**. The sensitive `youtube.readonly` (and the optional `youtube` write) are what trigger the warning. Therefore: 1. **Base login** requests only `openid email profile` → user signs in with a clean, familiar Google screen, no warning, lands in the app. 2. **Onboarding wizard** (first login, also reachable later from settings) walks the user through *optional, explained, one-at-a-time* grants: - **Step — read access (`youtube.readonly`)**: explains it is needed to read your subscriptions and build the feed; pre-warns about the Google "unverified app" screen and how to proceed (Advanced → Continue). Button → incremental consent. Skippable. - **Step — write access (`youtube`)**: optional; needed to unsubscribe from within Siftlode; same pre-warning. Button → incremental consent. Skippable. 3. App **degrades gracefully**: no read scope → no feed (UI explains why); no write scope → no unsubscribe button. **Why this design:** the tester consciously grants each permission, with a rationale, and can revoke on Google's side anytime — the cleanest answer to "I don't fully trust an AI-written app." It is also Google's recommended "incremental authorization" / "narrowest scope" best practice, which makes a future verification submission cleaner. ### Code implications (Phase A) - `backend/app/auth.py`: split current `READ_SCOPES` into `BASE_SCOPES` (`openid email profile`, used at `/auth/login`) and the YouTube scopes (granted via incremental upgrade). Reuse/extend the existing `/auth/upgrade`. - Add `has_read_scope()` alongside `has_write_scope()`. - Scope-gate the feed/import endpoints; expose `/api/me/scopes` so the frontend knows when to show the wizard / which cards to render. - Frontend: wizard UI + explanatory copy + pre-warning about the Google screen. --- ## 3. Infrastructure — all on asgard ``` User browser ──HTTPS──> Caddy (asgard :443, siftlode.b1fr0st.eu) │ reverse_proxy 127.0.0.1:8080 ▼ siftlode-api (FastAPI + built SPA, Docker) │ internal docker network only ▼ siftlode-db (Postgres 16 — never published) ``` - **DNS**: `porkbun add b1fr0st.eu A siftlode 88.218.78.254` (+ AAAA). Agent-owned via `/usr/local/bin/porkbun` on the PVE host. - **TLS**: Caddy DNS-01 (porkbun plugin), same as forge/pages — auto cert for `siftlode.b1fr0st.eu`. - **Caddy**: new vhost block `siftlode.b1fr0st.eu { reverse_proxy 127.0.0.1:8080 }` inheriting the existing security-header set (HSTS, X-Frame-Options, etc.). - **OAuth redirect URI**: `https://siftlode.b1fr0st.eu/auth/callback` → `OAUTH_REDIRECT_URL`. Registered in the Google OAuth client. - **Stack**: dedicated prod compose at `/srv/siftlode/`; app bound to **127.0.0.1:8080 only** (behind Caddy, never a public port); Postgres on the internal docker network only (no host port). ### Resource limits (critical on the 3.3 GB box) - `siftlode-api`: `mem_limit: 768m`, `cpus: 1.0`, `pids_limit: 256`, `no-new-privileges`, `cap_drop: ALL`, read-only rootfs + tmpfs `/tmp`, non-root user. - `siftlode-db`: `mem_limit: 512m`, `cpus: 0.75`, tuned `shared_buffers`/`work_mem`. --- ## 4. Security — layered defence Baseline already in place on asgard (from infra notes): UFW (80/443/2222/222 only), fail2ban (sshd + caddy + forgejo, with home-IP DDNS auto-whitelist), sysctl hardening, Docker daemon hardening (no-new-privileges, userland-proxy off, icc off, log caps), auditd, key-only SSH, swap. | Threat | Mitigation | |---|---| | **Bot registration spam** | Strongest control already built: invite/allowlist-only. Set `ALLOWED_EMAILS` to exactly the two emails → no open registration; double gate (Google login + email allowlist). Do **not** open registration during the test. | | **App breach / OAuth token theft** | Tokens already encrypted at rest (Fernet). Strong prod `SECRET_KEY` / `TOKEN_ENCRYPTION_KEY`. Verify session cookie flags (Secure/HttpOnly/SameSite); audit OAuth `state`/CSRF; check SSRF in the YouTube client. | | **Server kill (resource exhaustion / OOM)** | Container hard-limits above; Caddy per-IP rate limit on `/auth/*` and API; Postgres connection cap. | | **DDoS** | Honest limit: a small VPS can't absorb a large volumetric DDoS. Realistic layers: Caddy rate-limit + fail2ban on Caddy access log (present). Cloudflare free declined for now (re-evaluate if attacked). | | **AI-written code flaws** | Dedicated security review of the codebase (§5). | - **Backups**: extend the existing `asgard-snapshot` timer with a `pg_dump` of the siftlode DB (rides the nightly tar.zst; pulled to the home array by `pull-asgard-backups`). - **Monitoring**: Prometheus blackbox probe for `siftlode.b1fr0st.eu` + container-up alert, mirroring the Forgejo probes. --- ## 5. Trust-building for the tester - Minimal default scope (the wizard) → zero YouTube access and zero warning at sign-in. - Private repo **read access** for the friend → he can inspect exactly which scopes are requested and what the app does with tokens. - Permissions granted voluntarily, step by step, with rationale; revocable on Google's side anytime (the wizard says so). - Optional: self-host — the app is fully Dockerized; the friend could run his own instance (ultimate trust option; offered, not required). --- ## 6. Deploy pipeline (Forgejo Actions, near one-click) The runner already runs **on the same asgard host** as the app: 1. Push to a **private Forgejo repo** at `forge.b1fr0st.eu` (Siftlode is currently local-git only). 2. **Actions workflow** on tag push: multi-stage Docker build → push to the built-in registry (`forge.b1fr0st.eu/peter/siftlode:`). 3. **Deploy step** in the same workflow: `cd /srv/siftlode && docker compose pull && docker compose up -d` (local, since the runner is on asgard); migrations run via the entrypoint's `alembic upgrade head`. Result: `git tag vX && git push --tags` → live in minutes. --- ## 7. Work packages (phased) **Phase 0 — baseline security audit (do early; fold fixes into Phase A)** - OAuth flow, token storage, session cookies, secret handling, dependency audit (`pip-audit` + `npm audit`), Caddy headers. Since Phase A refactors auth, findings here are fixed during that refactor (avoids re-auditing churned code). **Phase A — required for sharing** 1. Onboarding wizard + scope split (§2): `auth.py` base/incremental scopes, `has_read_scope()`, scope-gated endpoints, `/api/me/scopes`, wizard UI + copy. 2. Public **landing/about**, **Privacy Policy**, **Terms of Service** pages (reachable without login). Privacy Policy references the YouTube ToS (https://www.youtube.com/t/terms) and Google Privacy Policy, includes the Limited Use disclosure. 3. Prod config: strong secrets, `OAUTH_REDIRECT_URL`, `ALLOWED_EMAILS` = the two emails, set `YOUTUBE_API_KEY` (decouples backfill from user tokens), `FRONTEND_ORIGIN` empty in prod. 4. Google Cloud Console: OAuth consent screen (app name, logo, support email, authorized domain `b1fr0st.eu`, privacy/ToS/homepage links), declare scopes, **Publish app → Production**, verify domain in Search Console (TXT added via the porkbun CLI by the agent). 5. Infra: DNS record, Caddy vhost, `/srv/siftlode/` compose stack with limits, backup hook, monitoring probe. Forgejo repo + Actions pipeline. **Phase B — focused security review before publish** - Review the new/changed code (wizard, new pages) right before the tester gets a live token. **Phase C — optional, later** - Full Google verification (demo video + scope justification) if the warning must be removed entirely or 100+ users are expected. --- ## 8. Open / external steps (need the user or browser) - Google Cloud Console actions (OAuth client, consent screen, publish, Search Console) are behind login — the user clicks them per the agent's instructions, or the agent drives via Claude-in-Chrome. - DNS + asgard-side infra + deploy pipeline are agent-handled. ## Notes / assumptions - YouTube video playback streams from Google (embedded player), not through our server → server bandwidth ≈ SPA bundle (cached) + JSON API; the asgard 5 TB/mo cap is a non-issue for 2 users. - YouTube Data API quota is 10,000 units/day per project (app already manages a 9,000 budget) — ample for 2 users.