# Phase 1: Foundations, Registry & OAuth - Context

**Gathered:** 2026-05-21
**Re-discuss:** 2026-05-22 (FND-02 + FND-03 reopened — see §Re-Discuss below)
**Status:** Ready for planning (Plan 01-03 frontmatter correction + dev-website OAuth backend scope addition pending)

> **Supersedence notice:** §Re-Discuss (2026-05-22) below SUPERSEDES the FND-02 PKCE-loopback lock and modifies the FND-03 Option D lock with respect to the bsweb-cli auth flow. The original §Implementation Decisions block remains for historical context but reflects Arda-V1-incompatible assumptions for the auth path. VERIFICATION-MEMO.md §FND-02 and §FND-03 also require update to match this section.

<rediscuss>
## Re-Discuss (2026-05-22) — FND-02 + FND-03 Reopen vs Arda V1

### Trigger
Arda OAuth client registration UI inspection during Plan 01-03 Task 3 surfaced four contradictions with Phase 1 closures. Full discovery doc: `.planning/phases/01-foundations-registry-oauth/ARDA-V1-BLOCKER.md`.

### Code investigation (cloud monorepo, 2026-05-22)
Inspected files in `C:\Users\decid\gsd-workspaces\cloud\cloud`:
- `auth/OAuthClientDatabase.ts:23,236-238` — `ClientType.Public` exists in enum but explicit throw blocks registration: *"public clients are not supported in v1 — use confidential"*. **No public-client path exists.**
- `auth/OAuthClientDatabase.ts:195-230` — Redirect URI validation: exact-match host parsed via `new URL()`, no wildcards, no port ranges, no fragments. HTTPS required except for `localhost`/`127.0.0.1` (exact-match loopback IS allowed but only at a fixed port).
- `auth/OAuthTokens.ts:59-76` — `authenticateClient()` REQUIRES + bcrypt-verifies `client_secret` on every `/oauth/token` call for both `authorization_code` and `refresh_token` grants. **PKCE alone does not authenticate; the secret is mandatory.**
- `api/src/OAuthApi.ts:138` — `/.well-known/oauth-authorization-server` advertises only `response_types_supported: ["code"]`; no `response_modes_supported` key. **Treat `response_mode=fragment` as NOT supported in V1.**
- `auth/OAuthClientDatabase.ts:51` — Audit enum has `IpRegistrationMismatch` but **no enforcement code path found** in `auth/` or `api/src/OAuthApi.ts`. Server IPs field appears to drive admin_api SG config, not OAuth-time checks.

**Consequence:** Brandon's original "two-hop CLI-PKCE relay" hypothesis (CLI does PKCE exchange itself, dev-website just relays the code) is **not viable** — confidential-client + mandatory client_secret means a server MUST broker code exchange. CLI cannot reach `/oauth/token` directly.

### Locked decisions (D-13 .. D-20)

#### D-20: OAuth backend hostname — api.bigscreencloud.com (supersedes D-13 + D-16 host fields)
**Date:** 2026-05-22 (R-NEW-01 outcome (a) resolution)
**Supersedes:** D-13 host `dev-website.bigscreencloud.com` and D-16 redirect URI host.

**Discovery (Plan 01-03 Task 1, RESEARCH-DELTA outcome a):**
- `dev-website.bigscreencloud.com` → CloudFront → S3 serving THIS repo's CRA `build/` artifact. No Node backend. CRA-only SPA deploy. Cannot mount `/api/cli-auth/*` or `/oauth/cli-callback` handlers without breaking live marketing traffic.
- Cloud monorepo has zero `dev-website` references. Hostname is entirely a website-repo deploy artifact.
- D-13's "dev-website OAuth backend" was planned against a host that does not exist as a Node service. Planning miss in v1/v2.

**Decision:** OAuth backend mounts on `api.bigscreencloud.com` (the apps/api ELB hostname) directly. No new hostname. No CloudFront layering.

**Field-level supersedence:**

| Original (D-13/D-16) | Superseded by D-20 |
|---|---|
| Host: `dev-website.bigscreencloud.com` | `api.bigscreencloud.com` |
| Redirect URI: `https://dev-website.bigscreencloud.com/oauth/cli-callback` | `https://api.bigscreencloud.com/oauth/cli-callback` |
| `/api/cli-auth/start` mount | `https://api.bigscreencloud.com/api/cli-auth/start` |
| `/api/cli-auth/refresh` mount | `https://api.bigscreencloud.com/api/cli-auth/refresh` |
| `client_secret` env var location | Still `ARDA_BSWEB_CLI_CLIENT_SECRET` on apps/api deploy (D-13 step 5 wording updated: "Provision … in apps/api deploy env" not "dev-website deploy") |
| D-16 Server IPs = "resolved IP(s) of dev-website.bigscreencloud.com" | **apps/api egress IP(s)** — VPC NAT gateway EIP(s) (see open-q-egress below) |

**What does NOT change (D-13 flow remains correct):**
- Step 1: CLI binds ephemeral loopback port P.
- Steps 2–9 flow shape: state-bound PKCE, fragment redirect to loopback, single-use state map, stateless refresh, Bearer call to `/api/site/*`.
- D-14 fragment-only token transport invariant.
- D-15 state map TTL + in-memory custody.
- D-18 router mount file `cloud/apps/api/api.ts` (was already on apps/api; D-20 just confirms host = apps/api LB).

**Open question for devops (gates D-16 Arda registration only, not Tasks 2–5 implementation):**
- **open-q-egress:** What is the source IP / IP range that apps/api EC2 instances use when calling Arda `/oauth/token` outbound? Two possibilities:
  - **Public path (NAT gateway):** apps/api → NAT GW EIP → public internet → Arda public endpoint. Server IPs field = NAT GW EIP(s).
  - **Internal path (VPC routing):** apps/api → admin_api/Arda private endpoint via VPC. Server IPs field semantics break (Arda V1 UI takes IPs, not SG references). Resolution: devops adds intra-VPC SG-to-SG rule on admin_api SG; Arda Server IPs field may need a placeholder or empty entry.

Plan 01-03 Task 6 (Arda registration, blocking-human) blocks on devops surfacing the answer. Tasks 2–5 (code + middleware + tests) are independent of this answer and can ship in parallel.

**Cert / TLS:** `api.bigscreencloud.com` ACM cert already terminates on apps/api ELB; no SAN add required.

**Why X4 over X2 (CloudFront behavior on dev-website):**
- X2 preserves D-13 wording but requires devops Terraform edit to dev-website's CloudFront distribution (path-based routing /api/cli-auth/* + /oauth/cli-callback → apps/api origin). CloudFront origin-request policy + WAF tuning + two-hop latency.
- X4 = zero DNS / CloudFront work. Hostname already routes to apps/api. D-13 wording cost is small (host swap). User chose X4 (2026-05-22).
- X1 (CNAME flip) rejected — breaks live marketing.
- X3 (new subdomain) rejected — more DNS+cert work than X4 for no architectural gain.

**Downstream rewrites (mechanical, this rev):**
- Plan 01-03 frontmatter + must_haves + tasks: dev-website.bigscreencloud.com → api.bigscreencloud.com. Bump `replan_iteration: 3`.
- VERIFICATION-MEMO FND-02 v3 + FND-03 v3 append.
- ROADMAP.md: dev-website refs in Phase 1 success criteria → api hostname.
- 01-03-RESEARCH-DELTA.md: append X4 resolution chain.

Historical files (DISCUSSION-LOG, ARDA-V1-BLOCKER, 01-03-PLAN-CHECK*, 01-01-SUMMARY, quick/260521-vlu SUMMARY) preserve dev-website refs as audit trail — NOT rewritten.

---

### Locked decisions (D-13 .. D-19) — historical; D-20 supersedes host fields

#### D-13: Auth architecture — dev-website becomes OAuth backend (FND-02 v2)
**Supersedes:** FND-02 PKCE-loopback lock from Plan 01-01 Task 5.

`dev-website.bigscreencloud.com` is the only server-side component that talks to Arda's `/oauth/token` on behalf of `bsweb-cli`. It holds the confidential-client `client_secret`. bsweb-cli never has the secret and never directly hits `/oauth/token`.

Flow:
1. CLI binds an ephemeral loopback port `P` on `127.0.0.1`, starts a local HTTP server.
2. CLI `POST https://dev-website.bigscreencloud.com/api/cli-auth/start` with `{loopback_port: P}` → dev-website generates `state` nonce + PKCE `code_verifier` + `code_challenge`, stores `{state → {verifier, port, expires_at}}` in-memory (D-15), returns `{auth_url, state}` where `auth_url = https://arda/oauth/authorize?client_id=bsweb-cli&redirect_uri=https://dev-website.bigscreencloud.com/oauth/cli-callback&response_type=code&code_challenge=...&code_challenge_method=S256&scope=website:read website:write website:promote&state=<state>`.
3. CLI opens `auth_url` in default browser (per RFC 8252 §6 — system browser preferred over embedded).
4. User signs into Arda admin session (cookie-gated consent UI, per existing Arda design).
5. Arda 302s browser to `https://dev-website.bigscreencloud.com/oauth/cli-callback?code=<code>&state=<state>`.
6. dev-website `GET /oauth/cli-callback` handler: lookup `state` in memory map → recover `{verifier, port}` → `POST` to Arda `/oauth/token` with `grant_type=authorization_code`, `code`, `redirect_uri=<same>`, `client_id=bsweb-cli`, `client_secret=<env>`, `code_verifier=<recovered>` → receive `{access_token, refresh_token, expires_in, scope}` from Arda.
7. dev-website returns a small HTML page to the browser containing inline JS that does `window.location.replace('http://127.0.0.1:' + <port> + '/cb#access_token=...&refresh_token=...&expires_in=...&token_type=Bearer')`. **Fragment, not query** — keeps tokens out of any HTTP access log.
8. CLI's loopback `GET /cb` returns an inline-JS page that reads `window.location.hash` and POSTs it back to `http://127.0.0.1:<port>/cb-finalize` (same loopback, different path). Loopback server receives parsed tokens server-side, stores in OS keychain (Phase 2 work), closes the loopback server, prints success.
9. CLI calls `apps/api/api/site/*` endpoints directly with `Authorization: Bearer <access_token>` — no further dev-website involvement until refresh.

Refresh (when access_token expires):
- CLI `POST https://dev-website.bigscreencloud.com/api/cli-auth/refresh` with `{refresh_token}` → dev-website wraps Arda `/oauth/token` call with `grant_type=refresh_token` + its `client_secret` → returns new `{access_token, refresh_token, expires_in}` to CLI. **dev-website is a stateless secret-proxy for refresh.**

**Rejected alternatives** (documented in DISCUSSION-LOG.md): (A') dev-website-issued session JWT (adds internal auth layer, unnecessary indirection); (C) drop OAuth for v1, ship signed admin JWT (loses per-user audit + Arda integration); (B) block on Arda V2 public-client support (unknown unblock timeline).

**Rejected sub-variants** of (A): dev-website holds refresh_token server-side (would require persistent session store; user picked stateless variant); CLI holds verifier (splits cryptographic state across two hosts; user picked verifier-on-dev-website).

#### D-14: Token transport — fragment redirect to loopback
**Mechanism:** Step 7 above. dev-website's `/oauth/cli-callback` response is a static HTML page whose body is a single inline `<script>` doing `window.location.replace('http://127.0.0.1:' + state.port + '/cb#' + tokenFragment)`. The fragment-only encoding guarantees:
- Tokens never appear in dev-website's HTTP access logs (server never sees the fragment).
- Tokens never appear in CloudFront / LB access logs for the same reason.
- Tokens never appear in browser history beyond the user's own browser.

**State→port binding:** the `state` nonce must be cryptographically tied to the port to prevent a malicious page from triggering the JS-redirect with a different port. dev-website's in-memory map (D-15) holds `(state, port)` together; the redirect script is rendered server-side with the port baked in from the map lookup, not from any URL/cookie input.

#### D-15: PKCE verifier custody — in-memory map with 5-minute TTL
dev-website holds `Map<state, {verifier: string, port: number, created_at: Date}>` in process memory. Entries expire 5 minutes after creation (background sweep or check-on-lookup). `state` is 32 bytes of `crypto.randomBytes`, base64url-encoded.

**Rationale:** zero infra surface; multi-instance not required for v1 (dev-website single-instance today); cookie-on-browser alternative rejected for cookie-size + key-management overhead; Postgres table rejected as schema-overhead for ephemeral 5-minute state.

**On restart:** in-flight flows fail with `state_unknown` → CLI shows clean error, user retries.

#### D-16: Arda client registration shape (AUTH-02 v2)
Locked configuration for the `bsweb-cli` client in Arda's V1 registration UI:
- **Name:** `bsweb-cli`
- **Description:** `CLI publisher for www.bigscreenvr.com static pages`
- **Logo URL:** (defer)
- **Homepage URL:** (defer; could be the future bsweb docs URL)
- **Redirect URIs:** `https://dev-website.bigscreencloud.com/oauth/cli-callback` — single exact-match entry.
- **Allowed scopes:** `website:read`, `website:write`, `website:promote` (post-rename, see D-17). All three must appear in Arda's dropdown before registration.
- **Server IPs:** resolved IP(s) of `dev-website.bigscreencloud.com`. dev-website is the only server that calls `/oauth/token`; this is semantically correct. If Arda V2 later wires IP-enforcement into `/oauth/token`, only dev-website will pass — matches our architecture exactly.
- **Client type:** confidential (only V1 option).

**Storage of returned `client_secret`:** Arda dashboard displays the plaintext once on creation. Store in dev-website's deploy environment as `ARDA_BSWEB_CLI_CLIENT_SECRET` env var (per cloud's secret-handling conventions; researcher confirms exact var name). Never commit to repo.

**Prod registration:** out-of-scope for Phase 1. A separate `bsweb-cli` client will be registered against the prod Arda surface with `https://www.bigscreenvr.com/oauth/cli-callback` as redirect URI when prod cutover ships (Phase 4).

#### D-17: Scope rename `site:*` → `website:*` (BEFORE the Arda deploy)
Renames the OAuth scope identifiers originally added under the `site:*` prefix to the `website:*` prefix: `site:read` → `website:read`, `site:write` → `website:write`, `site:promote` → `website:promote`. Applies BEFORE the scopes are merged to `dev-gem` / deployed to Arda — clean window with zero deployed callers.

**Scope of rename** (planner produces task list; this is the index):
- **cloud monorepo:** commits `a913ef2b` (originally added `site:read` + `site:write`) and `983974ac` (originally added `site:promote`) on `dev-web-publisher` branch must be amended/superseded before cherry-pick. Specifically the `OAuthScope` enum keys (`SiteRead` → `WebsiteRead`, `SiteWrite` → `WebsiteWrite`, `SitePromote` → `WebsitePromote`), enum values (`"site:read"` → `"website:read"`, `"site:write"` → `"website:write"`, `"site:promote"` → `"website:promote"`), and the `ScopeRequiredPolicies` entries. No external callers — these were never deployed.
- **website repo planning docs:** `.planning/ROADMAP.md`, `.planning/REQUIREMENTS.md`, `.planning/PROJECT.md`, `.planning/STATE.md`, `.planning/phases/01-foundations-registry-oauth/{01-CONTEXT.md (this file, decisions section), 01-RESEARCH.md, 01-PATTERNS.md, 01-01-PLAN.md, 01-01-SUMMARY.md, 01-02-PLAN.md, 01-02-SUMMARY.md, 01-03-PLAN.md, 01-04-PLAN.md, 01-05-PLAN.md, VERIFICATION-MEMO.md, ARDA-V1-BLOCKER.md, SKELETON.md}`.
- **cloud-side downstream consumers:** Plan 01-03 Task 2's `SitePagesPresign.ts` and Plan 01-02's modules do NOT reference scope strings directly — they're guarded by `requireScopeAndPolicy` invocations at the router layer (not built yet). The scope rename only touches the scope catalog + planning docs; no cloud-side implementation code references the literal `"website:read"` etc. yet.

**Sequencing:** rename in this repo first (commit), then cloud-side commits get superseded (force-push to `dev-web-publisher` OR new clean branch off `dev-gem` with renamed commits), then cherry-pick the two scope-add commits → PR `cloud/dev-gem` → merge → Arda redeploy → scope dropdown populates → register `bsweb-cli` per D-16.

#### D-18: Plan 01-03 router mount target correction (FND-03 Option D holds)
Plan 01-03's `01-03-PLAN.md` frontmatter currently lists `cloud/apps/admin_api/admin_api.ts` in `files_modified`. This contradicts FND-03 Option D, which locked site-publishing endpoints to `apps/api`. **Correct the frontmatter** before resuming execution:
- `files_modified` swap `cloud/apps/admin_api/admin_api.ts` → `cloud/apps/api/api.ts` (or the precise router file in `apps/api` — researcher/executor confirms exact filename against cloud repo state).
- Update `affects` and `requirements` lines to reflect the mount target if currently mis-tagged.
- Routes mount: `POST /api/site/upload-url`, `POST /api/site/publish` (Plan 01-03), plus `POST /api/site/rollback`, `POST /api/site/unpublish`, `GET /api/site/pages`, `GET /api/site/pages/:slug/history` (Plan 01-04), plus `GET /site/pages.json` (Plan 01-05). All on `apps/api`.
- All write routes wrap in `requireScopeAndPolicy({ scopes: ['website:write'], policy: Admin })`. Read routes wrap in `{ scopes: ['website:read'], policy: Admin }`. Public read snapshot endpoint (`/site/pages.json` on apps/api) is unauthenticated per FND-03 Option D + Phase 1 success criterion §3.

#### D-19: Scope deploy sequencing — cherry-pick + clean PR (resolves Q6)
**Sequence:**
1. Rename `site:*` → `website:*` per D-17 in this repo (single commit; planning-doc-only, no code).
2. On `cloud/dev-web-publisher` branch: amend or supersede commits `a913ef2b` + `983974ac` with renamed enum + value (planner decides amend vs new commits; either is fine since these are not yet shared).
3. Create a clean branch `cloud/scopes-website-only` off `cloud/dev-gem`. Cherry-pick the two renamed scope commits onto it. Push, PR to `dev-gem`, merge.
4. Cloud team triggers Arda redeploy (their normal pipeline). Verify scope dropdown shows `website:read`, `website:write`, `website:promote`.
5. Register `bsweb-cli` per D-16 in Arda dashboard. Capture `client_id` + plaintext `client_secret`. Provision `ARDA_BSWEB_CLI_CLIENT_SECRET` env var in dev-website deploy.
6. Plan 01-03 Tasks 4–5 unblock; rest of Phase 1 work resumes.

### New scope: dev-website OAuth backend (NOT IN ORIGINAL PHASE 1 PLAN)
D-13 introduces new code on `dev-website.bigscreencloud.com` that wasn't planned. Researcher must resolve where this code lives before planner writes new tasks.

**Open research question (researcher resolves before planning):**
- **R-NEW-01:** Is `dev-website.bigscreencloud.com` (a) the dev/staging deploy of THIS `website` repo (CRA SPA — no backend capability), (b) a separate cloud-monorepo service, or (c) a hostname alias for `apps/api`'s LB? If (a), where do `/api/cli-auth/start`, `/oauth/cli-callback`, `/api/cli-auth/refresh` get implemented? **Recommended:** mount the three CLI-auth endpoints on `apps/api` and CNAME `dev-website.bigscreencloud.com` to `apps/api`'s LB — consolidates secret handling, reuses existing auth infra, no new service. Researcher confirms this against cloud `docs/architecture.md` + DNS state.

**If R-NEW-01 resolves to (c) / "endpoints on apps/api":**
- Three new handlers on `apps/api`: `POST /api/cli-auth/start`, `GET /oauth/cli-callback`, `POST /api/cli-auth/refresh`.
- In-memory `Map<state, {verifier, port, created_at}>` module (D-15) — singleton in apps/api process.
- Static HTML+JS template for the callback page (D-14 fragment-forward).
- Three new integration tests (start → callback → fragment-redirect; refresh round-trip; state TTL expiry).
- These probably fold into a new wave between current Wave 3 and Wave 4, OR get added to Plan 01-03 alongside the publish endpoints (planner decides).

### Open questions resolved (vs ARDA-V1-BLOCKER.md Q1..Q6)
- **Q1** (response_mode=fragment): NOT supported in V1 — code investigation; dev-website implements client-side fragment via inline JS (D-14), not server-side.
- **Q2** (public-client path elsewhere): NO — `OAuthClientDatabase.ts:236-238` hard-blocks.
- **Q3** (client_secret mandate): YES, confidential clients require bcrypt-verified secret on every token call. Resolved by D-13 (dev-website holds secret).
- **Q4** (dev-website IPs on admin_api SG): not gating — no OAuth-time enforcement code found; field filled per D-16 with dev-website IPs for semantic accuracy.
- **Q5** (Plan 01-03 mount target): apps/api (D-18).
- **Q6** (scope deploy sequencing): cherry-pick + clean PR (D-19).

### Updated canonical refs (additions to §Canonical References below)
- `.planning/phases/01-foundations-registry-oauth/ARDA-V1-BLOCKER.md` — discovery doc; full evidence chain for Q1–Q6.
- `C:\Users\decid\gsd-workspaces\cloud\cloud\auth\OAuthClientDatabase.ts:23,51,195-230,236-238` — ClientType enum, IP audit enum, redirect URI validation, public-client block. **MUST read before planning the dev-website OAuth backend code.**
- `C:\Users\decid\gsd-workspaces\cloud\cloud\auth\OAuthTokens.ts:59-76,186,238` — `authenticateClient` (secret bcrypt check); confirms secret is mandatory on both authorization_code and refresh_token grants.
- `C:\Users\decid\gsd-workspaces\cloud\cloud\api\src\OAuthApi.ts:138,185-189` — `/.well-known` metadata (no response_modes_supported), proxy-aware IP extraction.
- RFC 8252 §6 — Native App OAuth (system browser, loopback redirect) — still authoritative for the loopback-side; only the server-side broker changes.

</rediscuss>

<domain>
## Phase Boundary

Close six external-system verification gaps (FND-01..06) AND stand up the registry contract end-to-end against local dev infrastructure:

- `site_pages` + `site_pages_audit` Postgres tables exist via migration in cloud monorepo
- `apps/admin_api` exposes write endpoints: `upload-url`, `publish`, `pages` (list), `pages/:slug/history`, `rollback` — all gated by `requireScopeAndPolicy({ scopes: ["website:write"|"website:read"], policies: [Admin] })`
- `apps/api` exposes public read snapshot at `/site/pages.json` under bigscreenvr.com CORS umbrella, no admin token required
- New OAuth scopes `website:read` / `website:write` added to `auth/OAuthScopes.ts`
- `bsweb-cli` OAuth client registered in Arda
- Reserved-paths allowlist enforced server-side at every write
- Per-actor write rate-limit prevents agent runaway
- Audit log distinguishes `actor=human` vs `actor=agent`

Everything runs locally against dev Postgres + LocalStack S3 before Phase 2 (CLI + CDN) begins. No CloudFront, no real S3, no CLI work in this phase.

</domain>

<decisions>
## Implementation Decisions

> **Note:** D-13..D-19 in §Re-Discuss (2026-05-22) supersede the FND-02 PKCE-loopback and FND-03 Option D auth-path decisions below for the bsweb-cli flow. Decisions D-01..D-12 below remain authoritative for everything else.

### Verification gap execution (FND-01..06)
- **D-01:** Claude-led investigation across all 6 gaps. Codebase reads against `cloud`, `bigstack`, `bigscreen10` repos first. Escalate to humans (cloud-infra owner, Arda team) only on LOW confidence after exhausting code-side answers.
- **D-02:** Each gap produces a written closure artifact (paragraph in phase Verification Memo with: question, answer, confidence, evidence source). Phase 1 verifier checks all 6 closed before Phase 2 unblocks.

### Render-layer choice (FND-04)
- **D-03:** **Scoped-inject** chosen over iframe. Mounts inside SPA's existing `<Page>` wrapper for single-document scroll and nav/footer cohesion.
- **D-04:** **Shadow DOM web component wrapper** is the chosen isolation mechanism. Researcher must:
  - Survey React/Node ecosystem for Shadow-DOM-mount packages compatible with React 18 + react-router-dom 6 (e.g. `react-shadow`, `@webcomponents/custom-elements`, native `attachShadow` patterns)
  - Identify a strategy for font/reset re-import inside the shadow root (since SPA global styles do not pierce shadow boundary)
  - Identify a `<slot>`-based approach for any SPA→microsite communication needs (likely none for `/10years`)
  - Document trade-off vs publish-time CSS scoping in RESEARCH.md so planner can validate
- **D-05:** Researcher writes a 1-day prototype task into PLAN.md: mount `bigscreen10` inside a Shadow-DOM web component within the SPA, verify visual fidelity + scroll behavior. Result is FND-04 closure artifact.

### `site_pages` data model + audit (REG-01, REG-07)
- **D-06:** **Single mutable pointer + audit log** schema (research recommendation). `site_pages` has one row per slug with `currentSha`. `site_pages_audit` is append-only and records every mutation (`sha_before`, `sha_after`, `actor_claim`, `verified`, `timestamp`, `manifest_snapshot_json`, `op` ∈ publish|rollback|unpublish).
- **D-07:** Actor distinction is **CLI-included claim, server-verified later**. CLI sends `actor_type: human|agent` in request body. Server records claim verbatim + `verified: false` flag. Hardening (signed assertion or per-actor OAuth client) deferred to v2. Skill / CLAUDE.md instructs LLM agents to send `actor_type: agent`.
- **D-08:** Rollback semantics: query `site_pages_audit` for prior `sha` of slug, set `currentSha = <that sha>`, append new audit row with `op: rollback`. No data deleted; audit is immutable history. Old shas remain reachable in S3 until lifecycle policy expires them (Phase 2 concern).

### Cross-repo coordination
- **D-09:** **Mirror approach.** Real work lands in `cloud` monorepo PRs. This `website` repo's PLAN.md tracks via path + SHA refs (e.g. `cloud@<sha>: api/src/site/SitePagesApi.ts`). Where PRs exist, cross-link PR number. Summary commit (`Phase 1 site_pages migration shipped: cloud@SHA`) lands in this repo per task milestone. No submodule.
- **D-10:** Planner produces PLAN.md tasks that explicitly tag which repo each task lives in. Executor commits in this repo with `[cloud@SHA]` annotation pointing at the cloud-repo commit it represents.

### Reserved-paths allowlist (FND-05, REG-06)
- **D-11:** Defer mechanism choice (TS const vs JSON file vs DB table) to planner. Decision constraint: list must be readable at admin_api startup, expandable without DB migration, and version-controlled in cloud repo so additions are reviewable. Initial seed contents are LOCKED below in Specifics.

### Rate-limit (AUTH-04)
- **D-12:** Defer numeric/scope shape to planner. Decision constraint: must throttle by OAuth-token-subject (user identity), default to a low burst limit (suggested ~30 writes / 5 min) to catch agent runaway, return structured `{code: "RATE_LIMIT", message, retry_after_sec}` error.

### Claude's Discretion
- Exact column types in `site_pages` / `site_pages_audit` (TIMESTAMPTZ vs TIMESTAMP, varchar lengths, JSONB vs TEXT for manifest snapshot)
- LocalStack version + Postgres version (match cloud monorepo dev defaults)
- OAuth scope claim shape (likely standard JWT `scope` claim per Arda convention)
- Exact `requireScopeAndPolicy` invocation patterns — follow cloud monorepo precedent
- Verification Memo file location (`.planning/phases/01-foundations-registry-oauth/VERIFICATION-MEMO.md` recommended)

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Project-level
- `.planning/PROJECT.md` — Milestone scope, Key Decisions, Open Investigation list
- `.planning/REQUIREMENTS.md` — 74 v1 requirements; Phase 1 covers FND-01..06, REG-01..08, AUTH-01..04
- `.planning/ROADMAP.md` §Phase 1 — Success Criteria (5 acceptance items)
- `.planning/STATE.md` — Current progress + accumulated decisions

### Research
- `.planning/research/SUMMARY.md` — Executive synthesis; Phase 1 rationale; gaps section
- `.planning/research/PITFALLS.md` §1, §2, §3, §4 — CloudFront fallback, registry SPOF, marketer-induced outage, CSS bleed. **Pitfalls #1–#4 are Phase 1 gates per SUMMARY.**
- `.planning/research/ARCHITECTURE.md` — Three-service split (admin_api writes, apps/api reads); `site_pages` model rationale; inject-vs-iframe trade-off
- `.planning/research/STACK.md` — OAuth library choices, AWS SDK v3 selections, render-layer trade-offs
- `.planning/research/FEATURES.md` — Must-have / should-have / defer matrix

### Codebase maps
- `.planning/codebase/ARCHITECTURE.md` — `src/App.js:114-160` regex dispatcher (target of Phase 3 replacement, not Phase 1)
- `.planning/codebase/INTEGRATIONS.md` — Existing integrations (Builder.io, Stripe, Hyperbeam, GA)
- `.planning/codebase/CONCERNS.md` — Auth, security posture

### External — `cloud` monorepo (Phase 1 work happens here)
- `cloud/docs/architecture.md` — Three-service split + workspaces
- `cloud/docs/services/oauth.md` — Arda OAuth provider (plan 14), PKCE support, scope model
- `cloud/docs/external-services.md` — External service catalog
- `cloud/docs/webapps/arda.md` — Arda admin app architecture
- `cloud/docs/workspaces.md` — Monorepo workspace conventions
- `cloud/auth/OAuthScopes.ts` (path inferred) — File to extend with `website:read` / `website:write`
- `cloud/apps/db_setup/*` — Where the `site_pages` migration lands
- `cloud/apps/admin_api/*` + `cloud/api/src/*` — Where write route module mounts
- `cloud/apps/api/*` — Where public read snapshot endpoint mounts
- `cloud/website/src/{api.js,config.js}` — Confirmed no website-side backend exists in cloud repo

### External — adjacent projects
- `bigscreen10/deploy/{README.md,nginx-bigscreen10.conf}` — Source state of microsite to be adapted (Phase 3 content concern, but Shadow DOM prototype reads from here)

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **None in this `website` repo for Phase 1** — all Phase 1 code lands in `cloud` monorepo. Website-side changes begin in Phase 3.
- **In cloud repo (per research):** `requireScopeAndPolicy({...})` middleware already exists and gates other admin endpoints; reuse it directly. Existing presigned-URL minting patterns in cloud's S3 helpers should be reused (do not reinvent).

### Established Patterns
- **Three-service split** is locked: writes → `apps/admin_api`, public reads → `apps/api`, no cross-talk between them at the network layer.
- **`requireScopeAndPolicy({ scopes, policies })`** is the canonical authorization wrapper for admin endpoints — Phase 1 endpoints must use it, not bespoke checks.
- **`apps/api` already has the bigscreenvr.com CORS umbrella + public LB** — the public read snapshot endpoint must mount here, not on admin_api.
- **OAuth scope model** is per-token + per-policy (see `cloud/docs/services/oauth.md`). New `site:*` scopes follow existing scope-naming convention.

### Integration Points
- **OAuth scopes** are added to `auth/OAuthScopes.ts` (cloud repo). One file change touches everything downstream.
- **DB migration** goes through `apps/db_setup` (cloud repo).
- **No `website` repo changes in Phase 1.** Website integration starts Phase 3 (`src/components/DynamicPage/`).

</code_context>

<specifics>
## Specific Ideas

### Reserved-paths allowlist — initial seed (FND-05, REG-06)
The allowlist MUST cover, at minimum:
- `/` (root — never publishable as static page)
- `/account/*`, `/auth/*`, `/scans/*`, `/scan/*`, `/token2/*`, `/browser/*` — React-owned screens
- `/privacypolicy`, `/enrollprivacy`, `/hardwareterms`, `/termsofservice` — React-owned legal pages
- All currently-known Builder.io paths — researcher extracts the full list from `src/App.js:114-160` `builderIoFilter` regex and produces an explicit array
- `/api/*` — backend reservation
- `/static-pages/*` — CDN-internal namespace (Phase 2)

Write attempts to any reserved path must be rejected with structured error: `{code: "PATH_RESERVED", message, suggestion: "Choose a path outside these prefixes: [...]"}`.

### Audit log row shape — minimum fields
- `id` (PK), `slug`, `sha_before` (nullable on first publish), `sha_after`, `op` (publish|rollback|unpublish), `actor_claim` (human|agent|unknown), `verified` (boolean, default false), `oauth_subject`, `oauth_client_id`, `manifest_snapshot` (JSONB), `created_at` (TIMESTAMPTZ)

### Shadow-DOM mount research scope
Researcher must answer:
1. Does `react-shadow` (or equivalent) support React 18 + react-router-dom 6 cleanly?
2. How are global CSS resets/fonts handled inside the shadow root? (Re-import? `@import` inside shadow stylesheet? Adopted stylesheets?)
3. Does `bigscreen10`'s CSS use any selectors that escape shadow scope (e.g. `:root` vars set on light DOM)?
4. Image / asset path resolution inside shadow root under nested URLs — does `<base href>` work, or does the CLI need to rewrite paths?

</specifics>

<deferred>
## Deferred Ideas

- **`SiteEditor` Arda role** — Use existing `Admin` policy in v1 (AUTH-V2-01).
- **Per-actor OAuth clients** for cryptographic actor verification — server-trusted `actor=agent` distinction. Deferred from D-07.
- **Signed actor assertion** as alternative to per-client distinction — explored later.
- **DB table for reserved paths** (vs code config) — if marketing needs to add reserved paths without a deploy. Deferred unless need emerges.
- **Image optimization at publish** — CDN-V2-02.
- **Draft URLs on prod CDN** — PAGE-V2-01.
- **Subsuming Builder.io routing into the registry** — SPA-V2-01.
- **Webhooks on publish events** — out-of-scope per REQUIREMENTS.md.
- **Arda V2 with public-client support** — would let bsweb-cli do pure PKCE-loopback per RFC 8252, eliminating the dev-website OAuth backend code (D-13). Track for v2 / Arda team roadmap.
- **Persistent PKCE verifier storage** (signed cookie or `auth_flow_state` Postgres table) — would let dev-website OAuth backend (D-15) run multi-instance. Defer until multi-instance becomes a need.
- **Prod `bsweb-cli` Arda client** with `https://www.bigscreenvr.com/oauth/cli-callback` redirect URI — registered separately when prod cutover ships (Phase 4).

</deferred>

---

*Phase: 1-Foundations, Registry & OAuth*
*Context gathered: 2026-05-21*
