Migrating from v7
Keep v7 content working with aliases, replace v7 loaders, actions, telemetry and analytics, fold site settings into the CMS block, and migrate saved content safely.
Your v7 store's saved pages refer to blocks like site/sections/Product.tsx, and its product shelf calls a VTEX loader from the apps. You want the new code without rewriting all that content.
Saved content refers to your code by name: every block stores a type name in __resolveType, references store saved block names, and inputs are stored by field name. Treat these names like database columns: once content uses them, changing one breaks that content.
This page describes the planned migration, how to keep old names working with aliases, what to do with v7 loaders and actions, what replaces v7's telemetry and analytics, how secrets move over, and a checklist for moving content.
Migration availability
The migration tooling in the source proposal is not present at the pinned repository revision. Its suggested @decocms/blocks@^8.1 installation and deco-v7-to-v8-migration skill have not been verified as available, so this documentation does not offer them as runnable steps. Continue using the supported v7 migration guides until the preview release and migration tooling are confirmed.
The remaining sections describe the intended migration contract: preserve saved names with aliases, move data access into application code, update telemetry and secrets, and validate behavior before rollout. The planned next framework removes the v7 bindings and admin package; those dependencies must remain installed while a site still runs v7.
Seven v7 type names have no alias in the next major, so the migration rewrites them in your saved content to a name that resolves the same way, with the same props:
| v7 name | Saved as |
|---|---|
website/flags/multivariate/image.ts, website/flags/multivariate/message.ts, website/flags/multivariate/page.ts, $live/flags/multivariate.ts | website/flags/multivariate.ts |
$live/matchers/MatchAlways.ts | website/matchers/always.ts |
website/matchers/date.ts, $live/matchers/MatchDate.ts | date |
The Next design removes v7's framework-specific deferred-rendering wrapper blocks. The proposed migration unwraps them in saved content, pages and saved blocks alike, putting each section in its wrapper's place with its props unchanged:
| v7 wrapper | Becomes |
|---|---|
website/sections/Rendering/Lazy.tsx, website/sections/Rendering/SingleDeferred.tsx ({ "section": … }) | its section |
website/sections/Rendering/Deferred.tsx ({ "sections": [ … ] }) | its sections, in place in the list that held it |
The wrapper's own options (loading, display, behavior) are removed too; recreate any loading or display behavior in your application where needed. This does not remove asynchronous block functions or framework-native streaming. The rendering guides use per-block Suspense boundaries in Next.js and deferred promises in TanStack Start, so a section may still arrive after the initial response. A wrapper that held several sections where only one block fits is intended to be reported for you to unwrap by hand. The migration is intended to be idempotent.
Rename a block type
Aliases work for any rename, not only a v7 migration. An alias is another key in the block map pointing at the same function. Content that stores the old name keeps working, and routing is unaffected because matchRoute looks at each entry's path field (see Pages and routing), not its type name:
import type { Blocks } from "@decocms/blocks";
import productCard from "../src/product-card";
export default {
"product-card": productCard,
"site/sections/Product.tsx": productCard, // the name legacy content stores
} satisfies Blocks;Legacy page names such as website/pages/Page.tsx need no entry: the CLI's alias table maps them to the built-in page (see Site editor compatibility). Legacy website/flags/multivariate.ts blocks map to the built-in multivariate, and the alias bridge wraps each variant, a plain value, in a lazy block on the way.
An alias only works while both names take the same inputs. If the old shape differs, register a function under the old name that converts the old inputs and calls the new function, or migrate the content first. Here the old content called the title name:
type CardInput = Parameters<typeof productCard>[0];
export default {
"product-card": productCard,
"site/sections/Product.tsx": ({ name, ...rest }: Omit<CardInput, "title"> & { name: string }) =>
productCard({ ...rest, title: name }), // converts the old inputs
} satisfies Blocks;deco check rejects an alias that collides with a saved block's name.
Loaders, actions and invoke
On v7, saved content often calls loaders and actions that ship in Deco's apps, such as a VTEX product loader. In the next major the apps are thin upstream clients and ship no loaders, so the migration copies (vendors) the ones your site actually uses into your own code, rewritten over the clients, and registers each under its old type name as an alias. Content keeps resolving, and the copied code is yours to change from then on.
v7's /deco/invoke endpoint (which ran loaders and actions over HTTP for the site editor and the browser) and cachedLoader (see v7 caching) are gone. Call upstream clients from your framework's server functions or route handlers instead; upstream caching is a fetch your app passes to a client (see Upstream data).
Telemetry and analytics
v7 configured telemetry with environment variables and shipped its analytics as components. In the next major, they're two separate parts of Deco CMS: code says where each one sends, and the CMS block holds their switches and rates.
| v7 | Next major |
|---|---|
The OpenTelemetry export, switched on with DECO_OTEL and the DECO_OTEL_* endpoint and header variables (see v7 observability) | The telemetry option of createCMS: { endpoint, headers? } for your own collector, or the standard OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS variables. Sampling moves to the telemetry section of the CMS block. |
| Metrics Deco collected for your site in its own backend | telemetry: { site, token }, set explicitly (see Hosted telemetry) |
The Stats and OneDollarStats components, and the DECO_ANALYTICS_ENABLED and ONEDOLLAR_ENABLED variables | AnalyticsScript in your root layout, with the analytics section of cms.settings(), which speaks the One Dollar Stats format. Rendering it turns analytics on; the section's enabled field switches it off. |
The Analytics section with Google Tag Manager and GA4 tags | A tag manager block from your platform template, owned by your site; the migration lists the tag IDs in its report. Built-in analytics only counts page views, so it doesn't take them. |
| Experiment results keyed on the random matcher's saved-block name | The experiment ID of the A/B test; the migration copies the old name into it, so results carry over. |
Site settings
The next major keeps site-wide settings in one saved block, CMS, of the built-in type cms-settings. The migration folds what it finds into .deco/blocks/CMS.json, keeping anything already there, and writes nothing when there's nothing to fold:
| From | Into CMS.json |
|---|---|
The v7 Site block's (Site or site) previewHosts | preview.hosts, entry for entry, each trimmed and lowercased as v7 compared it. v7 compared hosts exactly, port included, and so does an entry with a port here (see Host patterns). An entry that isn't a host is left out and listed in the report. On TanStack Start, v7 also allowed <site>.deco.site and <site>.deco-cx.workers.dev for the site named by DECO_SITE_NAME; the migration adds both when it finds the name in wrangler.*, .env or the Vite config, and lists them in the report when it doesn't. The rest of the Site block stays where it is. |
A literal collectorAddress on v7's OneDollarStats component in your code | analytics.collector |
A Telemetry saved block (type telemetry) from an earlier next-major prerelease | The telemetry section, field for field; the old file is deleted |
An Analytics saved block (type analytics) from an earlier next-major prerelease | The analytics section, field for field; the old file is deleted |
Variants come along unchanged: a saved block that was a multivariate becomes the same multivariate in its section. The telemetry and analytics built-ins are gone, with no alias, so a leftover block of either type fails deco check until it's folded. Running the migration again changes nothing.
Settings v7 read from your hosting environment never reach the repository, so the report lists them for you to move by hand:
DECO_ALLOWED_PREVIEW_HOSTSreplaced the Site block's list. Put its hosts inCMS.json'spreview.hosts, or, to make them the most content may allow, inpreview.hostsofcreateCMS. Itsnoneis an empty list.DECO_OTEL_*sampling variables become thetelemetrysection's rates, within thelimitsin code.DECO_ANALYTICS_ENABLEDandONEDOLLAR_ENABLEDbecome theanalyticssection'senabled, andONEDOLLAR_COLLECTORitscollector.
A v7 site with no allowed hosts had draft preview off. In the next major, previews work on every host unless you list some, so add preview.hosts if you relied on that.
Where you render analytics, replace the resolved Analytics block with the settings: const { analytics } = await cms.settings(), then <AnalyticsScript {...analytics} /> (see Analytics).
Secrets
v7 saved credentials as website/loaders/secret.ts blocks, encrypted with the DECO_CRYPTO_KEY environment variable. The next major uses a key pair instead (see Secrets), so the migration re-encrypts each secret once:
- Create the key pair and commit
.deco/secrets.pub. - Run the migration with
DECO_CRYPTO_KEYset. It decrypts each v7 secret and saves it as asecretblock encrypted with your public key, in the same commit as the rest of the content. - Deploy with
DECO_SECRETS_KEYset, then removeDECO_CRYPTO_KEY.
Checklist for migrating existing content
- Keep existing names. If the content stores file-path type names, register them as they are or alias them, including the app loaders and actions you vendored.
- Check inputs. Run
deco check: it validates every saved block against the schema and lists each one that no longer fits. It can't see changed defaults, so review those yourself. - Migrate the content in one commit. A script reads the JSON in
.deco/blocks, rewrites it, and writes it back. The commit is a new revision you can revert in one step. Keep fields the script doesn't recognize instead of deleting them, and report references to missing entries (NOT_FOUND) instead of dropping them. - Test the integration. Load real pages against the migrated content: routes resolve, titles and SEO tags are right, blocks that load later (if your framework streams them) appear, failing blocks show their fallback, and navigation works.
- Keep a rollback pair. Keep the previous build plus the content revision it was tested with. Rolling back content alone can't bring back code you removed.
Test behavior, not just schemas. A function can keep its input type and still change its output, defaults, or upstream calls.