Skip to content

v8 N-16: drafts are the fast-preview pointer, with only the branch's changes over the local release - #619

Draft
tlgimenes wants to merge 27 commits into
v8-15-variant-previewfrom
v8-16-draft-overlays
Draft

tlgimenes wants to merge 27 commits into
v8-15-variant-previewfrom
v8-16-draft-overlays

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

What

Draft previews go back to the v7 "fast preview" protocol: Studio mints a pointer that the SDK fetches. The only change from v7 is the answer: the pointer returns only what the draft branch changed compared with production, not a whole decofile. cms.forDraft(pointer) layers those changes over the production snapshot the server already has in memory.

There's no /preview route, no "preparing" state, no overlay or blob uploads to object storage, no /api/_delivery draft routes and no overlay grants. Drafts don't need site/token.

Spec: deco-sites/docs-tanstack#5 (into #4's branch): content-delivery#draft-previews, hosted-drafts (#who-may-preview), api-reference (#draft-pointers, #loaders), content (#write-a-loader), releases-and-drafts.

Draft pointer contract (what the SDK enforces)

  • Pointer (in ?__draft= and the __deco_draft cookie, v7's name; ?__draft=off ends a preview): <studio host>/api/<org>/decofile/<project>/<branch>/changes?token=<draft token>@<version>. The version is the commit of the editor's last save; Studio doesn't read it, it only gives each save a new address.
  • Allowed hosts (v7's rule, 773229cc): the preview API domains v7 shipped: the .decocms.com suffix (studio.decocms.com, pr-*.pr.studio.decocms.com, local.studio.decocms.com) and localhost, 127.0.0.1, [::1], *.localhost. DECO_PREVIEW_API_DOMAINS (a comma list; a leading dot is a suffix match on a label boundary, anything else an exact host) replaces the list, exactly as in v7. Never content. A port is allowed only on loopback hosts and local.studio.decocms.com. Anything else is refused before any fetch: look-alikes (evil-decocms.com, studio.decocms.com.evil.example), metadata IPs, userinfo, schemes, // paths.
  • Request: GET https://<host><path>&v=<version> (?v= when the path has no query). The scheme comes from the domain that admitted the host: plain http only for localhost, *.localhost, 127.0.0.1 and [::1]. The pointer's __variant parameters are removed first. The request has no cookies, no authorization and no site token, and redirect: "manual" means a redirect is a failure. A response that was redirected anyway (a fetch polyfill such as React Native's or whatwg-fetch ignoring redirect: "manual"), or whose URL has another origin, is refused. The version is URL-encoded into v.
  • Accepted response: status 200 only, at most 16 MiB (refused while streaming, or up front on content-length), within 10 s. The body must be exactly { "format": 1, "set": { "<block>": <block JSON> }, "delete": ["<block>"] }: no other keys, string names, no repeats, and set and delete disjoint.
  • Layering: production is captured once per client: the client's release, the content module or the latest hosted release. A block in set replaces production's whole (draft wins per file). A block in delete is absent from lookup and list. Every other block is production's own object, and production is never mutated. list is production's names plus set's, minus delete. Forced variants apply on top. The draft's revision is the opaque <release revision>~<version>, and forRevision can't reach it.
  • Failure: a parse error, refused host, network error, timeout, non-200, oversize or bad shape makes every call on that client return LOADER_FAILED, with the cause. It never falls back to published content.
  • Caching: changes are kept per pointer without its variants, so every variant shares one fetch. Each is reused for up to 60 s (so the token is checked again), with at most 3 per CMS, and all are dropped when the release changes or on a hot reload. Failures aren't reused.
  • local: a pointer whose version is local (deco serve) names no draft. Nothing is fetched, and only its forced variants apply.
  • Release checks: these are unchanged. A draft client schedules the same background check as any client, and it never runs in front of the draft.

Breaking changes (v8 prerelease)

  • Loader.load() takes no pointer. Drafts are the CMS's job and work over any loader, so custom-loader drafts are gone.
  • remoteLoader serves releases only.
  • Removed from @decocms/blocks/protocol: computeBlockHash, computeOverlayVersion, DRAFT_OVERLAY_FORMAT, DraftOverlay. Removed from /protocol/conformance: draftOverlayFixtures.
  • parseDraftPointer now also accepts a bracketed IPv6 host ([::1]:4000). (preview.sources, added earlier in this PR, is removed again in 773229cc in favour of v7's DECO_PREVIEW_API_DOMAINS.)

CMS settings block

One optional, well-known saved block, CMS (.deco/blocks/CMS.json), of the built-in type cms-settings, replaces the telemetry and analytics built-ins and the Telemetry / Analytics saved blocks outright (v8 is prerelease-only, so there are no aliases):

{ "__resolveType": "cms-settings",
  "preview":   { "hosts": ["staging.example.com"] },
  "telemetry": { "enabled": true, "metrics": true, "errorSampleRate": 0.05, "traceSampleRate": 0 },
  "analytics": { "enabled": true, "collector": "…" } }
  • Each section keeps the fields and defaults its old block had. Without the block, every default applies. Any field can have variants.
  • Code keeps destinations, secrets and caps: telemetry: { endpoint, headers, limits } and analytics' code options, as before, plus preview: { hosts }, the most content may allow (as telemetry.limits caps the rates).
  • Read path: cms.settings() is typed, with defaults filled in and caps applied. It reads the current production release already in memory: the bundled content at boot and offline, and the hosted release once the background check swaps it in. It never fetches, never reads a draft and never rejects. Telemetry, analytics (AnalyticsScript in the root layout reads (await cms.settings()).analytics) and the draft helpers all use it. The result is frozen.
  • deco schema emits cms-settings. deco check points a leftover telemetry/analytics block at CMS and warns on a CMS of another type.
  • Migration (deco-v7-to-v8-migration skill, siteSettings.ts): folds a v7 site's Site-block previewHosts into preview.hosts, trimmed and lowercased as v7 compared them. An invalid entry is reported. On TanStack Start, the <site>.deco.site / <site>.deco-cx.workers.dev hosts v7 added for DECO_SITE_NAME are added too, or reported when the name isn't found. It also folds a literal OneDollarStats collectorAddress and an already-migrated site's Telemetry.json / Analytics.json into their sections, variants included. It's idempotent and never replaces a CMS of another type. Settings that lived only in the environment are listed in the report.
  • Site editor: Studio's "Settings" entry opens this block's form, showing the defaults. Its first save creates CMS.json with blocks.apply guarded by ifMatch: { CMS: null } (feat: support Blocks v8 (content protocol) behind a flag studio#7728).

Preview hosts

  • Host patterns: exact names, *.name wildcards (whole leading label, matched label by label from the right, at least two labels after the *), "*", optional ports, and exact IPs. Matching is lowercase, ignores one trailing dot and compares punycode.
    • Look-alikes such as x.example.com.attacker.com never match.
    • A URL with a username or password, or a hostname with an empty label (a..example.com), matches only "*".
    • createCMS throws on an invalid code pattern; content drops one. A content entry counts only when it falls within code's list.
    • No cap and no block: every host may preview, as before.
  • Enforcement: cms.draftPointer(request) / cms.draftCookie(request) (they replace the free draftPointer, draftCookie and DRAFT_COOKIE) ignore a draft on a host outside the effective list, so the request gets published content, never an error. ?__draft=off works on any host. This isn't access control (the signed, expiring token in the pointer is); it keeps drafts off public domains, caches and search.
  • A draft never reaches the settings. Settings read only the release path, so a draft can't allow its own host or change telemetry.
  • Review fixes (e7a1d1d1, 963fd2b4):
    • Each createCMS call is a handle with its own block map on the shared instance, which still shares content, caps, telemetry and update checks. Before, Next's proxy.ts importing the app's cms replaced the app's block map, and PLPs answered 500 on faststore-fila. The documented recipe works again, and faststore-fila's proxyCms.ts workaround is deleted. The same block map returns the same handle, and an instance stored by an older package copy is replaced rather than adopted.
    • A custom loader's update() keeps the last release in memory. Settings no longer fall back to the defaults (every host) until the next load.
    • The settings are deep-frozen, so one caller can't change them for every request.

Restored legacy secret guard exemption (63b07191)

A v7 website/loaders/secret.ts block marks its encrypted string "format": "secret", so the v8 secret guard refused every blocks.apply that carried one once a v7 site was edited through the content protocol. The exemption that upstream ccdf6c25 had added was lost; this restores it. v8 Secret fields stay strictly validated.

deco serve has no token (9a45c8c7, 85bf7112)

There's no bearer token, --token or DECO_SERVE_TOKEN. What protects the server is unchanged: it listens on loopback by default, browser requests need an allowed Origin, and Host must be its own address. --host 0.0.0.0 prints a 127.0.0.1 site-editor link. --host is case-insensitive, and a refused origin is logged once with the --allow-origin to pass.

Update (46c0ea4b, product-owner decision): deco serve now answers every origin (CORS reflects Origin, with Vary: Origin; Chrome's private-network preflight is still answered). Removed --allow-origin, the Studio origin allowlist, the refused-origin 403 and its log line, and the Host check. With no token either, any website open in the browser can read and write the content, so the docs say to run it only while editing.

Update (c606a458, product-owner decision): deco serve shows and links localhost: every startup line and the site-editor link (#endpoint=http%3A%2F%2Flocalhost%3A<port>%2Frpc, also for --host 0.0.0.0/::), and without --host it listens on 127.0.0.1 and ::1 on one port (::1 skipped where IPv6 is unavailable), so http://localhost:<port> reaches it either way. --host still listens on that one address.

Product-owner fixes (773229cc)

  • Content protocol handler has no auth. createContentHandler/createAssetHandler lose token, authorize and the receipt scope (server/auth.ts deleted); the client loses token. Who may call the handler is the mounting server's job: deco serve on loopback, Studio's /rpc behind Studio's session. ErrorCode.Unauthorized/Forbidden stay as vocabulary for such a host.
  • Unused protocol options deleted: requestKey + idempotency receipts (describe.writes), ifSchemaMatch (+ Conflict.data.schema), the ref parameter and refs/autoCreate, and describe.limits with the read/batch limits (list, schema, batch response) and their limits options. blocks.apply keeps its own guards as internal constants (500 names, 1 MiB per entry, 8 MiB per request); ifMatch and assets.maxBytes stay; a removed parameter is now an unknown one (InvalidParams). ContentStorage: snapshot() and readSchema() take no options; StorageDescription loses refs/idempotency/limits; getReceipt, StoredReceipt, CommitAttempt.ref/receipt and applyRequestDigest/APPLY_DIGEST_DOMAIN are gone. resolvedRef and expectedSchemaVersion (the secret guard's) stay. Conformance suite updated (no limit/idempotency/ref/token cases; apply guards checked against the constants; apply/unknown-params covers the removed names).
  • Draft cookie is __deco_draft again (v7's name), still HttpOnly; Secure; SameSite=None; Partitioned, and ?__draft=off.
  • Draft pointer hosts follow v7 (see the contract above); preview.sources is removed.
  • Telemetry reads v7's names as aliases: DECO_OTEL_{METRICS,LOGS,TRACES}_ENDPOINT → OTEL_EXPORTER_OTLP_{METRICS,LOGS,TRACES}_ENDPOINT (full per-signal URLs, now honoured too), DECO_OTEL_HEADERS → OTEL_EXPORTER_OTLP_HEADERS, and DECO_OTEL_AUTH_TOKEN as the authorization header unless the headers set one. The standard name wins when both are set; a signal with no URL is dropped.
  • Downstream: Studio's GitHub storage (repo-content-storage.ts) must drop refs/idempotency, its ref checks and the { ref } arguments to snapshot/readSchema; nothing in Studio used the other removed options.

Commits

  • 773229cc product-owner fixes (above): no handler auth, unused protocol options deleted, __deco_draft, v7 draft hosts + DECO_PREVIEW_API_DOMAINS, DECO_OTEL_* aliases.

  • e07ae05d review fixes: refuse a redirected draft response (or one from outside preview.sources) even when a fetch polyfill followed the redirect; URL-encode the version. Conformance DP-21.

  • 5f9dd074 fast-preview drafts: the pointer returns only the branch's changes; draftChanges.ts (fetch, checks, layering), preview.sources, Loader.load() without a pointer. Removes the overlay/blob/grant machinery and its protocol exports, fixtures and DO-1..18 conformance; adds DP-1..20.

  • 5020aea2 overlays (superseded by 5f9dd074): loader, protocol helpers, conformance DO-1..DO-14.

  • b69e7634 no preview-server concept (comments, DO-18 wording, HD-18 one-host recipe gating pointer and cookie); check-schedule tests (HP-5..8, cms interval tests) await exactly the background work they scheduled, so they no longer flake under load.

  • 0141f5a5 deco schema: @format on a type alias reaches every field of that type (plain, optional, nullable, listed, literal select), so /** @format color */ type TextTone = "black" | "white" gets v7's format: "color" with its values kept. A field's own @format wins. Unit test + conformance sch-26; documented in schema.mdx.

  • 3a985534 v7→v8 migration skill: unwraps v7 async-rendering wrappers (website/sections/Rendering/Lazy.tsx, SingleDeferred.tsx, Deferred.tsx) to the sections they held, in pages and saved blocks; idempotent, with fixtures.

  • 32650f6b review fixes (overlay parts superseded):

    • grant expiry on warm servers (manifest max-age, 60 s CMS draft TTL)
    • forDraft schedules the release check
    • per-caller block budget
    • one shared BoundedMap (LRU, optional size) instead of two map classes
    • the composed-view cache is removed, since ContentStore already keeps drafts
  • 63b07191 restored legacy secret guard exemption (above).

  • 9a45c8c7, 85bf7112 deco serve has no token; link on a wildcard host, case-insensitive --host, refused origins logged.

  • dddefd54 the CMS settings block, cms.settings(), host patterns, cms.draftPointer/cms.draftCookie, schema/check support, migration fold, docs.

  • e7a1d1d1 review fixes: per-call block maps (the Next proxy bug), settings held across a custom loader's update, frozen settings, userinfo and empty-label hosts, migration host normalisation and the TanStack deco-hosted hosts, the stale observability fixture.

  • 963fd2b4 an instance stored by an older package copy is replaced, not adopted.

  • 766b0ff7 schema.get with no schema yet returns { notModified: false, version: null, resolvedRef, schema: null } instead of NotFound (no .deco folder is still NotFound); blocks.list/blocks.apply already work without one. Conformance schema/absent + cp-31; docs: content-protocol "No schema yet" (docs commit befb394e, imported by deco-sites/docs-tanstack#4). Studio side: feat: support Blocks v8 (content protocol) behind a flag studio#7728.

Tests

  • bun run check (typecheck, biome, knip) passes.
  • bun run test at 773229cc: 1863 passed, 64 skipped (71 files).
  • Conformance DP-1..DP-21 (delivery.test.ts), one per documented claim:
    • request shape (URL, v, no variants, no cookies/credentials/site token, no redirects)
    • set/delete layering, and list as the union minus deletes
    • production captured once per client
    • opaque revision
    • host allowlist with bypass attempts (look-alikes, sibling/sub domains, loopback, metadata IP, userinfo, scheme, //, backslash, uppercase)
    • DECO_PREVIEW_API_DOMAINS replacing the default, not settable from content
    • scheme per host
    • non-200 and redirects
    • 16 MiB while streaming and by content-length
    • 10 s timeout
    • strict shape
    • failures as LOADER_FAILED on every call, never reused
    • bad token
    • 60 s / 3-entry reuse
    • no site/token needed or sent
    • local pointers
    • forced variants sharing one fetch
    • empty changes
    • release checks never in front of a draft
    • __proto__ entries
    • a redirected response refused even when the fetch followed it
  • HD-2/3/4/8/15/16, CD-1, RD-7, H-5, HRI-10, AR-08/11/18/22/66, RD-02, X21, dd-11, si-10, cms/settings unit tests: rewritten for the pointer flow. Bracketed IPv6 pointer parsing is in draft.test.ts.

Decisions (product owner)

  1. There are no preview servers. A server that renders a draft is a production server rendering the draft a pointer names. The release check is unconditional and identical whether or not it serves drafts. Done in the SDK comments, DO-18, the HD-18 recipe and the docs.
  2. Drafts are the v7 fast-preview pointer, returning only the branch's changes (supersedes the overlay design: no /preview, no preparing state, no object-storage overlays, no grants). Accepted with the docs' decisions:
    • Loader.load() has no pointer.
    • The default source is exactly studio.decocms.com. Superseded by decision 6.
    • An old link shows the branch's current changes while its token is valid.
    • A local version names no draft.
  3. Editor-form differences (Eitri app): the color picker is fixed in deco schema (@format on a type alias reaches its fields). The other differences are accepted.
  4. Async rendering: v8 has none, and the migration unwraps v7 Lazy/Deferred wrappers.
  5. One CMS settings block. Preview hosts, telemetry and analytics live in the optional saved block CMS (type cms-settings), which replaces the telemetry/analytics built-ins and blocks outright. Code holds destinations, secrets and caps, including preview.hosts. cms.settings() reads only the production release in memory, never a draft and never the network. Drafts on a host outside the list get published content. The site editor edits the block under "Settings". There's no channel-manifest setting and no protocol change.
  6. v7 compatibility, fewer mechanisms (773229cc): deco serve and the protocol handler have no token (Studio's session protects its /rpc); the cookie is v7's __deco_draft; draft hosts are v7's domains with DECO_PREVIEW_API_DOMAINS; v7's DECO_OTEL_* names keep working; unused protocol options (request keys, schema preconditions, refs, advertised limits) are deleted.

Stack: v8 ← … ← 15 ← 16

🤖 Generated with Claude Code

https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig


Summary by cubic

Hosted drafts now load as a pointer to only the branch's changes, layered over the production snapshot already in memory. The PR also centralizes preview, telemetry and analytics settings in the CMS block, moves draft helpers onto the CMS, and drops the content protocol's shared authentication and deco serve's bearer token, origin allowlist, and Host check. The conformance tests now read the docs from deco-sites/docs-tanstack.

Drafts and settings

  • A draft pointer fetches only the branch's changes — a { format, set, delete } document — layers them over production, applies tombstones and variants, and fails with LOADER_FAILED without a published fallback. Its hosts follow v7's patterns exactly (.decocms.com suffix, local.studio.decocms.com, localhost, 127.0.0.1, *.localhost; [::1] is no longer a default, but a listed [::1] still counts as loopback), with DECO_PREVIEW_API_DOMAINS replacing the list.
  • The draft cookie is __deco_draft again and preview.sources is gone. cms.settings() reads release settings without fetching, applies defaults and caps, and gates cms.draftPointer() and cms.draftCookie() by configured preview hosts.
  • telemetry and analytics built-ins are replaced by the cms-settings block; existing sites must migrate these settings into .deco/blocks/CMS.json, and telemetry also reads v7's DECO_OTEL_* environment names as aliases.
  • The shared instance returns a handle per block map, so a second bundle no longer replaces another's; a stale stored instance from an older package copy is replaced, and settings survive an update (last release kept in memory, deep-frozen) instead of falling back to defaults.
  • The content protocol handler no longer authenticates: neither the handler nor its client takes a token, and who may call it is the mounting server's job (Studio's session, deco serve on loopback). Request keys and idempotency receipts, ifSchemaMatch, ref params and describe.limits are removed; blocks.apply keeps its own guards (500 names, 1 MiB per entry, 8 MiB per request).
  • Host matching for preview.hosts tightens: URL credentials make a host unreadable except by *, and an empty label matches nothing but *.

Migration and tooling

  • The v7 migration skill now supports native apps, Next.js/VTEX and non-ejected FastStore storefronts, folds legacy site settings into CMS.json, and unwraps v7 async-rendering wrappers. Lessons from a second TanStack + VTEX storefront cover the removed /deco/invoke (one server function per call site), gating draft bypass on cms.draftPointer, module-level config singletons as cross-request state, and parity gotchas (pin request.cf, track flake counts). The FastStore one covers a vendored runtime built from the published package, Pages Router SSG drafts hooked into Next's compiled runtimes, listing the dev host in preview.hosts, and deco check gating the build. A placeholder site name replaces fila-store in the deployment skill.
  • previewHosts are trimmed, lowercased and validated as host patterns, with the TanStack default hosts (<site>.deco.site, <site>.deco-cx.workers.dev) added when the site name is found; @decocms/blocks/cli exports the host parser.
  • Schema generation preserves literal order and alias-level @format annotations, maps datetime to date-time, avoids saved-block pickers for free-form maps, and stamps every schema with blocksMajor: 8 — the only signal Studio reads to tell a v8 site from a v7 one — which deco check requires.
  • schema.get reports "no schema yet" as schema: null (with version: null) instead of NotFound, so clients can list and edit blocks without typed forms and keep polling until one appears.
  • deco serve has no bearer token, --allow-origin, or Host check: it answers any origin's CORS, so it's for editing only, and without --host it listens on 127.0.0.1 and ::1 on one port, so every startup line and the site-editor link say localhost.
  • Legacy website/loaders/secret.ts blocks are excluded from the v8 secret guard so their existing ciphertext is preserved.

Written for commit 73bc4f3. Summary will update on new commits.

View guided diff Turn on auto-fix

tlgimenes and others added 2 commits October 3, 2026 23:02
… production

forDraft on a hosted CMS no longer downloads a complete draft snapshot.
remoteLoader.load(pointer) now captures the production snapshot it already
has (newest release, else the fallback) once, fetches the immutable overlay
manifest the pointer's version names and only the changed-block assets
missing from its cache, and layers replacements and tombstones over the
captured map (list = union minus deletions). It never fetches a production
revision to align. The Loader interface is unchanged: the composed view is
an ordinary Snapshot whose revision is "<base revision>~<overlay version>".

- Contract: GET /sites/<site>/drafts/<version>.json and
  /sites/<site>/draft-blocks/<hash>.json on the delivery origin, with the
  site token and the pointer's query (the grant) forwarded verbatim. The
  pointer must be <delivery host>/sites/<site>/drafts?<grant>@<version>.
- Integrity: the manifest must hash (SHA-256 of canonical JSON) to its
  version and each block to its hash; strict manifest shape, disjoint
  set/delete, format 1 only.
- Caches per site: manifests by version + grant (a version alone unlocks
  nothing), 16 MiB of verified blocks by hash, 3 composed views keyed by
  captured base and version; ContentStore keeps 3 drafts and drops them when
  a hot reload swaps the fallback under the same loader.
- One 64 MB budget per draft (manifest + blocks), charged while downloading.
- Any failure is LOADER_FAILED; failures aren't cached. Forced variants keep
  applying on top.
- Protocol: computeBlockHash, computeOverlayVersion, DRAFT_OVERLAY_FORMAT,
  DraftOverlay, and draftOverlayFixtures golden hashes for the preparer.
- Conformance DO-1..DO-14 for every documented overlay claim; existing
  draft claims moved to overlays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…se checks

Review fixes on top of the overlay loader (docs: content-delivery#exact-draft-previews,
hosted-releases-internals).

- Grant expiry on warm servers: an authorized overlay manifest is cached by
  version + grant only for its response's `max-age` (none, 0 or no-store is
  not cached), so an expired grant is asked about again and fails. The CMS
  reuses a loaded draft for at most a minute before `load(pointer)` runs again.
- `forDraft` schedules the same background release check `forRelease` does:
  a server that only serves previews still picks up new releases. It never
  runs in front of the draft and is not an alignment fetch.
- A shared changed-block download is bounded on its own and then charged to
  every waiting draft, so one draft's budget never fails another's.
- Lean: one `BoundedMap` (LRU, optional size) for content.ts and
  remoteLoader.ts; the composed-view cache is gone (ContentStore already keeps
  drafts).
- Conformance: DO-2 now asserts the release check never blocks a draft;
  DO-15..18 cover manifest max-age, no-store, the CMS draft TTL and release
  pickup on preview-only servers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 7 commits October 4, 2026 00:03
…nd free-form maps

- A string/number literal select lists its values in the order the
  annotation (or the alias it names) writes them, not the checker's
  type-id order, which another file could change.
- @Format datetime is the date-time picker, as v7 wrote it (and deco
  check now validates it as one).
- Record<string, any> fields no longer offer the saved-block picker and
  every block as a choice: every block's output fits a map of anything.

Found migrating an Eitri app's editor forms to v8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Adds reference/native-apps.md for apps whose v7 binding only generated
Studio files (bundled JSON, native rendering): dependencies, prestart
scripts, committed schema.gen.json, the non-discovered block map, deco
serve with --allow-origin and no preview, a static bundle/content/forms
parity harness, and pinning a blocks release with the form fixes.
parity.md gains the pending-approval state and sorts editor-form
differences into stale v7 files, dropped v7 heuristics, and CLI bugs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- Next.js: add @decocms/blocks to transpilePackages while the 8.1.0-next
  prereleases publish TypeScript source (v7's withDeco used to).
- Verify once without a local link: a clean frozen-lockfile install and
  build, plus a schema.gen.json diff against the npm CLI.
- Gotchas: vendored loaders the v7 site never ran, explicit error reporting
  in place of onResolveError, drafts on static Next pages, listing pages
  once per revision (probes included), head meta order, Jest with an ESM
  package, bundled .deco/blocks imports under hosted releases, v7 leftovers
  (env, /deco /live /.decofile exclusions), editor settings that leave with
  the site block.
- Parity: build against what ships, ISR build age, flakes under load,
  naming what the cases don't cover; editor forms: dynamic option
  pickers become text fields, page form order and labels, the RichText
  widget substring rule, literal order depending on the v7 generator.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…rule

A server that renders a draft is a production server reading a draft
pointer: its release checks are the same as any other's. Comments and
DO-18 say so; HD-18 is the one-host recipe (pointer and cookie gated by
the app's own rule). The check-schedule tests (HP-5..8, cms interval
tests) await exactly the background work they scheduled instead of a
fixed sleep, so they no longer read a count short under load.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
/** @Format color */ type TextTone = "black" | "white" gives each
TextTone field (optional, nullable, listed) format: "color", the select's
values kept, as v7 wrote it for its color picker. A field's own @Format
wins. Unit test plus conformance sch-26.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
v8 has no async rendering. The script replaces website/sections/
Rendering/Lazy.tsx and SingleDeferred.tsx ({ section }) with their
section and Deferred.tsx ({ sections }) with its sections, spliced into
the list that held it, in pages and saved blocks; wrapper options go.
A wrapper with several sections where one block goes is reported.
Idempotent. Parity notes: color-picker loss is not approved, annotate
the alias @Format color.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@tlgimenes
tlgimenes force-pushed the v8-16-draft-overlays branch from 56828b5 to 3a98553 Compare October 5, 2026 13:43
tlgimenes and others added 6 commits October 5, 2026 11:20
A v7 website/loaders/secret.ts block marks its encrypted string
"format": "secret", so the guard refused every apply that carried one
(an app config, for instance) once a v7 site was edited through the
content protocol.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Remove the bearer token from `deco serve` entirely: no random token, no
--token flag (now an unknown flag), no DECO_SERVE_TOKEN, no Authorization
check on /rpc or /assets, and no token in the site editor link, which is now
https://studio.decocms.com/site-editor#endpoint=<encoded endpoint>.

What protects it is unchanged: it listens on 127.0.0.1 by default, browser
requests need an allowed Origin, the Host header must be its own address
(DNS rebinding), requests without Origin are accepted, and CORS and Chrome
local-network preflights are answered. --host beyond loopback prints a
one-line warning that other machines on the network can reach it.

Tests cover: no token anywhere (DECO_SERVE_TOKEN ignored, --token unknown,
a stray Authorization header ignored), foreign Origin refused (including on
preflight), rebinding Hosts refused (evil.com, 127.0.0.1.evil.com, another
port, missing Host), no-Origin requests accepted, --host accepting its own
Host and warning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
… analytics

The well-known saved block CMS, of the built-in type cms-settings, replaces
the telemetry and analytics built-ins (no aliases) with three sections:
preview, telemetry and analytics. Code keeps destinations, secrets and caps:
telemetry.limits caps the rates, and the new createCMS preview.hosts caps the
hosts content may allow previews on.

- cms.settings() resolves CMS from the release already in memory (the
  content module at boot, the hosted release once a background check swaps
  it in), never a draft and never a fetch; defaults filled in, caps applied,
  a failing section falls back to its defaults. It never rejects.
- cms.draftPointer and cms.draftCookie replace the free draftPointer,
  draftCookie and DRAFT_COOKIE: on a host outside the effective list they
  ignore the draft, so the request gets the release, never an error.
- Host patterns: exact names, *.name wildcards (label by label, two labels
  at least), "*", optional ports, IPs exact, punycode; createCMS throws on an
  invalid pattern, content drops it, and content entries must fall within
  code's list.
- Telemetry follows the release's telemetry section, read when the release
  changes and then at most once a minute (date rules only).
- deco schema emits cms-settings in the content group; deco check points a
  leftover telemetry/analytics block at the CMS block and warns on a CMS of
  another type.
- The v7-to-v8 migration folds the Site block's previewHosts, a literal
  OneDollarStats collectorAddress and prerelease Telemetry.json /
  Analytics.json into CMS.json, idempotently, and reports env-only settings.
- Examples: async client(request); schemas regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…, log refused origins

--host 0.0.0.0/:: prints a 127.0.0.1 site-editor link (Studio accepts only
loopback); a specific non-loopback --host warns the site editor connects
only through 127.0.0.1/localhost. --host is lowercased to match the Host
check. Each refused browser origin is logged once with the --allow-origin
to pass, since the browser shows the 403 as a network error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ter preview hosts

- createCMS returns a handle per block map on the shared instance: content,
  caps, telemetry and update checks are shared, and each handle resolves with
  its own map. A second bundle in the process (Next's proxy.ts importing the
  app's cms) no longer replaces the app's map; adopt() only swaps content.
- ContentStore.update() keeps the last served release in memory, so a custom
  loader's settings (and preview hosts) don't fall back to the defaults
  (every host) between an update and the next load.
- cms.settings() is deep-frozen: one object per release reaches every caller.
- Host matching: a URL with a username or password is unreadable (only "*"
  matches), and a hostname with an empty label matches nothing but "*".
- v7-to-v8 migration: previewHosts are trimmed, lowercased and checked as host
  patterns (invalid ones reported); on TanStack Start the <site>.deco.site and
  <site>.deco-cx.workers.dev hosts v7 always allowed for DECO_SITE_NAME are
  added (or reported when the name isn't found); the behaviour-change note
  says what v7 actually allowed. @decocms/blocks/cli exports the host parser.
- The observability fixture uses the CMS block instead of the removed type.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
An instance an older copy of @decocms/blocks stored on globalThis (a dev
reload across versions) has no handles, so adopting it threw
"existing.handle is not a function". createCMS now replaces it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes and others added 7 commits October 5, 2026 16:10
deco serve has no token, and now no origin allowlist either: CORS is
answered for any Origin (reflected, with Vary: Origin), and Chrome's
Private/Local Network Access preflights are still answered. Any website
open in the browser can read and write the content through it, so it is
meant to run only while editing.

Removed: --allow-origin (and the parser's "list" flag kind it alone
used), the STUDIO_ORIGINS allowlist, the refused-origin 403 and its log
line, and the Host-header check (cp-43), which is pointless once every
origin is allowed. --host still prints its network warning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
….1 and ::1

Without --host, deco serve listens on 127.0.0.1 and ::1 on one port (::1
skipped where IPv6 is unavailable), so http://localhost:<port> reaches it
whichever address the OS gives localhost. Every startup line and the site
editor link now say localhost. --host keeps listening on that one address.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
A site whose schema isn't generated yet is a normal state: blocks.list and
blocks.apply already work without one, so the site editor can still list
and edit every block. schema.get now says so with a typed result instead of
NotFound:

  { notModified: false, version: null, resolvedRef, schema: null }

There's no version to poll with, so a client reads it unconditionally until
a schema appears. A site with no .deco folder is still NotFound (the snapshot
check), and a sent ifSchemaMatch conflicts with actual: null. describe
doesn't report it: that would read the whole schema on every describe.

Conformance: new schema/absent case (listed in cp-50), schema/read skips on a
schemaless endpoint, cp-31 now claims schema: null plus a list and a save.
deco serve's startup line says "no schema yet: run deco schema for typed
forms".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- /deco/invoke: one server function per key imported by the call site,
  never a rebuilt invoke tree or string-keyed dispatcher; v7's invoke
  passed no app context; keys v7 had no handler for stay explicit
  failures; check the client bundle for server code. The script's report
  line says the same.
- Caching and privacy: gate the copied worker entry's draft bypass on
  cms.draftPointer (v7 gated it on preview hosts); never let router
  hydration wait on a cross-origin call.
- Rendering: pages awaited because headers depend on the result; side
  effects of deferred sections now start on load, and gating them on
  v8's layout doesn't reproduce v7's (short v7 layouts mounted them on
  load); Tailwind generates classes from words in comments and docs.
- Framework code: module-level config singletons are cross-request
  state; identification headers are the site's; keep v7's platform
  client or adopt the v8 one, and drop the unused dependency.
- Hosted releases replace v7 fast deploy (secrets, memory estimate);
  telemetry bindings with no v8 writer are a listed decision;
  service.version from the build commit.
- Verify: typecheck against the v7 count, type fixes can change the
  editor form. Parity: pin request.cf across checkouts, re-record bad
  baselines, track flake counts, prove code fixes on every page type,
  name what a replaying harness can't see.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
From migrating a non-ejected VTEX FastStore storefront (Next.js 16 Pages
Router, private):

- reference/faststore.md: keep v7's seam (a vendored runtime built from the
  published package, the Content Platform client patch returning FastStore's
  section shape); drafts on Pages Router SSG need a hook in Next's compiled
  pages runtimes (a require.cache patch never runs), gated on a parseable
  pointer and memoised per request; x-nextjs-cache: HIT is no evidence;
  password-gated preview domains must read the v8 cookie; preview hosts and
  draft sources (no loopback in production); imported settings vs the SEO
  form; schema-derived defaults mapped through the block map; case-only
  name pairs; global sections that matched per URL.
- SKILL.md: probe drafts by hand on dev and a production build (a parity
  harness can't see a dead draft path when v7's was dead too); list the dev
  host in preview.hosts (v8 ignores DECO_ALLOWED_PREVIEW_HOSTS).
- gotchas.md: deco check in the build gates the deploy (importers must emit
  check-clean content); don't route on undeclared __ fields; case-only name
  pairs; Pages Router drafts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…e branch's changes

Product-owner decision: no /preview protocol. A draft pointer names Studio's
API (<host>/api/<org>/decofile/<project>/<branch>/changes?token=…@<version>),
which answers { format: 1, set, delete }: only what the draft branch changed
against production. cms.forDraft fetches it once and layers it over the
production snapshot the server already has.

- draftChanges.ts: fetch only from createCMS({ preview: { sources } })
  (default studio.decocms.com; refused before any fetch otherwise), https
  except loopback, v=<version>, no cookies or credentials, no redirects,
  200 only, 16 MiB, 10 s, strict shape; set/delete layering.
- A `local` pointer (deco serve) names no draft: nothing fetched, forced
  variants still apply.
- Drafts need no site/token. Loader.load() takes no pointer; remoteLoader
  serves releases only.
- Removed: overlay manifests and block blobs, grants, blob caches, the
  delivery-host-only pointer rule, computeBlockHash/computeOverlayVersion/
  DRAFT_OVERLAY_FORMAT/DraftOverlay and draftOverlayFixtures.
- parseDraftPointer accepts a bracketed IPv6 host.
- Conformance DP-1..DP-20 replace DO-1..DO-18.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
@tlgimenes tlgimenes changed the title v8 N-16: drafts are overlays over the local release v8 N-16: drafts are the fast-preview pointer, with only the branch's changes over the local release Oct 6, 2026
tlgimenes and others added 3 commits October 6, 2026 14:46
… version

A fetch polyfill that ignores redirect: "manual" (React Native, whatwg-fetch)
could follow a redirect off the allowed host; the SDK now refuses any response
that was redirected or came from a host outside preview.sources. The version
is URL-encoded so a looser version format can't add query parameters.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
deco schema writes a top-level "blocksMajor": 8 (BLOCKS_MAJOR, exported
from @decocms/blocks/protocol: the package major, not the version string).
It is the only signal Studio reads to tell a v8 site from a v7 one; the
schema file name does not tell. deco check fails on a schema without it or
with another value and says to run deco schema. schema.get serves it as-is.

Claims sch-27, chk-16, cp-51; example schemas regenerated; v7-to-v8
migration skill notes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ts and OTEL names

- Content protocol: the handler no longer authenticates (no token,
  authorize or scope; the asset handler neither). Who may call it is the
  mounting server's job: deco serve on loopback, Studio behind its session.
  The client's `token` option goes too.
- Removed: requestKey and idempotency receipts (describe.writes),
  ifSchemaMatch, the ref parameter and refs/autoCreate, and describe.limits
  with the read/batch limits (list, schema, batch response). blocks.apply
  keeps its own guards (500 names, 1 MiB per entry, 8 MiB per request) as
  internal constants. ifMatch and assets.maxBytes stay. Storages lose
  refs/idempotency/limits/getReceipt, and snapshot()/readSchema() take no
  options. Conformance suite updated to match.
- Draft cookie is `__deco_draft` again (v7's name), same attributes.
- Draft pointer hosts follow v7: `.decocms.com` suffix,
  local.studio.decocms.com, localhost/127.0.0.1/[::1]/.localhost, with
  DECO_PREVIEW_API_DOMAINS replacing the list. preview.sources is removed.
- Telemetry reads v7's DECO_OTEL_{METRICS,LOGS,TRACES}_ENDPOINT,
  DECO_OTEL_HEADERS and DECO_OTEL_AUTH_TOKEN as aliases of the standard
  OTEL_EXPORTER_OTLP_* names (standard wins), including per-signal URLs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The default preview API domains now match v7's draftSource exactly
(local.studio.decocms.com, localhost, 127.0.0.1, .localhost, .decocms.com).
DECO_PREVIEW_API_DOMAINS still replaces the list, and a listed [::1] is
still treated as loopback (plain HTTP, any port).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant