Skip to content

Scope definition — one site, materially operational (garvanbay)

Status: v2 — revised 2026-07-04 after a two-lane adversarial pressure-test (deep-reasoner + Codex, independent, convergent). v1 was the pre-review draft. Date: 2026-07-04 Author: Cathal Dempsey (with Claude) Target site: garvanbay.ie (the single seeded site)


PROGRESS — 2026-07-04 (Phase 0 spike) - Gate 1 (auth) ✅ PASSED — non-dev login to the deployed admin via Cloudflare Access. See spike-0-auth-cloudflare-access.md. - Gate 2 (text edit → publish → live, no corruption) ✅ PASSED — heading edit published live, images survived. Spike found a real defect: the emdash editor injects a generic Portable Text block node that the dispatcher didn't render (loud-fail banner). Fixed by adding Prose.astro + a "block" renderer (commit 25112bc). Also surfaced: publish needs Admin role — set defaultRole: 50 + syncRoles (Editor/30 can draft but not publish). - Gate 3 (image round-trip via admin) ⏳ NEXT — the scalar-vs-object image-field test (ADR-0005 §6 / R-1) is not yet run.

Net: the risky write-path loop is proven end-to-end for text. Remaining Phase-0 unknown is image editing (Gate 3).

0. Why this document exists

Four slices shipped on 2026-05-12 (image migration, Path A adapter enforcement, D1 reseed, theme tokens). The deployed CMS renders the garvanbay home page. But where we got to was not materially operational — it is a single-page, single-site render demo with no working content-editing loop, and (as the review surfaced) no working login to the admin at all.

This document defines what "materially operational" means for one site, and — post-review — reorders the work so the risky write-path loop is spiked before any read-path polish.

What the review changed (v1 → v2)

The v1 draft correctly identified the write-path as the crux but mis-allocated the risk. Both reviewers, reading the actual code and the emdash package, converged on:

  • emdash is a capable CMS — native draft→publish, revisions, _rev optimistic concurrency, a media picker, pluggable auth. The worry "can emdash even edit these blocks" was misplaced.
  • The real blockers are FCR-specific: (1) nobody can log into the deployed admin (auth is unconfigured and the dev bypass is DEV-only), and (2) image fields are stored as objects but declared as scalar text inputs, so saving any image-bearing block can corrupt the image — threatening text editing, not just image editing.
  • "Publish" is not undefined — emdash defines it (draft → publish → live D1). The scope over-rated that as an unknown.
  • Spike the loop first. Routing and the image-deficit are read-path polish worth zero if the loop can't run.

1. The bar — definition of "materially operational"

A person who is not Cathal can operate the site end to end, with no SQL, no scripts, and no out-of-band steps. That person can:

  1. Log in to the admin with their own identity.
  2. View every page, rendered correctly, themed, all images present.
  3. Edit any page's text, headings, and images through the emdash admin UI — without corrupting the block.
  4. Publish those edits so they appear live.
  5. Submit the contact form and have the enquiry actually delivered.
  6. Do all of the above without a developer in the loop.

If any of those requires Cathal to run a script or touch D1/SQL, the site is not materially operational.


2. Current state (grounded in the code, 2026-07-04)

What works

  • Deployed Worker replatform-emdash-demo.cathaldempsey.workers.dev (version 7b0bf8e8). Home / renders correctly — canonical fulldev blocks via Path A adapters, garvanbay theme, R2 images.
  • Render architecture is sound and low-risk: FcrBlocks.astro dispatcher → thin adapters (apps/cms/src/components/fcr-blocks/) → canonical blocks, with loud-fail at three layers and a check-renderers.mjs prebuild guard. Both reviewers flagged this as the solid part.
  • D1 (replatform-emdash-demo) holds ~10 garvanbay pages + globals.
  • emdash provides a native draft→publish lifecycle, revisions, and a media picker (details in WS-3/WS-4).

What does NOT work (the operational gaps)

  1. No admin login on the deployed Worker — the whole loop's step 0. astro.config.mjs passes no auth config → emdash defaults to passkey (node_modules/emdash/src/auth/mode.ts). The dev-bypass door returns 403 when not DEV (.../routes/api/auth/dev-bypass.ts) — which is why local editing appears to work and the deployed admin has never been exercised. Self-signup is doubly gated (needs an email provider — returns 503, none configured — and an enabled allowed_domains row — none). No script provisions an operator on remote D1. Result: /_emdash/admin shows a login nobody, including Cathal, can pass.

  2. Image fields corrupt on save (data↔schema mismatch). Images are stored as objects {src, originalName, alt} (ADR-0005 §6; apps/cms/seed/seed.json:69-71) but declared in the FCR schema as scalar text_input (apps/cms/src/plugins/marketing-blocks/index.tsimageUrl at :79,125,155,178,232,373; logoUrl at :392). The editor binds a text widget to an object value; saving the block writes the field back as a string, flattening the object. Because text and image share a block, editing a heading can blank the image. emdash's native media_picker exists but also writes a URL string, not the object (@emdash-cms/admin/dist/index.js:3515-3536) — so "just use media_picker" does not resolve the mismatch.

  3. Routing — 8 of ~10 pages 404, and existing routes bypass Path A. No [slug].astro. Pages are hand-authored files with hardcoded entry keys: indexhome (Path A), contactcontact (old MarketingBlocks, not FcrBlockscontact.astro:5,63, plus a hardcoded demo form), pricingpricing (dead template — no pricing entry, renders Acme/demo content). These slugs have D1 data but no route (404): about, payroll, management-accounting, company-secretarial, tax-and-advisory, accountspreparation, accountants-waterford, book-an-appointment.

  4. Contact form drops enquiries. The /contact POST handler just logs with a TODO for real email/webhook (contact.astro:16-20,39-48). No delivery, no storage, no spam protection.

  5. Publish path exists but is unproven on deploy. emdash Save writes a draft revision (pages supports drafts/revisions, seed.json:17-20); the live loader serves published-only (emdash/src/loader.ts:491); Publish promotes the draft to live D1 (emdash/src/database/repositories/content.ts:916-1000). No redeploy, no reseed — but this has not been demonstrated on the deployed Worker for the FCR block shapes.

  6. Image deficit — 4 images missing vs live (footer partner logo = a one-line fromFooter field-mapping bug; 3 welcome-section images). Tracked in known-issues.

  7. Silent "needs a developer" leaks. Theme changes require a redeploy (global.css is build-time from theme.json). Globals (header/footer/topbar) declare no drafts/revisions in seed, so their edit/publish semantics differ from pages — unverified.

  8. Per-edit uploads would fork the ADR-0005 image pipeline. emdash's generic upload stores ULID keys with no original-name metadata (emdash/src/astro/routes/api/media.ts:137-147; @emdash-cms/cloudflare/src/storage/r2.ts:41-61) — not the ADR-0005 <domain>/<stem>-<8hex> + provenance contract.


3. Target operational loop

  LOGIN         VIEW          EDIT                    PUBLISH        LIVE
  ─────         ────          ────                    ───────        ────
  Operator   →  Any page   →  Edit text / heading  →  Click       → Change
  logs into     renders,      / image in            publish       live on
  /_emdash/     all pages,    /_emdash/admin         (draft →      deployed
  admin         all images    (no corruption)        live D1)      Worker
                                                                    + contact
                                                                    form
                                                                    delivers

No terminal. No SQL. No reseed script. No developer.


4. Workstreams

Phase 0 (SPIKE) must pass before Phase 1 build starts. Phase 0 proves the write-path loop end-to-end on the deployed Worker; Phase 1 is the (lower-risk) build-out the spike de-risks.

PHASE 0 — Prove the loop (spike, ~1–2 days, 3 gates)

WS-0 — Auth: an operator can log in

  • Decided: Cloudflare Access. Code side is wired (astro.config.mjs emdash({ auth: access(...) }), inert until CF_ACCESS_TEAM_DOMAIN is set). Remaining work is Cloudflare dashboard config — full click-through runbook at spike-0-auth-cloudflare-access.md.
  • Key finding (de-risks the "no provisioning script" gap): emdash auto-provisions the first Access login as Admin (role 50); later users get their mapped/default role. No SQL user creation needed.
  • Prerequisite: the Worker needs a custom domain — Access can't front a *.workers.dev URL. (Also the R2 custom-domain we deferred in ADR-0005 — worth doing together.)
  • Access is scoped to the /_emdash path; the public site stays open (emdash never gates public routes). Access mode generalises to N sites.
  • Gate 1 — done when: a non-Cathal identity logs into the deployed /_emdash/admin via Access and reaches the editor, with the public site unaffected.

WS-3 — Text edit → publish, proven on deploy

  • In the deployed admin, edit a hero heading on an image-bearing block. Save. Confirm the change is not yet live (draft). Publish. Confirm / updates from remote D1. Inspect the D1 row JSON.
  • Gate 2 — done when: the heading edit publishes live and the block's image is not blanked.

WS-4(spike) — One image field round-trips without corruption

  • Wire one image field to a fix (see §5 decision b) and prove an operator can set/upload an image through the admin. Record the exact value written to content[].imageUrl.
  • Gate 3 — done when: an image set via the admin persists in a usable shape and renders live, with provenance preserved (or a consciously-accepted divergence documented).

If gate 2 or 3 fails, the image-shape rework is the real milestone — not routing.

PHASE 1 — Build out (only after Phase 0 passes)

WS-1 — Dynamic routing ([slug].astro)

  • One dynamic route rendering every D1 slug through FcrBlocks, resolving entries without hardcoding per-site keys. Remove the dead pricing.astro.
  • Done when: all ~10 garvanbay pages return 200 and render via Path A.

WS-2 — Content completeness (image deficit)

  • Fix the fromFooter creditpartnerLogos bug. Resolve the 3 welcome-section images (Option A hand-patch vs Option B fresh crawl — still open, §5).
  • Sequencing note: do this after WS-4's image shape is fixed — otherwise deficit images get patched into the interim object shape and migrated twice.
  • Done when: deployed pages carry every live image (or a documented exception list).

WS-4(full) — Image editing + upload UX

  • Generalise the WS-4 spike to every image field. If uploads go through emdash, reconcile with the ADR-0005 key/provenance contract (custom upload adapter/hook) or amend ADR-0005.
  • Done when: an operator can change/upload any image and see it live, provenance handled.

WS-5 — Contact form: real submission (IN SCOPE — decided)

  • Move /contact onto the FcrBlocks dispatcher (retire the hand-rolled layout + demo form). Wire a real submission path: destination (form-to-email provider, D1 store, or third-party endpoint) + spam protection + delivery confirmation.
  • Done when: a submitted enquiry is actually delivered, and /contact renders/edits through the same Path A contract as every other page.

WS-6 — Publish semantics + operator runbook

  • Confirm and document: Save = draft, Publish = live D1 write, no redeploy/reseed. Cover both pages and globals (globals lack draft/revision support — verify they're editable-and-publishable).
  • Retire reseed-d1-from-json.mjs from the operator path (dev/bootstrap only). Add a short operator runbook (log in, edit, preview, publish, recover).
  • Done when: publish is a button, and a non-dev can follow the runbook.

WS-7 — Acceptance verification

  • A non-Cathal person runs the full loop touching nothing but a browser: log in → view all pages → edit text → edit an image → preview → publish → confirm live → submit the contact form.
  • Done when: that run succeeds with zero terminal/SQL steps.

5. Open decisions

  1. Image deficit — Option A (hand-patch, ~0.5d) vs Option B (fresh garvanbay crawl, 1–2d). Still open. Blocks WS-2. Do it after the image shape is fixed (WS-4) to avoid double migration.
  2. ~~Contact form~~ DECIDED: in scope — wire real submission (WS-5).
  3. Image-field fix — DECIDED (target + fallback):
  4. Target — Option 1: custom emdash field widget that reads/writes the {src, originalName, alt} object, preserving ADR-0005 provenance-in-data. More build (widget + ADR-0005-compliant upload).
  5. Fallback — split-scalar variant: if the widget proves heavy, keep native scalar fields but split provenance into its own scalar field (imageUrl + imageOriginalName) — native-emdash-compatible and keeps provenance in the data.
  6. Rejected — plain scalar (drop provenance): contradicts ADR-0005 §6 and the standing provenance-in-data preference.
  7. The WS-4 spike (gate 3) determines widget cost → picks target vs fallback.
  8. Copied-vs-shared packages/components-v3/ vs fulldev upstream — still unverified (known-issues). Matters if canonical edits are needed.

6. Non-goals for this milestone (one site, not scale)

  • 50-site / 1800-site onboarding.
  • R2 upload throughput architecture (deferred per ADR-0005).
  • Multi-tenant runtime theme — build-time global.css stays. (Explicit limitation: brand-colour/font changes need a developer + redeploy. Acceptable; stated so the bar is honest.)
  • Per-host routing / many sites on one Worker.
  • Booking, matcher-layer loud-fail, R2 GC, custom CDN domain, verify-design automation.

Bank-now caveat (don't let one-site choices cement single-tenant): pick an auth mode (WS-0) and slug resolver (WS-1) that generalise to N sites; carry domain/tenant context in any new editor/image field design.


7. Definition of Done (acceptance test)

On the deployed Worker, by someone using only a browser:

  • A fresh non-Cathal identity can log in to /_emdash/admin.
  • Every garvanbay page slug renders at its URL (200, Path A, themed).
  • No image is missing vs live (or exceptions documented).
  • A text/heading edit publishes live — and does not blank the block's image.
  • An image can be changed/uploaded through the admin and appears live, provenance preserved.
  • A draft can be previewed before publish (emdash preview-url.ts).
  • The contact form delivers a real enquiry.
  • The whole loop ran with zero terminal/SQL/reseed steps (reseed permitted only to reset the environment before the test).
  • An operator runbook exists (login, edit, preview, publish, recover; covers pages and globals).

8. Risks — re-rated after the review

ID Risk Rating (v2) Evidence
R-0 Deployed admin login is impossible (auth unconfigured, dev-bypass DEV-only, no provisioning) SEV-1 — new, highest astro.config.mjs (no auth); auth/mode.ts; dev-bypass.ts (403 !DEV)
R-1 Image-bearing blocks corrupt on save (object data vs scalar schema; media_picker writes strings) SEV-1 seed.json:69-71 vs marketing-blocks/index.ts:79…392; admin/dist/index.js:3515-3536
R-2 Scalar-only schema blocks image editing Down-rated — solvable via custom widget/split-scalar; not an upstream-emdash blocker (but media_picker alone is insufficient) plugin fieldWidgets extension point; media_picker string output
R-3 "Publish" undefined Down-rated as a code unknown — emdash defines it; still needs deployed verification loader.ts:491; content.ts:916-1000; [id]/publish.ts
R-4 Contact form / /contact bypasses Path A Up-rated to in-scope work (WS-5) contact.astro:5,16-20,63
R-5 10 pages vs dynamic route Low — WS-1 is small no [slug].astro; hardcoded keys
R-6 Per-edit upload forks ADR-0005 key/provenance SEV-2 media.ts:137-147; r2.ts:41-61
R-7 Single-tenant leaks (theme, PUBLIC_MEDIA_BASE) SEV-3 — fine for one site, bank generalisable choices wrangler.jsonc:7-9; astro.config.mjs:110-112

9. Review provenance

Pressure-tested 2026-07-04 by two independent lanes (deep-reasoner / Opus-max, and Codex) against the code and the emdash package. They converged on the verdict and the two SEV-1 blockers; Codex added the media_picker-writes-strings and ADR-0005-upload-fork nuances, deep-reasoner surfaced the auth lockout mechanics. No material contradiction, so no Fable adjudication was needed. A first Fable run hung and was abandoned; the lane was later confirmed healthy.

Verdict carried into v2: not buildable as v1 was ordered. Phase 0 spike (WS-0/WS-3/WS-4) gates the milestone; if gates 2–3 fail, the image-shape rework is the milestone.