feat(site-editor): v8 is per site — no org flag; on when the site runs the v8 runtime
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.
| Mode | When | Schema read from | v8 (isV8Schema) | not v8 |
|---|---|---|---|---|
| Localhost | a deco serve connection is chosen (or the account-less /site-editor) | the deco serve | content protocol on the deco serve | the guided "not v8" state (no schema yet: the schemaless guide) |
| Sandbox | the session has a sandbox (a coding session) | the Go daemon's working tree | content protocol on the daemon | the existing v7 sandbox path (an older daemon without the protocol reads as v7) |
| GitHub | otherwise | main: 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 backend | content 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 ispending.- 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.getfails (GitHub down, revoked token), main's answer iserror(the editor waits with "Trying again" and keeps retrying) only on a project whosemetadata.blocksMajormarker says the server last found it v8. Any other project readsv7and 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.
- GitHub mode: when main's
useMainSchemaV8(hooks/use-main-schema-v8.ts) is how the web app knows main's answer: oneschema.getper project per session onPOST /api/:org/decofile/:id/main/rpc(the endpoint the v8 form reads its schema from anyway;describe/schema.getalways read main, whatever the branch), thenisV8Schemaon 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), fromprojectMainIsV8alone (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 409main_schema_changed. - Gates only a v8 main reaches settle it as v8 without a schema: 409
site_not_owned, 503hosted_delivery_unconfigured(newly coded). - 404
no_repository(newly coded) and 403feature_not_in_plansettle it as v7. Anything else (an unreadable main, 5xx) is an error that is retried, never a v7 answer.
- v7 main, asked without
- The marker is a hint, never a v8 answer.
metadata.blocksMajoris 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 readuseMainSchemaV8. - 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, thedeco serve's own schema decides: a non-v8deco serveon 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) runsisV8Schemaon 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.geton main's rpc per project per session. On the server that isprojectMainIsV8, 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) whoseblocksMajor === 8. Anything else is v7: a missing field,"8",9,null, a nested field, unparseable text, or no file. -
isV8SchemaText(text)andschemaGenPath(packagePath). The file is always<packagePath>/.deco/schema.gen.json, which is whatdeco schemawrites and whatdeco contenthashes.meta.gen.jsonno longer counts. -
isBlocksV8Project(metadata)andBLOCKS_MAJOR_METADATA_KEY, moved here fromthread/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/.cjsfile underapps/andpackages/, with comments stripped. It fails on:- any use of the
blocksMajortoken in source outside this module. Only two places are allowed: theVmcpRuntimeMetadata.blocksMajortype 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
v8in 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.jsonin 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) inapps/weboutsideuse-main-schema-v8.ts. There,useMainSchemaV8compares the server's fresh answer with the project it holds to re-read it, anduseLastFoundV8gives 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 sandReflect.get(s, "blocksMajor"). - any use of the
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:line | detector |
|---|---|
packages/shared/src/organization/schema.ts:286 | org flag site_editor_content_protocol |
apps/api/src/api/routes/decofile.ts:261-280, 292, 676 | org 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-87 | org flag (Releases, site tokens) |
apps/api/src/hosted/draft-publish-status.ts:77-82, 95-111 | org flag, and "a draft exists" counts as v8 |
apps/api/src/hosted/blocks-major.ts:92-95 | org flag plus owned site, before the marker is written |
apps/api/src/api/routes/admin.ts:1063-1073 | org flag, before the backfill writes the marker |
apps/api/src/hosted/scope.ts:51 mainIsV8 | its own copy: schema.gen.json, via schemaHashOfText !== null |
apps/api/src/hosted/release-objects.ts:56-73 schemaHashOfText | its own copy, checks blocksMajor !== 8 inline |
apps/api/src/decofile/repo-content-storage.ts:50 isV8SchemaText | its own copy, falls back to meta.gen.json |
apps/web/.../content-backend.ts:134 isV8Schema | its own copy, which accepts arrays |
apps/web/.../use-content-backend.ts:146 | org flag |
apps/web/.../use-content-backend.ts:153 | route presence: /site-editor counts as v8 |
apps/web/.../content-backend.ts:192 | connection presence: a deco serve counts as v8 whatever its schema |
apps/web/.../content-backend.ts:82 hostedCmsAvailable | org flag (Versions, Site tokens) |
apps/web/.../use-project-native-view-presence.ts:42 | org flag (Versions tab) |
apps/web/src/views/virtual-mcp/index.tsx:888 | org flag (Site tokens) |
apps/web/.../content-version-badge.tsx:24 | any protocol backend shows "v8" |
After (this PR @ b94f0afe1). There is one predicate. Every row below applies it:
| file:line | what it does |
|---|---|
packages/shared/src/blocks-major.ts | isV8Schema / isV8SchemaText, the only detector; isBlocksV8Project reads the server's cached answer |
apps/api/src/hosted/scope.ts:110 mainIsV8 | the predicate on main's schema.gen.json (per-commit, per-blob and recentIf caches) |
apps/api/src/hosted/release-objects.ts:53 | the predicate at a commit, before hashing |
apps/api/src/decofile/repo-content-storage.ts:149 | the predicate on the schema the GitHub backend serves |
apps/api/src/hosted/blocks-major.ts:85 readMainBlocksMajor | fresh mainIsV8 + marker sync, for create and thread create (module-private) |
apps/api/src/hosted/blocks-major.ts:142 projectMainIsV8 | the hosted gates: mainIsV8 (recentIf the marker) + write-through |
apps/api/src/api/routes/decofile.ts:293, 318, 687 | scopeIsV8 / unownedV8 / rpc gate, all projectMainIsV8 |
apps/api/src/api/routes/hosted.ts:111 | middleware: projectMainIsV8; site-token issue re-reads fresh |
apps/api/src/hosted/draft-publish-status.ts:93 | projectMainIsV8, after the cheap ownership/bucket checks |
apps/api/src/tools/thread/create.ts, tools/virtual/create.ts, routes/admin.ts | thread stamp, create/import, backfill: mainIsV8 |
apps/web/src/hooks/use-main-schema-v8.ts | isV8Schema on main's schema (rpc schema.get), once per project per session |
apps/web/.../content-backend.ts:141, 176, 229, 91 | contentMode, selectContentBackend, contentProbesEnabled, hostedDraftSession: the table above |
apps/web/.../use-content-backend.ts | the 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.tsx | the 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.tsx | main's answer (useMainSchemaV8) |
packages/shared/src/thread/session-runtime.ts:67 | the 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)
| Suite | origin/main | this branch | passes on main, fails/skipped/gone here |
|---|---|---|---|
Unit (bun test, per-file isolation) | 10551 pass, 0 fail, 1014 files | 11014 pass, 0 fail, 1062 files | none (5 names in secret-field.test.ts renamed, see below) |
| Web component tests | 252 passed | 252 passed | none, same list |
| E2E (2 shards) | 383 passed, 10 skipped | 408 passed, 10 skipped | none, the same 10 voice/decopilot skips |
| Sandbox daemon e2e | 225 pass, 1 skip | 304 pass, 1 skip | none, 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 oldSecretFieldunchanged on a legacy backend;ct/tests/secret-field.ct.tsxstill covers v7 with the backend stubbed to legacy. The rest add tests only, or keep v7 behaviour (admin-project-metadata,fast-previewfixture still defaults on,github-stubreal blob ids/GraphQL,playwright.confighosted 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).
| Flow | origin/main | this branch | difference |
|---|---|---|---|
| 1. Open editor (forms + preview) | ok; preview loads the draft link | same | one 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 Production | commits to main; preview 2.2 s | commits to main; 1.7 s | none |
| 3. New draft + edit | draft branch, edit committed there, "Review & Publish" | same | none |
| 4. Switch drafts | Draft 2 shows main; Draft 1 its edit (~0.5 s) | same | none |
| 5. Publish | commit, PR, merge into main (~0.9 s) | same | toast copy only (below) |
| 6. Advanced → Local | Branches / PRs / Local; reads /.decofile and /live/_meta from the local URL | same | none |
| 7. Fast Preview switch off/on | saves false then true | same | none visible (below) |
| 8. Releases / Versions / Site tokens | absent; hosted routes 404 | same | /rpc 409 instead of 404 (below) |
| 9. GitHub unreachable 50–75 s | "Blocks unavailable … Retry"; Retry recovers in 0.94 s | identical, no "Trying again" | main's schema.get re-polled every 5 s (network only) |
v7 regressions found and fixed in this PR
- Unreadable main held every project (d422522c8). Main's read failing (then
POST /blocks-major, nowschema.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). - 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). - 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).
/rpcexists on a v7 project (main had no route): it answersschema.getwithschema: null(read only), and 409main_schema_changedto 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/_metaand 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-onlyschema: 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/:branchandPOST …/publish: 409main_schema_changedwhen the editor'sx-deco-main-schemaanswer is out of date, 409site_not_ownedfor 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 carriescode: "no_repository"; rpc's 503 carriescode: "hosted_delivery_unconfigured". GET /api/:org/sandbox/:projectId/:branch/git/statusandPOST …/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 rpcschema.getand decides withisV8Schema. 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'sPUT /_sandbox/assets/{name...}(#7877).
Asset uploads, by mode (no new route; the existing S3 service)
| Mode | Uploads go to |
|---|---|
| v8 hosted (GitHub) and v8 sandbox | Studio'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 mode | same, unchanged |
Localhost and /site-editor play mode | the 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 otherblocksMajorreader, knownv8functions only, no old heuristics, no flag) and, new, no web use of the marker outsideuseMainSchemaV8.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 thedeco 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 frompendingto one final editor, never from one editor to the other; probes (the GitHub backend only for a v8 main, the daemon in Sandbox mode);hostedDraftSessiononly 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 isundecidedwhile 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 serveprobes 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-majortests): a v7 site answersschema: 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 503hosted_delivery_unconfiguredwithout a bucket; unowned v8 → 409site_not_owned; a stale marker is cleared; no repository → 404no_repositorywith 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 getssite_not_owned.
- Main's schema over rpc (new, replaces the
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.geton main's rpc, 200) and made no other protocol call; the unreadable-main test fails that read;refreshV8(what opening does) is nowschema.get+isV8Schema.content-protocol-deco-serve.spec.tsopens its project the same way. use-main-schema-v8.test.ts: decides v8 from the schema it fetched (isV8Schema, v7null,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.tsserveAssetUploadOrigin: Localhost and/site-editor(a connecteddeco serve) upload to it; hosted and sandbox projects go to Studio's file storage, even when adescribeadvertises 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 --noEmitclean forapps/api,apps/web,packages/shared,packages/e2e;oxlint,biomeandknipclean;bun testpasses forpackages/sharedblocks-major (16),apps/apihosted + decofile + v8-gates + sandbox-proxy (149 + 18),apps/websections-editor/preview/main-panel-tabs/pills/hooks (1676; the oneuse-local-app-reachabletiming 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-majorgone), 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 --noEmitclean forapps/api,apps/web,packages/shared,packages/e2e,packages/sandbox;oxlint,biome,knipclean;bun test:v8-gates(27),apps/api/src/hosted(110),managed-site-config(7),sandbox-proxy.content(3),blocks-majorguard (16),use-main-schema-v8(10),content-backend(56),use-content-backend(21),use-deco-serve-connection(8),use-local-preview-url(6); Govet+testfor 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).
- Local @ 81e0b6acd:
- 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.tsread the marker directly. Fixed in 6082b95c7: it reads it throughuseLastFoundV8inuse-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 serveconnection now readsuseMainSchemaV8, 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