# Project Research Summary

**Project:** Bigscreen Website — Page Setup & Publishing Flow (Milestone v1.0)
**Domain:** Brownfield SPA augmentation — dynamic page registry + LLM-operated static-page publishing pipeline on top of an existing CRA + Builder.io site, integrated with the Arda OAuth provider already shipped in the `cloud` monorepo
**Researched:** 2026-05-21
**Confidence:** MEDIUM-HIGH

## Executive Summary

This is a brownfield "thin platform" milestone — not a CMS, not a competitor to Netlify, but a narrowly scoped operations layer that lets one marketer (Max) plus his LLM agents publish static microsites to live `www.bigscreenvr.com/<path>` URLs without going through Jenkins or Builder.io. Across all four research dimensions a single shape emerges: a Node CLI (`bsweb`) authenticates against Arda over OAuth, uploads immutable content-hashed asset trees to a new S3 bucket via presigned URLs minted by `apps/admin_api`, and flips a pointer in a `site_pages` table. A read-only snapshot of that table is served publicly through `apps/api` (which already has the bigscreenvr.com CORS umbrella and a public LB), cached at the CloudFront edge, and consumed by a new dynamic-route branch added *above* the existing `App.js` regex dispatcher. The first deliverable — `/10years` — wraps the pre-built `bigscreen10` microsite in the SPA's existing nav/footer chrome.

The load-bearing decisions are tightly coupled and largely already settled by the research: **(1)** Arda's OAuth provider is already shipped (plan 14 in the `cloud` repo) and supports PKCE — the milestone adds two new scopes (`website:read`, `website:write`) and registers a `bsweb-cli` client, not a new auth system; **(2)** writes go to `apps/admin_api`, public reads to `apps/api`, and a new bucket `bigscreen-static-pages` with object keys `<slug>/<sha>/<path>` gives atomic deploys and instant rollback via a single SQL UPDATE; **(3)** git is the version of record, which collapses huge swaths of CMS feature scope (rollback, audit, history, drafts → "use git"); **(4)** the CLI's LLM-ergonomic surface (`--json` everywhere, structured errors, manifest-as-contract, bundled `.claude/skills/publish-page/SKILL.md`) is the actual differentiating axis, not a polish item.

The one decision the research is not unanimous on — and which **must be resolved in Phase 1** — is how static HTML actually reaches the screen: STACK.md argues for **sandboxed iframe** because it lets `bigscreen10` ship unchanged and contains a structural CSS/JS isolation boundary; ARCHITECTURE.md argues for **inject by default** because it preserves single-document scroll and site-chrome cohesion. PITFALLS.md confirms CSS bleed (#4) is rewrite-grade if mishandled. The cross-cutting risks are concentrated at CloudFront and the registry: behavior ordering can silently serve `index.html` as CSS (#1), the registry endpoint is a single point of failure for *every* SPA route if it goes down (#2), and a single malformed manifest from Max's LLM agent could take the site down (#3). Mitigations exist for all of them but they cannot be retrofitted — they need to be baked into Phase 1 (CloudFront + registry data model + reserved-path allowlist) before the first publish runs end-to-end.

## Key Findings

### Recommended Stack

The stack is conservative and brownfield-respectful: every dependency is either already in the SPA (CRA 5, React 18, react-router-dom 6, Builder.io, superagent `BigApi`), already in the `cloud` repo (Arda OAuth, Postgres, S3 patterns), or a small surgical addition (the CLI's dependencies). No framework swap, no global state library added to the SPA, no new microservice.

**Core technologies:**
- **react-router-dom 6.28+** (bump from `^6.0.2`): `createBrowserRouter` + `patchRoutesOnNavigation` is the only first-party React Router v6 API for runtime-discovered routes. Trivial bump; no v7 rename needed.
- **Sandboxed `<iframe>` render layer** (with `@open-iframe-resizer/core` 6.x, MIT) **OR** **scoped innerHTML inject** with attribute-selector CSS scoping — render-layer decision is the single highest-leverage choice, deliberately left to Phase 1 verification (see Gaps below).
- **Commander.js 14 + `openid-client` 6 + `@napi-rs/keyring` 1** for the `bsweb` CLI — zero-dep CLI framework, certified OIDC client with PKCE/device support, OS-keychain token storage that replaces the archived `keytar`.
- **`@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/client-cloudfront` v3.700+** — modern SDK, multipart upload, single-path CloudFront invalidations.
- **PostgreSQL `site_pages` + `site_pages_audit` tables** added to the existing fabricator DB via `apps/db_setup` migration — single mutable pointer per slug (`currentSha`), immutable content tree in S3.
- **New `apps/admin_api` route module** (`api/src/site/SitePagesApi.ts`) for writes, gated by `requireScopeAndPolicy({ scopes: ["website:write"], policies: [Admin] })`; new read endpoint on **`apps/api`** for the public snapshot at `/site/pages.json`.
- **New CloudFront behaviors:** `/api/site/pages.json` → `apps/api` (60s TTL + stale-while-revalidate); `/static-pages/*` → new S3 bucket via OAC with a CloudFront Function rewriting `<slug>` → `<currentSha>`. Default catch-all behavior remains unchanged — SPA continues to absorb all flat-URL paths.
- **GitHub Actions builds** in the `static-pages` repo run any optional `npm run build` step; the CLI never executes marketer-supplied build commands. The whole sandboxing problem class is eliminated by architecture.

### Expected Features

**Must have (table stakes for Core Value):**
- Per-page YAML/JSON manifest declaring `path`, `source`, `wrapper`, `title`, `meta`, optional `build`, optional `redirect_from`, optional `namespace`, `mountMode`
- Live page registry endpoint readable by the SPA at runtime (no Jenkins redeploy to surface a new URL)
- SPA dynamic dispatcher branch added **above** existing `App.js` regex filters; legacy six-branch routing preserved untouched
- `bsweb publish`, `validate`, `list`, `status`, `unpublish`, `preview` commands — all idempotent
- Arda OAuth auth via PKCE-loopback (CLI opens browser → arda consent → token exchange)
- CloudFront cache invalidation on publish (single-path, `/api/site/pages.json*`)
- Static asset hosting under content-hashed S3 prefixes `<slug>/<sha>/<path>`
- 301 redirect support via manifest `redirect_from` field
- Both flat (`/10years`) and namespace (`/10years/*`) routing modes
- Reserved-paths allowlist enforced at write time (rejects `/`, `/account/*`, `/scan/*`, `/enrollprivacy`, all known Builder.io paths, etc.)

**Should have (LLM-ergonomic differentiators):**
- `--json` flag on every command + structured errors with `code`, `message`, `suggestion`, `docs_url`
- `.claude/skills/publish-page/SKILL.md` shipped in the `static-pages` repo (Anthropic Agent Skills format, Dec 2025)
- Self-describing schema (`bsweb schema --json`, `validate --explain`)
- Idempotent publish (content-hash skip when manifest+assets unchanged)
- Typed-confirm token for destructive ops (LLM must reason about the literal path being overwritten)
- Dry-run by default for agent-initiated publishes

**Defer (v2+):**
- `diff` command, operation log, draft URLs on prod CDN, asset hash-stamping by the CLI
- Image optimization, scheduled publish/expiry, multi-environment promotion
- Multi-user RBAC, A/B testing, webhooks, per-page CDN config, i18n routing, site search
- A dedicated `SiteEditor` Arda policy (use existing `Admin` policy for v1; promote later)
- Web GUI for Max (explicitly out of scope — LLM agent *is* the UI)
- Subsuming Builder.io routing into the registry

### Architecture Approach

Three repos cooperate at their existing organizational boundaries: **this `website` SPA** gets a new `src/components/DynamicPage/` directory and one new dispatcher branch in `App.js`; the **`cloud` monorepo** gets a new `api/src/site/SitePagesApi.ts` module mounted on `apps/admin_api` for writes and `apps/api` for the public read snapshot, plus a Postgres migration; a **new `static-pages` repo** holds page source + manifests; and a **new `bsweb` CLI** (separate repo or yarn workspace) handles auth, upload, and publish.

**Major components:**
1. **`bsweb` CLI** — PKCE-loopback OAuth → presigned-URL S3 upload → `POST /admin/site/pages/publish` orchestration. Single binary, OS-keychain token storage, JSON-output mode everywhere.
2. **`apps/admin_api` writes (new route module)** — CRUD over `/admin/site/pages/*`, gated by `requireScopeAndPolicy`. Mints presigned PUT URLs (no long-lived AWS credentials in the CLI). Audit-logs every mutation.
3. **`apps/api` public read endpoint** — `/site/pages.json` snapshot served behind the bigscreenvr.com CORS umbrella, 60s edge TTL + stale-while-revalidate.
4. **`site_pages` Postgres table** — single mutable `currentSha` per slug; old shas remain reachable in S3 for instant rollback.
5. **New S3 bucket `bigscreen-static-pages-prod`** — OAC-only, immutable `<slug>/<sha>/<path>` keys, lifecycle expiry of unreferenced shas after 30 days.
6. **CloudFront behaviors** — two new behaviors carved out *with higher precedence than the SPA catch-all*; SPA fallback remains for everything else.
7. **SPA `<DynamicPage>` branch** — added above `builderIoFilter` in `App.js`; renders matched content inside the existing `<Page>` wrapper (Header + Footer).
8. **`static-pages` repo + GitHub Actions** — page source lives here; CI builds (if a `build` block is declared) and invokes `bsweb publish` on merge. Builds never execute on Bigscreen infrastructure.

### Critical Pitfalls

1. **CloudFront SPA-fallback eats new routes** — Custom Error Response that rewrites `403/404 → /index.html` will silently serve HTML as CSS/JS when an asset 404s. Prevention: carve `/static-pages/*` and `/api/site/pages.json` as higher-precedence behaviors, scope the error-rewrite to `Accept: text/html`, add an integration test that asserts Content-Type matches extension. **Phase 1 gate.**
2. **Registry as single point of failure** — every SPA route depends on the registry being reachable. Prevention: bake a fallback registry into the SPA bundle at build time (live registry *augments*, doesn't *replace*), aggressive SWR caching, circuit-breaker that falls through to built-in routes after two consecutive fetch failures. **Phase 1 gate.**
3. **Marketer-induced production outage via manifest** — Max (or his LLM) publishes `path: "/"` or collides with `/account/*` or an existing Builder.io path → site down. Prevention: server-side reserved-paths allowlist enforced at write time; immutable slugs (directory name *is* the slug); typed-confirm token for destructive ops; mandatory diff preview; audit log distinguishing `actor=max-human` from `actor=max-agent`. **Phase 1 gate.**
4. **CSS bleed when injecting static HTML** — `bigscreen10`'s global resets (`body { margin: 0 }`, `* { box-sizing }`) clobber the SPA nav/footer; SPA's global SASS deforms the microsite. Prevention: either choose iframe (structural isolation, no bleed possible) or commit to attribute-selector CSS scoping in the CLI's publish-time HTML transform. **Decide before Phase 5 (SPA dispatcher) — see Gaps.**
5. **Asset path resolution under nested paths** — `bigscreen10` uses absolute `/assets/main.css` references that 404 when served under `/10years/`. Prevention: mandate relative paths, or have the CLI inject `<base href="/static-pages/<slug>/<sha>/">` in `<head>` at publish time, or have the CLI rewrite paths.
6. **LLM agent misinterprets manifest / overwrites wrong page** — agent edits the wrong manifest because slugs are similar, invokes `--force` it doesn't understand. Prevention: directory name = slug (immutable), typed-confirm tokens for destructive ops, dry-run-by-default for agent invocations, per-actor rate limit, SKILL.md enumerates forbidden operations.

Plus moderate pitfalls worth front-loading: race condition between registry write and asset reachability (#7 — publish order must be upload → wait → register), Builder.io coexistence and resolution order (#11 — `getComposedRegex` is order-sensitive; new branch must go *above* `builderIoFilter` without changing existing semantics), and token storage on disk (#10 — OS keychain via `@napi-rs/keyring`, never plaintext file).

## Implications for Roadmap

Phase order is **strictly bottom-up** because every upstream phase de-risks the next. Mock-driven UI work hides integration problems until there's no time to fix them — get a real static file served through CloudFront *before* writing the React mount component, so the CSS-bleed pain is visible from day one rather than week three.

### Phase 1: Foundations & Verification

**Rationale:** Six load-bearing assumptions about external systems must be confirmed before any code is written, and four design decisions must be settled.
**Delivers:** Locked design doc covering (a) CloudFront distribution ID, behavior ordering, and carve-out plan for `/static-pages/*` and `/api/site/pages.json`; (b) confirmation that Arda's OAuth provider accepts `127.0.0.1:*` PKCE redirect URIs (or device-flow endpoints if loopback is rejected); (c) confirmation that `apps/admin_api`'s network reachability supports OAuth-bearer traffic from public IPs (or a chosen workaround); (d) the inject-vs-iframe render-layer decision with a tested prototype against `bigscreen10`; (e) registry SPOF strategy (baked-in fallback registry in SPA bundle); (f) reserved-paths allowlist drafted and reviewed.
**Avoids:** Pitfalls #1 (CloudFront fallback), #2 (registry SPOF), #4 (CSS bleed).

### Phase 2: Registry Data Model + Admin API Endpoints

**Rationale:** Contract drives everything downstream. Build the `site_pages` + `site_pages_audit` migration and `SitePagesApi.ts` first with stub auth, tested against local Postgres + LocalStack S3.
**Delivers:** Migration in `apps/db_setup/site_db_setup.ts`; `api/src/site/SitePagesApi.ts` with `POST /admin/site/pages/upload-url`, `POST /admin/site/pages/publish`, `GET /admin/site/pages`, `GET /admin/site/pages/:slug/history`, `POST /admin/site/pages/rollback`; reserved-paths allowlist enforced at write time.

### Phase 3: OAuth Integration

**Rationale:** Doing OAuth early forces the auth contract to be honest. Arda OAuth provider already shipped (plan 14).
**Delivers:** `website:read` / `website:write` scopes added to `auth/OAuthScopes.ts`; `bsweb-cli` OAuth client registered; admin_api endpoints switched from stub auth to `requireScopeAndPolicy({ scopes: ["website:write"], policies: [Admin] })`.

### Phase 4: `bsweb` CLI MVP

**Rationale:** Build CLI against the real admin_api.
**Delivers:** `bsweb login` (PKCE loopback or device flow per Phase 3 outcome), `bsweb publish ./<dir>` (validate → content-hash → presigned-URL upload → publish call), `bsweb ls`, `bsweb status <slug>`, `bsweb unpublish <slug>`, `bsweb rollback <slug> --to <sha>`. All commands support `--json`. Manifest schema published. OS-keychain token storage via `@napi-rs/keyring`.
**Avoids:** Pitfalls #10, #14, #15, #6.

### Phase 5: CloudFront Behaviors + S3 Bucket + Public Snapshot

**Rationale:** Stand up `/static-pages/*` and `/api/site/pages.json` behaviors and confirm static content is reachable end-to-end through the CDN before writing any React mount code. Surfaces CSS-bleed and asset-path pain with real fetches.
**Delivers:** `bigscreen-static-pages-prod` S3 bucket with OAC; CloudFront behaviors with sha-rewrite Function; public read endpoint `/site/pages.json` on `apps/api`; integration test asserting correct Content-Type.
**Avoids:** Pitfalls #1, #5, #9.

### Phase 6: SPA Dynamic Dispatcher

**Rationale:** React side now has a real fetch target. Add `<DynamicPage>` branch above `builderIoFilter`, ship the chosen mount mode (iframe or inject + scoped CSS), wire the fallback registry. **Schedule extra time for CSS-collision debugging.**
**Delivers:** `src/components/DynamicPage/{index,useRegistry,IframeMount or InjectMount}.js`; one new branch in `App.js`; SPA bundle ships a fallback registry; SWR cache + revalidate-on-focus; bump react-router-dom to 6.28+ and migrate to `createBrowserRouter` + `patchRoutesOnNavigation`.
**Avoids:** Pitfalls #11, #12, #13, #4.

### Phase 7: `/10years` Content Adaptation + Namespace Variant

**Rationale:** Adapt `bigscreen10`/`timeline` source into `static-pages/10years/`, add a second sub-page (e.g. `/10years/intro`) to exercise namespace routing.
**Delivers:** `static-pages/10years/manifest.json` + content tree; first end-to-end publish in dev; `/10years` and `/10years/intro` both reachable with site chrome.

### Phase 8: Production Cutover

**Rationale:** Promote OAuth client + scopes to prod, deploy admin_api routes, add CloudFront behaviors to prod distribution, ship SPA changes via Jenkins (the **last** Jenkins deploy needed for the milestone), smoke-test rollback, retire `bigscreen10` VPS.
**Delivers:** `/10years` live on `www.bigscreenvr.com` via the new pipeline; mutation test proves end-to-end edit cycle works without Jenkins or Builder.io.

### Phase 9: Claude Code Skill + CLAUDE.md

**Rationale:** Once CLI is stable with dry-run + typed-confirm in place, write the skill. Skill describes real behavior, not aspirational.
**Delivers:** `.claude/skills/publish-page/SKILL.md` in `static-pages/`; CLAUDE.md guidance for Max's day-to-day agent workflow.

### Phase Ordering Rationale

- **Phase 1 is non-negotiable as a separate phase.** Cost of confirming each external assumption is hours; cost of getting any wrong is weeks.
- **Phases 2 → 3 → 4 is dependency-strict:** CLI can't be built before OAuth scopes exist; OAuth gating can't be added before there's an endpoint to gate.
- **Phase 5 before Phase 6 is deliberate** — get a real static file served through real CloudFront before writing React mount code.
- **Phase 7 (content) before Phase 8 (cutover):** content must work end-to-end in dev before any prod-environment change.
- **Phase 9 (skill) is last** because skill describes real CLI behavior, including all safety features added in Phase 4.

### Research Flags

Phases likely needing deeper research during planning:
- **Phase 1:** is itself a research/verification phase.
- **Phase 5 (CloudFront):** CloudFront Function pattern for `slug → currentSha` rewrite is the least-documented piece. Implementation path varies depending on whether distribution config lives in devops repo / Terraform / Jenkins-managed JSON.
- **Phase 6 (SPA dispatcher):** if inject mount mode is chosen, CSS scoping at publish time is the highest-effort, lowest-documented sub-problem (selector rewriting that handles `@media`, `@keyframes`, CSS variables correctly).

Standard patterns (skip research-phase): Phases 2, 3, 4, 9.

## Confidence Assessment

| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | Library versions Context7-verified or pulled from official docs. Only uncertainty is at the iframe-vs-inject seam, which is a design choice, not a library question. |
| Features | MEDIUM-HIGH | Table-stakes set is HIGH. LLM-ergonomic differentiators are MEDIUM (emerging space, 2025-2026 patterns less battle-tested). Anti-features HIGH. |
| Architecture | HIGH | Read directly from `cloud/docs/architecture.md`, `cloud/docs/services/oauth.md`, `cloud/docs/external-services.md`, `cloud/docs/webapps/arda.md`. The `cloud` repo's three-service split is concrete. MEDIUM only on the live CloudFront distribution config, which lives outside both `website` and `cloud` repos. |
| Pitfalls | HIGH | Most pitfalls derive from concrete codebase evidence or well-documented CloudFront/SPA gotchas. |

**Overall confidence:** HIGH on what to build; MEDIUM on a small set of external-system specifics that Phase 1 must close.

### Gaps to Address

Open verification items — all must be closed in Phase 1 before Phase 2 begins:

- **CloudFront distribution config:** Confirm distribution ID, origin layout, current Custom Error Response rules, where config is managed (devops repo / Terraform / Jenkins). Action: ask devops or grep `cloud` and adjacent repos.
- **Arda OAuth device-flow vs loopback support:** STACK.md prefers RFC 8628 device flow; ARCHITECTURE.md prefers PKCE loopback. Arda supports PKCE; does NOT yet expose `/oauth/device_authorization`. Action: confirm whether loopback is acceptable for Max's workflow. If yes, ARCHITECTURE.md's path wins with zero Arda team changes.
- **`admin_api` network reachability for OAuth-bearer traffic from public IPs:** `apps/admin_api` is IP-restricted at SG level today. Options: (A) open SG for path-based ALB routing on `/admin/site/*` and `/admin/oauth/*`; (B) proxy through `apps/api`; (C) require Max to be on VPN. Action: confirm with cloud-infra owner in Phase 1.
- **Render-layer decision: iframe vs scoped-inject.** Deciding factor: whether `bigscreen10` can ship unchanged (iframe wins) or single-document scroll + site-chrome cohesion matters more (inject wins). Action: 1-day prototype against `bigscreen10`. Default if no time: **iframe**.
- **`static.bigscreenvr.com` alias:** STACK.md recommends a subdomain alias for clean same-origin iframe `postMessage`. ARCHITECTURE.md prefers `/static-pages/*` as path. Recommend path approach unless render prototype shows specific same-origin friction.
- **`SiteEditor` role vs reusing `Admin`:** Recommendation: use existing `Admin` policy for v1, defer `SiteEditor`.

## Sources

### Primary (HIGH confidence)

- `cloud/docs/architecture.md`, `cloud/docs/services/oauth.md`, `cloud/docs/external-services.md`, `cloud/docs/webapps/arda.md`, `cloud/docs/workspaces.md`
- `cloud/website/src/{api.js,config.js}` — direct inspection confirming no website-side backend in `cloud`
- `bigscreen10/deploy/{README.md,nginx-bigscreen10.conf}` — confirms `bigscreen10` is plain static HTML on a VPS today
- `.planning/codebase/ARCHITECTURE.md`, `.planning/codebase/INTEGRATIONS.md`, `.planning/codebase/CONCERNS.md`
- Context7 — `/remix-run/react-router` (`patchRoutesOnNavigation`, `createBrowserRouter`), `/tj/commander.js` (Commander 14, Node ≥20)
- [React Router — Lazy Route Discovery](https://reactrouter.com/explanation/lazy-route-discovery)
- [AWS — CloudFront OAC for S3 origins](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html)
- [RFC 8628 — OAuth 2.0 Device Authorization Grant](https://oauth.net/2/device-flow/), [RFC 7636 — PKCE](https://datatracker.ietf.org/doc/html/rfc7636), [RFC 8252 — OAuth for Native Apps](https://datatracker.ietf.org/doc/html/rfc8252)
- [Anthropic Agent Skills overview](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
- [Open Iframe Resizer (MIT) docs](https://lemick.github.io/open-iframe-resizer/guides/getting-started/)
- [Netlify CLI deploy + `_redirects` docs](https://docs.netlify.com/cli/get-started/)

### Secondary (MEDIUM confidence)

- [InfoQ — Patterns for AI Agent Driven CLIs](https://www.infoq.com/articles/ai-agent-cli/)
- [Stack Overflow Blog — OAuth 2.0 Device Flow Explained](https://stackoverflow.blog/2026/05/11/oauth-2-0-device-flow-explained-for-engineers-especially-for-backend-engineers/)
- [Grizzly Peak — CLI Framework Comparison](https://www.grizzlypeaksoftware.com/library/cli-framework-comparison-commander-vs-yargs-vs-oclif-utxlf9v9)
- [LogRocket — React iframes best practices](https://blog.logrocket.com/best-practices-react-iframes/)
- [GitOps rollback — Komodor](https://komodor.com/learn/git-revert-rolling-back-in-gitops-and-kubernetes/)

### Tertiary (LOW confidence, needs validation)

- CloudFront distribution config + behavior ordering for `www.bigscreenvr.com` — lives outside both `website` and `cloud` repos; must be sourced in Phase 1
- Whether `apps/admin_api`'s SG can accept OAuth-bearer traffic from arbitrary public IPs without modification
- Existing CSP posture on `www.bigscreenvr.com`
- Whether `bigscreen10`'s CSS uses selectors generic enough to require attribute-prefix scoping

---
*Research completed: 2026-05-21*
*Ready for roadmap: yes*
