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)
- Content: this git repo (private, github.com/shapor/incline) stays the source of truth — markdown + JSON/CSV data files
- Site generator: Hugo (v0.153 extended already installed) — renders the markdown (community profiles, HOA docs, reviews) into a browsable site;
people.jsonpowers client-side search (Fuse.js, no backend); keep the existing hand-built pages initially viastatic/passthrough, port to templates over time - Hosting: GCS bucket + tiny GCS-proxy on Cloud Run behind GCLB + IAP — clone of qs-dev (
qsi-automation/qs-dev/proxy/main.gois self-contained; drop theprivate.subdomain / @querystory.ai special-case) - Auth: IAP with per-user IAM —
roles/iap.httpsResourceAccessorforuser:shapor@gmail.comanduser:shaporandjessica@gmail.com(+ Jessica’s account). IAP is domain-agnostic at the IAM layer; gmail users work.
Hard-won constraints (from qs-dev research, 2026-07)
- 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).
- IAP OAuth Admin API was shut down March 2026 — don’t use
google_iap_brand/google_iap_clientTerraform resources. Use the Google-managed OAuth client: enable IAP on the backend service without explicit client id/secret. - 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.
- Cloud Run ingress must be
INTERNAL_AND_GCLB(or internal-LB-only) — otherwise the.run.appURL bypasses IAP. - IAP’s service agent (
service-<projnum>@gcp-sa-iap.iam.gserviceaccount.com) needsroles/run.invokeron the service. - Single hostname → plain
google_compute_managed_ssl_certificate; skip Certificate Manager wildcard/DNS-01 complexity.
Plan
- Repo hygiene (done 2026-07-27): git init, private GitHub repo
shapor/incline - Hugo scaffold: minimal theme (or PaperMod),
content/symlinked/mounted over existing dirs; sections: Home (current index.html content), Lease, School, Community (profile pages generated fromcommunity/people/*.md), Docs; JSON search index frompeople.json - 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)
- Deploy loop:
hugo && gsutil -m rsync -r public/ gs://incline-portal/site/(Makefile target); later a GitHub Action on push - DNS: e.g.
incline.<personal-domain>A-record → LB IP (or the run.app-IAP URL if going LB-less) - 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:
- Delivery automation (Amazon + UPS): auto-ingest shipment status instead of hand-editing
data/dashboard.yaml. Needs: stored carrier credentials/API keys (UPS Tracking API; Amazon
has no clean personal orders API — likely parse the shipment emails via a Gmail-push →
function, or a forwarding address). A scheduled function writes adeliveries.jsonthe
dashboard reads. This is the first real backend dependency; everything else stays static. - Shared editable todos (both phones, persistent) — same shape: tiny API + a store.
Non-goals (for now)
Public web access, in-browser editing (edit via git/Claude sessions), a mobile app.
Open questions
- Domain: personal domain to use? (shapor.com subdomain? new one?)
- LB+IAP ($18/mo, custom domain) vs IAP-on-Cloud-Run-direct ($0 idle, run.app URL or custom domain via Cloud Run domain mapping) — recommend starting LB-less, graduate if needed