Incline Family Portal — spec

Turn this repo into a private web portal for Shapor & Jessica, mirroring how querystory-sales-kit is served (qs-dev pattern), but on personal infrastructure with @gmail.com access.

Why

One bookmarkable, phone-friendly, always-current place for the move/life admin: lease facts, school dates and forms, community directory, money summary — browsable and searchable, not “files on Shapor’s laptop.” Jessica gets full access without a dev environment.

Architecture (decided)

Hard-won constraints (from qs-dev research, 2026-07)

  1. OAuth brand must be External — API-created IAP brands are always Internal, which blocks @gmail.com at the consent screen. Fix: host in a personal no-org GCP project (consent screen External by default) — do NOT put this in a querystory org project (keeps work/personal separate anyway).
  2. IAP OAuth Admin API was shut down March 2026 — don’t use google_iap_brand/google_iap_client Terraform resources. Use the Google-managed OAuth client: enable IAP on the backend service without explicit client id/secret.
  3. No shared cache in front of IAP — qs-dev had a real incident (Cloudflare edge served an authenticated PDF to unauthenticated users for ~4h). DNS-only if Cloudflare is involved at all.
  4. Cloud Run ingress must be INTERNAL_AND_GCLB (or internal-LB-only) — otherwise the .run.app URL bypasses IAP.
  5. IAP’s service agent (service-<projnum>@gcp-sa-iap.iam.gserviceaccount.com) needs roles/run.invoker on the service.
  6. Single hostname → plain google_compute_managed_ssl_certificate; skip Certificate Manager wildcard/DNS-01 complexity.

Plan

  1. Repo hygiene (done 2026-07-27): git init, private GitHub repo shapor/incline
  2. Hugo scaffold: minimal theme (or PaperMod), content/ symlinked/mounted over existing dirs; sections: Home (current index.html content), Lease, School, Community (profile pages generated from community/people/*.md), Docs; JSON search index from people.json
  3. GCP: new personal project (billing on personal card — cost ≈ pennies: GCS + Cloud Run scale-to-zero + one forwarding rule ≈ $18/mo for the global LB address/rule — the only real cost; alternative if $18 rankles: IAP on Cloud Run directly (no LB) which is now supported, $0 idle)
  4. Deploy loop: hugo && gsutil -m rsync -r public/ gs://incline-portal/site/ (Makefile target); later a GitHub Action on push
  5. DNS: e.g. incline.<personal-domain> A-record → LB IP (or the run.app-IAP URL if going LB-less)
  6. Access: IAM grants for the 2–3 Google accounts; test from Jessica’s phone

Roadmap — features that cross the “needs a server” line

The static + browser-hits-public-API model covers weather, webcams, calendar embeds, and
photo links. The following need server-side logic (secrets, polling, or write-back) and are
where we’d add a single serverless function beside the site — not a rewrite:

Non-goals (for now)

Public web access, in-browser editing (edit via git/Claude sessions), a mobile app.

Open questions