feat(site-editor): v8 is per site — no org flag; on when the site runs the v8 runtime

Draft
TL
4d ago
+10376-9208

Stacked on #7876 (split/50-site-id-copy). Part of the v8 stack in #7796.

PO decisions

"we should not gate v8 like this [the site_editor_content_protocol org flag]. v8 is per site and enabled as soon as the website is running the v8 runtime. v8 runtime is detected by checking v8 markers on the schema."

"make sure v8 detection is homogeneous across the codebase. Always detect v8 by reading schema's version content, right?"

2026-10-12, the decision model this PR implements:

"The site-editor ui always waits for a schema and content. Having those, the ui can read the schema and decide if either v8 or not. When not using sandbox nor localhost, the schema should be fetched from main. If v8, content comes from main + cdn, otherwise content comes from branch. If using sandbox or localhost, content comes from the server's surrounding context, like either deco serve or the go sandbox."

2026-10-13, two routes this PR (and the stack under it) used to add are gone:

"/api/:org/blocks-major/:projectId should not exist. Why don't we just read the schema on the front? We need the schema anyways on the front for making the json schema form."

"/api/:org/sandbox/:projectId/:branch/assets/:name should not exist. Uploading directly to the fs should be done only on /site-editor play mode. Hosted mode (with a project etc) should use our s3 service, and sandbox has nothing to do with it."

See Routes below for the full list after this change.

Also decided on 2026-10-12:

  • Staging and prod share the site-token signing key and the R2 delivery bucket on purpose. No split.
  • No kill switch. A site leaves v8 when its main schema goes back to v7.

Every v8 decision in Studio, on the server and in the web app, comes from one predicate on the schema's content: isV8Schema in @decocms/shared/blocks-major (a top-level "blocksMajor": 8 in .deco/schema.gen.json). The org flag no longer exists.

Decision model

The editor shows a loading state until it has both a schema and the content for its mode, then reads the schema. It never renders the v7 path first and switches later.

ModeWhenSchema read fromv8 (isV8Schema)not v8
Localhosta deco serve connection is chosen (or the account-less /site-editor)the deco servecontent protocol on the deco servethe guided "not v8" state (no schema yet: the schemaless guide)
Sandboxthe session has a sandbox (a coding session)the Go daemon's working treecontent protocol on the daemonthe existing v7 sandbox path (an older daemon without the protocol reads as v7)
GitHubotherwisemain: schema.get on the project's existing /decofile/:id/:branch/rpc, decided on the front with isV8Schema (the server answers a v7 main schema: null from its cached per-head/blob answer)main + CDN draft (the hosted CMS) through Studio's GitHub backendcontent from the branch: the v7 path, exactly as on main
  • selectContentBackend (content-backend.ts) is the pure table above. Every answer that isn't known yet is pending.
  • A schema that can't be read is v7 unless the site was last found v8. The PO's rule for this PR is "make sure current v7 flows continue working just fine", so a site whose schema can't be confirmed v8 behaves exactly like main:
    • GitHub mode: when main's schema.get fails (GitHub down, revoked token), main's answer is error (the editor waits with "Trying again" and keeps retrying) only on a project whose metadata.blocksMajor marker says the server last found it v8. Any other project reads v7 and gets the v7 editor, preview and Local tunnel at once, as on main. The read keeps polling, so a later v8 answer still takes over.
    • Sandbox mode: a daemon that fails (anything but 404) before it ever answered waits only on a marked project; any other project gets the v7 sandbox editor, as on main. A later v8 answer from the daemon still takes over.
    • Server: the decofile GET/publish treat a failed read of main as v7 unless the editor said it acts on v8 (x-deco-main-schema: v8) or, with no header, the project is marked v8.
    • A failed read's answer holds while it is re-polled (readFailed), so the editor never flips between its fallback and the loader every 5 s.
  • useMainSchemaV8 (hooks/use-main-schema-v8.ts) is how the web app knows main's answer: one schema.get per project per session on POST /api/:org/decofile/:id/main/rpc (the endpoint the v8 form reads its schema from anyway; describe/schema.get always read main, whatever the branch), then isV8Schema on the front. Mounted when a project opens and shared by every surface. No route of its own. The rpc route answers any site:
    • v7 main, asked without x-deco-main-schema: v8 (the editor's open): a read-only storage with no schema (schema: null), from projectMainIsV8 alone (a fresh ETag'd head read, a 304 on an unchanged main, and the per-commit answer; no other GitHub call). Fresh like the removed route, so a push to main and the marker are picked up at once on open; gated calls from an open editor keep reusing the last minute's answer. An editor that says it acts on v8 still gets 409 main_schema_changed.
    • Gates only a v8 main reaches settle it as v8 without a schema: 409 site_not_owned, 503 hosted_delivery_unconfigured (newly coded).
    • 404 no_repository (newly coded) and 403 feature_not_in_plan settle it as v7. Anything else (an unreadable main, 5xx) is an error that is retried, never a v7 answer.
  • The marker is a hint, never a v8 answer. metadata.blocksMajor is the server's cached answer for main. The client uses it for one thing only: whether an unreadable schema waits (last found v8) or is v7. Nothing becomes v8 because of it; a test asserts this for every mode. The Localhost item, Versions tab, Site tokens, the v7 Local tunnel and the hosted draft session all read useMainSchemaV8.
  • Localhost item: in the draft list for projects whose main schema is v8 (known after the wait). v7 keeps main's Advanced → Local. A stored Localhost choice waits for main's answer (undecided) instead of picking another backend first. Once connected, the deco serve's own schema decides: a non-v8 deco serve on a v8 project shows the existing "not v8" guide.
  • Hosted draft session (nothing boots; the published site is the preview) only in GitHub mode on a v8 main. A sandbox session on a v8 site boots as usual.
  • Server gates compute, never trust the marker. projectMainIsV8 (hosted/blocks-major.ts:142) runs isV8Schema on main for the decofile GET/publish/rpc (decofile.ts:293), the hosted routes middleware (hosted.ts:111) and the sandbox-less git routes (draft-publish-status.ts:93). A missing marker (repo seeded after create) or a stale one (main back on v7) gets main's real answer, written through. A gate reuses its last minute's answer only while it agrees with the marker, so an open on any pod (which writes the marker) is picked up at once.

Cost on v7 (the only added cost is main's schema read, made cheap):

  • The editor: one schema.get on main's rpc per project per session. On the server that is projectMainIsV8, read fresh: an ETag'd head read (a 304 on an unchanged main) and a per-commit answer; the schema is downloaded only when its blob changes, and a v7 answer carries no schema body. No probe and no other protocol call on a v7 main.
  • Server gates on a v7 site: at most one head read per repository per minute (the rest reuse the answer). The route tests assert one schema read across GET/PATCH/status/publish/rpc.
  • A sandbox session also asks its daemon for the working tree's schema (the PO's rule: the sandbox decides). An older daemon answers 404, read as v7 at once.

What changed

One detector, packages/shared/src/blocks-major.ts (isomorphic):

  • isV8Schema(schema): a plain object (not an array) whose blocksMajor === 8. Anything else is v7: a missing field, "8", 9, null, a nested field, unparseable text, or no file.

  • isV8SchemaText(text) and schemaGenPath(packagePath). The file is always <packagePath>/.deco/schema.gen.json, which is what deco schema writes and what deco content hashes. meta.gen.json no longer counts.

  • isBlocksV8Project(metadata) and BLOCKS_MAJOR_METADATA_KEY, moved here from thread/session-runtime. They read the server's cached answer (see below); the web app never calls them to decide.

  • A guard test (blocks-major.test.ts) scans every .ts/.tsx/.js/.mjs/.cjs file under apps/ and packages/, with comments stripped. It fails on:

    • any use of the blocksMajor token in source outside this module. Only two places are allowed: the VmcpRuntimeMetadata.blocksMajor type field, and the en/pt-BR copy telling a developer to add "blocksMajor": 8.
    • a test or fixture that does anything but write blocksMajor: <value>.
    • any other definition of the predicate or its constants.
    • a function with v8 in its name that isn't on a short allowlist of functions built on the predicate.
    • the old heuristics: a version or major compared with 8, ["@decocms/blocks"] lookups, describe.version/describe.protocol, meta.gen.json in a deciding file or on a line next to v8.
    • the old flag name anywhere.
    • new: any use of the marker (isBlocksV8Project, BLOCKS_MAJOR_METADATA_KEY) in apps/web outside use-main-schema-v8.ts. There, useMainSchemaV8 compares the server's fresh answer with the project it holds to re-read it, and useLastFoundV8 gives the sandbox decision its hint for a failed read.

    It also has its own tests showing it catches m?.["blocksMajor"], { blocksMajor: m } = s, "blocksMajor" in s and Reflect.get(s, "blocksMajor").

The marker is the server's cache of main's answer. metadata.blocksMajor stores isV8Schema on main's schema. Only code that has just run the predicate writes it, through syncBlocksMajorMarker, in every org; it is cleared when main goes back to v7. Writers: create/import (background), thread create (bounded, only when the answer decides the runtime stamp), every hosted gate (projectMainIsV8), which includes the editor's read of main's schema on open, and the admin backfill. No client-triggered refresh route.

mainIsV8 is cheap. It reads main's head (ETag-cached, so an unchanged head costs a 304) and remembers the answer per (repo, site folder, head commit) and per schema blob, both immutable keys in bounded LRUs. With recentIf (the gates), the last minute's answer per (repo, branch, folder) is reused without the head read while it equals the marker.

A v8 project that doesn't own its site gets guidance, not an error. Examples: a second project on the same repository, or an import whose site another project or org already claimed. /rpc, the decofile GET/publish and the hosted routes answer 409 {code: "site_not_owned"}, kept apart from an unconfigured bucket (503). The editor's read of main's schema reads that 409 as v8. The editor's GitHub probe reads the 409 as a guided state and does not retry it. Blocks and Content then say "This site is edited from another project", with a link to the projects list (en, pt-BR).

Removed: the flag from OrgFlagsSchema (tool-io.ts regenerated), the flag gate in decofile.ts (contentProtocolEnabled), and the flag checks in hosted.ts, draft-publish-status.ts, blocks-major.ts and admin.ts. On the web side: the useOrgFlagState gates, hostedCmsAvailable, releasesPending, useBlocksMajorRefresh, useSessionRuntime().isBlocksV8 and every client read of the marker.

Ownership still applies. Hosted writes still need the project to own its site in org_sites (ownedProjectSite). That is an identity check, not a feature gate.

Every v8 detection site: before → after

Before (origin/split/50 @ 499b49861):

file:linedetector
packages/shared/src/organization/schema.ts:286org flag site_editor_content_protocol
apps/api/src/api/routes/decofile.ts:261-280, 292, 676org flag (GET, publish, rpc)
apps/api/src/api/routes/decofile.ts:394-399, 564-568"a CDN draft exists" counts as v8
apps/api/src/api/routes/hosted.ts:82-87org flag (Releases, site tokens)
apps/api/src/hosted/draft-publish-status.ts:77-82, 95-111org flag, and "a draft exists" counts as v8
apps/api/src/hosted/blocks-major.ts:92-95org flag plus owned site, before the marker is written
apps/api/src/api/routes/admin.ts:1063-1073org flag, before the backfill writes the marker
apps/api/src/hosted/scope.ts:51 mainIsV8its own copy: schema.gen.json, via schemaHashOfText !== null
apps/api/src/hosted/release-objects.ts:56-73 schemaHashOfTextits own copy, checks blocksMajor !== 8 inline
apps/api/src/decofile/repo-content-storage.ts:50 isV8SchemaTextits own copy, falls back to meta.gen.json
apps/web/.../content-backend.ts:134 isV8Schemaits own copy, which accepts arrays
apps/web/.../use-content-backend.ts:146org flag
apps/web/.../use-content-backend.ts:153route presence: /site-editor counts as v8
apps/web/.../content-backend.ts:192connection presence: a deco serve counts as v8 whatever its schema
apps/web/.../content-backend.ts:82 hostedCmsAvailableorg flag (Versions, Site tokens)
apps/web/.../use-project-native-view-presence.ts:42org flag (Versions tab)
apps/web/src/views/virtual-mcp/index.tsx:888org flag (Site tokens)
apps/web/.../content-version-badge.tsx:24any protocol backend shows "v8"

After (this PR @ b94f0afe1). There is one predicate. Every row below applies it:

file:linewhat it does
packages/shared/src/blocks-major.tsisV8Schema / isV8SchemaText, the only detector; isBlocksV8Project reads the server's cached answer
apps/api/src/hosted/scope.ts:110 mainIsV8the predicate on main's schema.gen.json (per-commit, per-blob and recentIf caches)
apps/api/src/hosted/release-objects.ts:53the predicate at a commit, before hashing
apps/api/src/decofile/repo-content-storage.ts:149the predicate on the schema the GitHub backend serves
apps/api/src/hosted/blocks-major.ts:85 readMainBlocksMajorfresh mainIsV8 + marker sync, for create and thread create (module-private)
apps/api/src/hosted/blocks-major.ts:142 projectMainIsV8the hosted gates: mainIsV8 (recentIf the marker) + write-through
apps/api/src/api/routes/decofile.ts:293, 318, 687scopeIsV8 / unownedV8 / rpc gate, all projectMainIsV8
apps/api/src/api/routes/hosted.ts:111middleware: projectMainIsV8; site-token issue re-reads fresh
apps/api/src/hosted/draft-publish-status.ts:93projectMainIsV8, after the cheap ownership/bucket checks
apps/api/src/tools/thread/create.ts, tools/virtual/create.ts, routes/admin.tsthread stamp, create/import, backfill: mainIsV8
apps/web/src/hooks/use-main-schema-v8.tsisV8Schema on main's schema (rpc schema.get), once per project per session
apps/web/.../content-backend.ts:141, 176, 229, 91contentMode, selectContentBackend, contentProbesEnabled, hostedDraftSession: the table above
apps/web/.../use-content-backend.tsthe predicate on the schema of every probed backend (GitHub, sandbox daemon, deco serve); localServeBackend gives a non-v8 deco serve the guided not-v8 state
apps/web/.../content-version-badge.tsxthe badge shows only for a schema that passed
use-deco-serve-connection.ts, use-local-preview-url.ts, use-project-native-view-presence.ts, views/virtual-mcp/index.tsx, chat-mode-row.tsx, preview.tsxmain's answer (useMainSchemaV8)
packages/shared/src/thread/session-runtime.ts:67the server's cached answer, for a new thread's default runtime only

The Go daemon only serves the schema file and makes no decision. I found no detector based on package.json, the manifest shape, describe fields or the @decocms/blocks version.

Risks

  • Hosted writes for any org's owned v8 site: drafts, revisions, latest.json, purges and site tokens. (Staging and prod sharing the key and bucket is intended, per the PO.)
  • Every v8 site gets these on the client: Fast Preview by default, Localhost, Versions, Site tokens, hosted drafts and the v8 badge. A v8 site that doesn't read the delivery CDN shows "Published" while nothing is live (plan §B-11).
  • The editor waits on main's read in GitHub mode, v7 included. It is one cached request per project per session, started when the project opens, so it has usually landed before the editor mounts (on the real v7 site below: no visible difference). If GitHub can't be read, a v7 site gets the v7 editor as on main; only a site last found v8 waits with "Trying again".
  • An unmarked v8 site whose schema can't be read (GitHub down on its very first open, or a daemon failing before it answers on a branch that turned v8) shows the v7 editor until the read succeeds, then switches. This is the price of never holding a v7 site behind an outage; any v8 site opened once is marked and waits instead.
  • A v8 site with a stale v7 Local tunnel shows the tunnel until main's answer lands (a v7 site never boots a sandbox it didn't on main, so the tunnel isn't held back).
  • The admin backfill writes the marker in every org.
  • An org that still has the flag stored keeps working. Reads are lenient and the key is inert; the admin editor lists it as a custom flag.

v7 regression check

PO: "make sure current v7 flows continue working just fine after changes". v7 = a site whose main .deco/schema.gen.json lacks "blocksMajor": 8. Baseline: origin/main 21912beaf (this stack's merge-base).

Test suites, test by test (origin/main CI vs this branch)

Suiteorigin/mainthis branchpasses on main, fails/skipped/gone here
Unit (bun test, per-file isolation)10551 pass, 0 fail, 1014 files11014 pass, 0 fail, 1062 filesnone (5 names in secret-field.test.ts renamed, see below)
Web component tests252 passed252 passednone, same list
E2E (2 shards)383 passed, 10 skipped408 passed, 10 skippednone, the same 10 voice/decopilot skips
Sandbox daemon e2e225 pass, 1 skip304 pass, 1 skipnone, the same voice skip
  • No test file deleted. 22 changed; the only v7-relevant expectation change is secret-field.test.ts (SecretField → SecretFieldForBackend), which renders the old SecretField unchanged on a legacy backend; ct/tests/secret-field.ct.tsx still covers v7 with the backend stubbed to legacy. The rest add tests only, or keep v7 behaviour (admin-project-metadata, fast-preview fixture still defaults on, github-stub real blob ids/GraphQL, playwright.config hosted env vars).
  • main CI runs: test, e2e, sandbox.

A real Blocks 7 site, nine flows, origin/main Studio vs this branch

Site: deco-sites/storefront-tanstack main (@decocms 7.77.1), its real .deco/blocks, meta.gen.json and no schema.gen.json, on a local GitHub stub that opens and merges PRs and can go unreachable on demand. A fresh project on each Studio; same two test-only shims on both (frame-ancestors, loopback draft host).

Floworigin/mainthis branchdifference
1. Open editor (forms + preview)ok; preview loads the draft linksameone extra POST /decofile/:id/main/rpc schema.get (200, schema: null) — measured with the earlier /blocks-major read, same cost
2. Edit a field on Productioncommits to main; preview 2.2 scommits to main; 1.7 snone
3. New draft + editdraft branch, edit committed there, "Review & Publish"samenone
4. Switch draftsDraft 2 shows main; Draft 1 its edit (~0.5 s)samenone
5. Publishcommit, PR, merge into main (~0.9 s)sametoast copy only (below)
6. Advanced → LocalBranches / PRs / Local; reads /.decofile and /live/_meta from the local URLsamenone
7. Fast Preview switch off/onsaves false then truesamenone visible (below)
8. Releases / Versions / Site tokensabsent; hosted routes 404same/rpc 409 instead of 404 (below)
9. GitHub unreachable 50–75 s"Blocks unavailable … Retry"; Retry recovers in 0.94 sidentical, no "Trying again"main's schema.get re-polled every 5 s (network only)

v7 regressions found and fixed in this PR

  1. Unreadable main held every project (d422522c8). Main's read failing (then POST /blocks-major, now schema.get) made v7 projects wait with "Trying again" forever and held the preview, live meta, decofile and Local tunnel; on main a v7 site never waited on GitHub. Now v7 unless marked v8 (above). Also fixed: each 5 s retry put the editor back to loading, reloading the preview. Tests: use-main-schema-v8.test.ts; e2e "a v7 site whose main can't be read still gets the v7 editor, as on main" (content-protocol-github.spec.ts).
  2. Coding session with a failing daemon (ae2af4d07, 6082b95c7). A daemon failing (not 404) before it ever answered made a v7 coding session wait forever; main showed the v7 sandbox editor. Now the v7 sandbox editor unless marked v8; the failure holds through re-polls. Tests: content-backend.test.ts (decision table rows, the unmarked sequence, "the marker never makes a project v8 on its own", readFailed).
  3. Publish without the main-answer header on an unreadable main (ae2af4d07). POST /decofile/:id/:branch/publish (and GET) on a linked v7 project answered 502 where main went on to merge. Now v7 unless the request said v8 or the project is marked v8. Tests: v8-gates.test.ts "main unreadable on a site the project owns".

Remaining v7-visible differences, each intended

  • Publish toast copy: one toast, "Published · Your changes are live on …", instead of "Published to …" plus two "Item updated successfully" (ea39a2ff6, the one-toast-per-action rule).
  • /rpc exists on a v7 project (main had no route): it answers schema.get with schema: null (read only), and 409 main_schema_changed to an editor acting on v8. It is re-polled every 5 s while GitHub is unreachable. Requests only; nothing on screen changes.
  • Coding session while the sandbox clones: this branch reads nothing until the working tree is there; main also read the live site's /live/_meta and tried the sandbox's decofile read, which 502s until the clone lands and is only re-driven at that moment (sandbox-events-context.tsx). Both show loading until then, so the editor is usable at the same point.

Routes (whole stack, after 2026-10-13)

New

  • POST /api/:org/decofile/:projectId/:branch/rpc: the content protocol over main + the CDN draft (v8). Answers main's schema for any site: a v7 main gets a read-only schema: null (no other read). This is how the editor reads main's schema.
  • GET /api/:org/hosted/:projectId/releases, POST /api/:org/hosted/:projectId/releases/current: Versions (v8 hosted).
  • GET|POST /api/:org/hosted/:projectId/site-tokens: site tokens (v8 hosted).
  • POST /api/:org/sandbox/:projectId/:branch/rpc: proxy to the daemon's content protocol (sandbox sessions).
  • POST /api/_admin/hosted/site-claims/backfill: admin backfill (site links + v8 marker).
  • Daemon (#7877): /_sandbox/rpc, one route.

Changed

  • GET /api/:org/decofile/:projectId/:branch and POST …/publish: 409 main_schema_changed when the editor's x-deco-main-schema answer is out of date, 409 site_not_owned for an unowned v8 site, and a v8 project's ?__draft= pointer instead of the decofile.
  • Every /api/:org/decofile/… route: the "no repository" 404 now carries code: "no_repository"; rpc's 503 carries code: "hosted_delivery_unconfigured".
  • GET /api/:org/sandbox/:projectId/:branch/git/status and POST …/git/diff (sandbox-less): a v8 project's hosted draft status and diff.

Removed (were in this stack, never on main)

  • POST /api/:org/blocks-major/:projectId: the front reads main's schema through rpc schema.get and decides with isV8Schema. Server gates keep deciding internally (projectMainIsV8, cached); the marker stays a server cache, refreshed by create, thread create, the backfill and every gated read.
  • PUT /api/:org/sandbox/:projectId/:branch/assets/:name (#7825) and the daemon's PUT /_sandbox/assets/{name...} (#7877).

Asset uploads, by mode (no new route; the existing S3 service)

ModeUploads go to
v8 hosted (GitHub) and v8 sandboxStudio's S3 asset service: POST /api/:org/file-configs/:id/upload (on main), the managed deco-assets-<site> config, assets.decocms.com/<site>/…
v7, any modesame, unchanged
Localhost and /site-editor play modethe connected deco serve: PUT <deco serve>/assets/<name> into its working tree, unchanged

The field decides with a pure serveAssetUploadOrigin (only a connected deco serve that takes uploads; a sandbox or hosted project never uploads to a filesystem, whatever its describe says). So the S3 path works for sites created in Studio too, project create now gives a linked site its managed config (the helper the deco.cx import already used, moved to file-storage/managed-site-config.ts); before, only imported sites had one and others opened the bucket picker.

Test evidence

  • packages/shared/src/blocks-major.test.ts: the predicate on every shape; the guard (one detector, no other blocksMajor reader, known v8 functions only, no old heuristics, no flag) and, new, no web use of the marker outside useMainSchemaV8.
  • apps/web/.../content-backend.test.ts (rewritten): the decision table, each mode × v8/v7 × loading/error (GitHub on main's answer, Sandbox on the daemon's, Localhost on the deco serve's, a stored Localhost choice waiting for main); Sandbox mode never reads main; an unreadable schema is v7 unless the project is marked v8, and the marker never makes a project v8; no v7 flash: every order in which reads land goes from pending to one final editor, never from one editor to the other; probes (the GitHub backend only for a v8 main, the daemon in Sandbox mode); hostedDraftSession only in GitHub mode on a v8 main.
  • apps/web/src/hooks/use-main-schema-v8.test.ts (new): the server's answer; no repository is v7; an unreadable main (502) waits (error) only on a project marked v8 and is v7 on any other; loading until it lands; the answer survives a failed refetch and holds through re-polls.
  • use-deco-serve-connection.test.tsx, use-local-preview-url.test.tsx: driven by main's answer, never the marker; a stored Localhost choice is undecided while main loads; the v7 tunnel stays in use while main loads or errors (no sandbox boot a v7 site didn't have on main).
  • use-content-backend.test.ts: the GitHub/sandbox/deco serve probes on the new inputs.
  • apps/api/src/api/routes/v8-gates.test.ts (real routes, in-memory storage, a recording fake repo):
    • Main's schema over rpc (new, replaces the /blocks-major tests): a v7 site answers schema: null, read only, from one cached read (a repeat open reads only the head); never takes a write; a v8 main writes the marker and answers 503 hosted_delivery_unconfigured without a bucket; unowned v8 → 409 site_not_owned; a stale marker is cleared; no repository → 404 no_repository with no read; an unreadable main is an error, never v7, and writes nothing.
    • Hosted middleware: a v7 main is 404 with one schema read for all four routes and no org_sites read; new an unmarked v8 project is served and marked; a stale v8 marker over a v7 main is 404 and cleared; new the last minute's answer is reused while the marker agrees and re-read once another pod's open writes a different one; site-token issue re-reads main fresh.
    • Decofile: a v7 project reads main's schema once across GET/PATCH/status/publish/rpc and writes nothing (rpc with the v8 header → 409 main_schema_changed); new an unmarked v8 project has the protocol; unowned v8 gets site_not_owned.
  • draft-publish-status.test.ts: main decides, not the marker (an unmarked v8 main gets its draft and the marker); v7: null; a repeat v7 read within the minute makes no repository call; unowned: no GitHub read.
  • e2e content-protocol-github.spec.ts: the v7 editor test asserts the page read main's schema (schema.get on main's rpc, 200) and made no other protocol call; the unreadable-main test fails that read; refreshV8 (what opening does) is now schema.get + isV8Schema. content-protocol-deco-serve.spec.ts opens its project the same way.
  • use-main-schema-v8.test.ts: decides v8 from the schema it fetched (isV8Schema, v7 null, blocksMajor: 9), the gates that settle it (no_repository, feature_not_in_plan → v7; site_not_owned, hosted_delivery_unconfigured → v8), errors that are retried, and the request (main's rpc, schema.get, no main-schema header, uncached).
  • content-backend.test.ts serveAssetUploadOrigin: Localhost and /site-editor (a connected deco serve) upload to it; hosted and sandbox projects go to Studio's file storage, even when a describe advertises an upload folder.
  • managed-site-config.test.ts: the managed (S3) config is created for a linked site, left alone when it exists, a legacy one upgraded.
  • Local runs @ b94f0afe1: tsc --noEmit clean for apps/api, apps/web, packages/shared, packages/e2e; oxlint, biome and knip clean; bun test passes for packages/shared blocks-major (16), apps/api hosted + decofile + v8-gates + sandbox-proxy (149 + 18), apps/web sections-editor/preview/main-panel-tabs/pills/hooks (1676; the one use-local-app-reachable timing failure passes alone and is unchanged); the four component specs that read fields (51) pass.
  • 2026-10-13 (routes dropped), commits on this PR: a7cbf23f0 (front reads main's schema, /blocks-major gone), 0485e6b56 (managed S3 config at create), d5b8a6a81 (the open reads main fresh), bb3f849a5 + 81e0b6acd (e2e). Lower in the stack: d6d03a7dd (#7877, daemon upload route gone), 9e8292493 (#7825, sandbox asset proxy gone), 5bfb2537a (#7826, sandbox uploads → S3), each merged up normally.
    • Local @ 81e0b6acd: tsc --noEmit clean for apps/api, apps/web, packages/shared, packages/e2e, packages/sandbox; oxlint, biome, knip clean; bun test: v8-gates (27), apps/api/src/hosted (110), managed-site-config (7), sandbox-proxy.content (3), blocks-major guard (16), use-main-schema-v8 (10), content-backend (56), use-content-backend (21), use-deco-serve-connection (8), use-local-preview-url (6); Go vet + test for the daemon; daemon content-protocol e2e 79 pass.
    • CI (workflow_dispatch on split/51-v8-per-site @ 81e0b6acd): Checks & Unit Tests success, E2E Tests success (186 + 190 passed, 0 flaky), Sandbox success @ d5b8a6a81 (no sandbox change after it).
    • On the way: the first E2E round caught the v7 rpc tests still expecting 409 and the "no repository" body; the second showed the open read could be a minute stale after main went back to v7 (fixed in d5b8a6a81, with a unit test). A signed-in site-editor test that fails its first attempt after 2.0 min and passes on retry also flaked before this change (/site-editor shows the org rail, run 38339765440).
  • CI (workflow_dispatch on split/51-v8-per-site @ 6082b95c7):
    • Checks & Unit Tests: success
    • E2E Tests: success, including the new v7 unreadable-main test
    • Sandbox: success
    • @ ae2af4d07 the guard test failed: use-content-backend.ts read the marker directly. Fixed in 6082b95c7: it reads it through useLastFoundV8 in use-main-schema-v8.ts, which the guard lists as a hint, never a v8 answer.
    • Earlier @ d422522c8: test, e2e, both success.
    • @ 711abfb17 the component tests failed: the fields' deco serve connection now reads useMainSchemaV8, which the CT harness has no provider for. Fixed in b94f0afe1 with a CT stub (main reads as v7, as the content backend already reads as legacy there).

Stack

v8 Fast Preview track: previous: #7876 · next: none (top of the stack). Base: split/50-site-id-copy. Part of the v8 stack in #7796. Same tree as feat/blocks-v8-hosted (its latest merge of split/51 @ 81e0b6acd; git diff empty).

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig