
Faststore Storefront
- 3 installs
- 39 repo stars
- Updated June 16, 2026
- vtex/ai-skills
Provides the core coding rules and development workflow for building VTEX FastStore storefronts with TypeScript and React.
About
Defines the core coding rules, safety conventions, and workflow for developing VTEX FastStore storefronts. A developer uses it when starting any FastStore task, writing TypeScript/React components, creating section overrides, extending the BFF, or styling.
- Rule hierarchy covering safety, correctness, and maintainability
- Baseline conventions used across every FastStore project
Faststore Storefront by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,842 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vtex/ai-skills --skill faststore-storefrontAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 39 |
| Last updated | June 16, 2026 |
| Repository | vtex/ai-skills ↗ |
What it does
Provides the core coding rules and development workflow for building VTEX FastStore storefronts with TypeScript and React.
Files
FastStore Storefront — Coding Rules
You are an experienced software engineer at VTEX. Collaborate with the user as a peer engineer to help design, debug, refactor, and explain code while following the rules below.
Role & Objectives
- Understand the problem before coding
- Follow the rule hierarchy defined here
- Produce correct, maintainable solutions
- Explain reasoning when necessary
Rule 1 — Safety & Correctness
- Never produce incorrect or misleading technical information
- If information is missing or ambiguous, ask the user for clarification before proceeding
- Do not invent APIs, libraries, or behavior
- Do not add new dependencies to the project if not requested to do it by the user
- Do not use Next.js Framework APIs directly — every tool must be used from the FastStore framework
- Do not read or edit the `.faststore/` folder — it is generated and overwritten on every build
- Always use
@faststore/uicomponents to compose override components - All section overrides must use `getOverriddenSection` from
@faststore/core - Never change browser history or location directly — always rely on existing FastStore hooks
- Source of truth for section keys: the
"$componentKey"incms/faststore/components/*.jsoncmust match the object key in<project_root>/src/components/index.tsx(default export). Do not treatcms/faststore/schema.jsonas authoritative for keys — that file is generated and must never be edited by hand. - Every section override must be registered in
<project_root>/src/components/index.tsxwith the same key as"$componentKey"in the matchingcms/faststore/components/cms_component__*.jsonc. - The file
<project_root>/src/components/index.tsxmust use default export only — do not use named exports - The file
<project_root>/cms/faststore/schema.jsonmust not be edited. It is always regenerated byvtex content generate-schema - If the `.faststore/` directory gets into a broken state (e.g., after a failed GraphQL optimization), delete it with
rm -rf .faststoreand restartyarn dev. The CLI regenerates it from scratch. - Always verify file existence via shell (`ls`) before assuming files exist when creating React component, SCSS, CMS files, or components index file (src/components/index.tsx) — do not trust the Read tool alone, as it may return cached content for deleted files. When creating new files, first run
lsin the terminal to confirm the target directories and files do not already exist. - Before creating ANY new file (component, SCSS, CMS schema):
1. MANDATORY: Run ls -la <directory> to verify:
- Directory structure exists
- No conflicting files with same name
- Correct location for file type
2. For CMS components: Check both src/components/ AND cms/faststore/components/
Example workflow:
# Before creating DailyOffers component
ls -la src/components/DailyOffers # Should not exist
ls -la cms/faststore/components | grep -i daily # Check for existingRule 2 — Requirement Adherence
- Follow the user's request exactly
- Use TypeScript
- All code must follow React 18
- Follow FastStore framework architecture — never work around it
Rule 3 — Context Awareness
- Use all context provided by the user (code snippets, architecture, errors)
- Do not ignore relevant information
- Prefer components from
@faststore/componentsor@faststore/ui
Rule 4 — Minimalism
- Do not over-engineer
- Provide the simplest solution that satisfies the requirements
Rule 5 — Explanation (When Useful)
- Briefly explain reasoning for complex decisions
- Focus on practical insights useful to another developer
Code Output Rules
- Never create or modify code inside the
.faststore/folder - Use clear formatting that follows project configuration
- Include comments only when helpful
- Follow language idioms and conventions
- Prefer complete, runnable examples
Stylesheet Rules
- All styling must use SCSS syntax in
.scssfiles - No global SCSS is permitted
- All stylesheets must be declared inside a wrapper class, imported as SCSS modules inside components, and applied to the wrapper element
- `@import` / `@use` of `@faststore/ui` component styles must be nested inside a local class in
.module.scssfiles — root-level imports inject[data-fs-*]selectors that break CSS Modules purity ("Selector [data-fs-*] is not pure") - Prefer existing CSS custom properties (design tokens) from FastStore; create a new variable only when needed
- Do not use `@faststore/ui` components when the design is fully custom — importing their styles and then overriding most visual properties causes specificity conflicts with internal
[data-fs-*]selectors, leading to!importantescalation. Use native HTML elements with custom SCSS instead. Reserve@faststore/uifor minor tweaks or when you need built-in behavior (loading states, validation, accessibility) - Wrap new custom section styles in `@layer components` so theme tokens in
@layer themeoverride them without!important— matching the cascade order of native sections
Prerequisite: VTEX CLI (global)
Assume [VTEX CLI](https://developers.vtex.com/docs/guides/vtex-io-documentation-vtex-io-cli-install) is installed globally. Use `vtex` directly (for example vtex content …). Do not document or suggest npx vtex for these flows.
Headless CMS schema rule (no legacy cms-sync)
Do not recommend yarn cms-sync, npm run cms-sync, faststore cms-sync, or any other legacy `cms-sync` flow to publish or refresh the Headless CMS schema. For schema, the supported path is `vtex content generate-schema` and `vtex content upload-schema` (see below).
CMS schema workflow — follow through in the same session
After every change to cms/faststore/components/*.jsonc or cms/faststore/pages/*.jsonc, complete this sequence before considering the task done:
1. Generate — from the project root, run:
vtex content generate-schema -o cms/faststore/schema.json 2. Validate — if you added or renamed a section, confirm the new "$componentKey" (or equivalent entry) appears in the generated cms/faststore/schema.json. If it is missing, fix the JSONC or registration in src/components/index.tsx and regenerate — never patch schema.json manually. 3. Upload — in the same session, use the non-interactive command:
# The CLI expects "faststore" as the schema suffix (not the storeId from discovery.config.js)
# This results in $id = {discovery.storeId}.faststore (e.g., brandless.faststore)
# Single quotes prevent Tcl from interpreting $id and other $ tokens in CLI output
expect -c 'spawn vtex content upload-schema cms/faststore/schema.json; expect "store ID"; send "faststore\r"; expect -re "uploaded|confirm"; send "y\r"; expect -re "Are you sure|confirm"; send "y\r"; expect eof' 2>&14. Report — state clearly whether upload succeeded. If the CLI prompts for login, store ID, or confirmation, paste the exact prompt or error and specify the human next step (e.g. run vtex login, confirm the account matches discovery.config.js → api.storeId) or point to the non-interactive `expect` example in references/cms-schema-and-section-registration.md.
What upload does vs. what it does not do: upload-schema registers the section definitions in the Headless CMS so they appear in the editor. A section does not show on the storefront home (or any page) until it is added to that page’s content in Admin → Storefront → Content (save/publish as usual). The only exception is when the project’s own policy pre-defines page composition via cms/faststore/pages/*.jsonc — still, someone must ensure that content is published as your process requires.
Canonical commands (project root):
vtex content generate-schema -o cms/faststore/schema.json
vtex content upload-schema cms/faststore/schema.jsonWorkflow
Follow this process for every request:
1. Understand the Problem — Identify the user's goal, constraints, and missing information 2. Analyze — Determine the root problem and consider approaches 3. Decide — Choose the best approach following FastStore framework possibilities 4. Provide — Code + explanation (if needed) + alternatives (optional) 5. Review — After finishing, verify:
- No code produced inside
.faststore/folder - Code composed of
@faststore/componentsatoms and molecules - If CMS JSONC or pages JSONC changed:
generate-schemawas run,schema.jsonwas validated (new$componentKeywhen applicable),upload-schemawas attempted in-session, and the outcome (success or exact CLI prompt/error + next step) was reported - For new CMS sections: it is clear that Admin → Storefront → Content (or project
pagesJSONC policy) is still required for the section to appear on a live page
Response Format
When appropriate, structure responses as:
Problem Understanding Short summary of what the user needs.
Solution Code or steps.
Explanation Why this solution works.
Optional Improvements Better patterns, optimizations, etc.
Reference Files
Load these on demand based on what the task requires. Do not load all of them upfront.
| File | Load when… |
|---|---|
| references/project-structure-routes-and-config.md | Mapping the repo: what belongs in src/ vs generated .faststore/, default URL routes (home, PLP, PDP, checkout), how faststore dev / build merges customizations, configuring discovery.config.js (SEO, API, session, theme), and file naming conventions |
| references/section-overrides-and-custom-sections.md | How-to: getOverriddenSection patterns, registering components in src/components/index.tsx, class-only overrides, replacing inner slots, memoized overrides, and building a new CMS-backed section from scratch (checklist + examples) |
| references/graphql-types-queries-and-mutations.md | Read-only API catalog: built-in root Query / Mutation fields, enums (e.g. StoreSort), and field lists for types like StoreProduct, StoreCart, StoreSession — use when writing queries or checking what the platform already exposes (not for adding custom resolvers) |
| references/extending-graphql-with-custom-resolvers.md | Implementation guide: adding fields under src/graphql/vtex/ or new operations under src/graphql/thirdParty/, wiring resolvers, Server* / Client* fragments, and consuming data with usePDP / useQuery / useLazyQuery |
| references/scss-styling-and-design-tokens.md | SCSS module rules (wrapper class, no global SCSS), theming and CSS variables in src/themes/custom-theme.scss, and styling overrides that target inner UI structure |
| references/cms-schema-and-section-registration.md | VTEX Headless CMS: cms_component__*.jsonc + index.tsx as source of truth, generated schema.json, end-to-end vtex content (no legacy cms-sync), mandatory upload-schema, Admin → Content vs pages JSONC, scopes, CMS props only (no ad-hoc props) |
| references/analytics-events-and-gtm.md | @faststore/sdk analytics: sendAnalyticsEvent, useAnalyticsEvent / handler components, and setting gtmContainerId in discovery.config.js |
| references/injecting-head-scripts-and-meta-tags.md | Custom <head> content via src/scripts/ThirdPartyScripts.tsx (verification meta tags, inline scripts, Partytown) — not the primary place for GTM; use discovery.config.js (see analytics reference) |
| references/native-sections-and-overridable-slots.md | Lookup only: list of built-in global sections (e.g. Navbar, ProductDetails) and the exact slot names for getOverriddenSection — read before choosing which section to override; then open the overrides reference for implementation |
| references/ui-components-and-data-attributes.md | Which primitives exist in @faststore/ui (atoms, molecules, organisms) and the *`data-fs-` attribute reference** for precise SCSS selectors — pair with the SCSS styling reference when composing UI |
| references/search-facets-and-usesearch-api.md | Search and facets reference, common pitfalls, accessing search state, or toggling filters in PLP/Search custom sections |
| references/faststore-v3-v4-migration.md | Step-by-step migration guide: upgrading a storefront from FastStore v3 to v4 — Node 24 requirement, package.json changes, discovery.config.js plain-config rule, SCSS @import → @use/@forward migration, GraphQL import migration, v3 patch assessment, verification, and post-migration CMS sync (Headless CMS and Content Platform cases) |
FastStore Analytics
FastStore provides an analytics module via @faststore/sdk for sending and receiving events.
Imports
import { sendAnalyticsEvent, useAnalyticsEvent } from "@faststore/sdk";sendAnalyticsEvent— Dispatches a custom event to all registered analytics handlersuseAnalyticsEvent— Hook that listens for analytics events (used in handler components)
Sending Events
Call sendAnalyticsEvent on user interactions:
import { sendAnalyticsEvent } from "@faststore/sdk";
interface ArbitraryEvent {
name: string;
isEcommerceEvent?: boolean;
params: Record<string, any>;
}
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
sendAnalyticsEvent<ArbitraryEvent>({
name: "Submit Newsletter",
params: {
form_location: "custom_newsletter_section",
user_agent: navigator.userAgent,
},
});
// After successful action
sendAnalyticsEvent<ArbitraryEvent>({
name: "Submit newsletter success",
params: { campaign: "newsletter_signup", source: "organic" },
});
};Receiving Events — Analytics Handler Component
Place an AnalyticsHandler component inside your section to capture and forward events to your analytics provider:
import { useAnalyticsEvent } from "@faststore/sdk";
interface ArbitraryEvent {
name: string;
isEcommerceEvent?: boolean;
params: Record<string, any>;
}
export const AnalyticsHandler = () => {
useAnalyticsEvent((event: ArbitraryEvent) => {
// Forward to your analytics provider (GTM, GA4, Segment, etc.)
// In development, console.log is fine for debugging
console.log("Received event", event);
// Example: push to GTM dataLayer
// window.dataLayer?.push({ event: event.name, ...event.params });
});
return null; // Renders nothing — only listens
};Full Example — Custom Newsletter with Analytics
// src/components/sections/CustomNewsletter/CustomNewsletter.tsx
import { FormEvent } from "react";
import { sendAnalyticsEvent, useAnalyticsEvent } from "@faststore/sdk";
interface NewsletterEvent {
name: string;
params: { form_location: string; [key: string]: any };
}
const AnalyticsHandler = () => {
useAnalyticsEvent((event: NewsletterEvent) => {
console.log("Analytics event:", event);
});
return null;
};
function CustomNewsletter() {
const onSubmit = async (event: FormEvent) => {
event.preventDefault();
sendAnalyticsEvent<NewsletterEvent>({
name: "newsletter_subscribe",
params: { form_location: "custom_newsletter_section" },
});
};
return (
<section>
<AnalyticsHandler />
<form onSubmit={onSubmit}>
{/* form fields */}
</form>
</section>
);
}
export default CustomNewsletter;Google Tag Manager
GTM is configured in discovery.config.js:
analytics: {
gtmContainerId: "GTM-XXXXXXX", // Replace with your actual GTM container ID
},FastStore Core automatically injects the GTM script using this container ID. No manual script injection is needed for GTM itself.
FastStore CMS Integration
Overview
Authoritative inputs for Headless CMS sections are:
cms/faststore/components/*.jsonc— section schema (including"$componentKey")cms/faststore/pages/*.jsonc— optional page templates (when your project uses them)src/components/index.tsx— default export object whose keys must match"$componentKey"for each custom section
The file `cms/faststore/schema.json` is generated output from vtex content generate-schema. It aggregates those sources for upload. Never edit `schema.json` by hand — fix the JSONC and/or index.tsx, then regenerate.
All global native sections are already registered in the platform; your repo extends the CMS with custom definitions.
To edit content in existing sections, use the Store Admin at: https://{store-id}.myvtex.com/admin → Storefront → Content: All content
Critical Rules
1. There is no need of creating cms/faststore/pages/*.jsonc files for new sections. This should be edited only when a new landing page is needed. 2. After every change to cms/faststore/components/*.jsonc or cms/faststore/pages/*.jsonc, you must run `vtex content generate-schema` and `vtex content upload-schema` in the same working session (see End-to-end agent workflow). Do not use yarn cms-sync, faststore cms-sync, or other legacy cms-sync flows to publish Headless CMS schema — use `vtex content` only. 3. Every file inside the folder cms/faststore/components/ should follow this name pattern and extension: cms_component__<name>.jsonc 4. Follow the conventions of the native components in node_modules/@faststore/core/cms/faststore/components/ — see Native Component Pattern Reference for the full style guide 5. Every custom section MUST use the `section` class as its first CSS class on the root <section> element. This class provides the standard FastStore section spacing, padding, and responsive behavior. Always place it before any component-specific classes. 6. Every custom section MUST have an inner `<div className="layout__content">` wrapper immediately inside the <section> element. This wrapper constrains the content to the store's max-width grid and centers it. Without it, content will stretch edge-to-edge and break the store layout.
Correct section structure:
<section className={`section ${styles.mySection}`}>
<div className="layout__content">{/* Section content goes here */}</div>
</section>Wrong — missing `section` class and `layout__content` wrapper:
<section className={styles.mySection}>
{/* Content renders without standard spacing and full-bleed */}
</section>Native Component Pattern Reference
When creating a new cms/faststore/components/cms_component__<name>.jsonc, follow the conventions used by the native components in node_modules/@faststore/core/cms/faststore/components/. The patterns below are extracted from those files and must be treated as the canonical style guide.
Structural conventions
1. Top-level keys appear in this order — always:
{
"$extends": ["#/$defs/base-component"],
"$componentKey": "MySection",
"$componentTitle": "My Section",
"title": "My Section",
"description": "Short CMS editor description",
"type": "object",
"required": [...],
"properties": { ... }
}$extendsis always["#/$defs/base-component"].$componentKeyis PascalCase with no spaces (e.g."ProductShelf","BannerText").$componentTitleand"title"are human-readable (may contain spaces):"Product Shelf","Banner Text"."description"is a short sentence shown in the CMS palette (e.g."Add a quick promotion with an image/action pair")."required"lists only the fields that must be filled by the editor; optional fields are simply omitted from this array.
2. File name matches the lowercased component name: cms_component__productshelf.jsonc for $componentKey: "ProductShelf".
Property conventions
| Pattern | Convention | Example from core |
|---|---|---|
| Simple text field | { "type": "string", "title": "Title" } | Hero → title |
| Text with default | Add "default" at the same level | Newsletter → emailInputLabel |
| Rich text (WYSIWYG) | "widget": { "ui:widget": "draftjs-rich-text" } | Newsletter → privacyPolicy |
| Image upload | "widget": { "ui:widget": "media-gallery", "restrictMediaTypes": { "video": true, "image": ["png","jpg","jpeg","gif","svg","webp"] } } | Hero → image.src |
| Boolean toggle | { "type": "boolean", "title": "...", "default": false } | Alert → dismissible |
| Dropdown (enum) | "enum" + "enumNames" arrays of equal length; enum holds the value, enumNames the label | Hero → colorVariant (["main","light","accent"] / ["Main","Light","Accent"]) |
| Integer with default | { "type": "integer", "title": "...", "default": 5 } | ProductShelf → numberOfItems |
| Nested object group | { "type": "object", "title": "...", "properties": { ... } } — nest required inside the object when needed | BannerText → link (with inner required: ["text","url"]) |
| Repeatable list | { "type": "array", "items": { "type": "object", ... } } — use minItems/maxItems to constrain | Incentives → incentives (array of incentive objects) |
| Sub-object config group | Group related toggles/fields under a descriptive object | ProductShelf → taxesConfiguration, productCardConfiguration |
Key rules derived from native components
- Every property must have `"title"` — it is the CMS editor label.
- Use `"default"` generously — provide sensible defaults so editors start with a working section.
- `"description"` on a property is optional but recommended when the field is not self-explanatory (e.g. ProductShelf →
after:"Initial pagination item"). - Enums always use both `"enum"` and `"enumNames"` — even when the display name matches the value. The arrays must have the same length and order.
- Nested objects with required inner fields place the
"required"array inside the object definition, not at the root level. - No trailing commas in JSON — although
.jsonctolerates them, the native components do not use trailing commas. Follow the same style. - `"type"` is always explicit on every property, including nested objects and array items.
Complete annotated example (following native style)
{
"$extends": ["#/$defs/base-component"],
"$componentKey": "PromoBanner",
"$componentTitle": "Promo Banner",
"title": "Promo Banner",
"description": "Display a promotional banner with image and call to action",
"type": "object",
"required": ["title", "image"],
"properties": {
"title": {
"title": "Title",
"type": "string",
},
"subtitle": {
"title": "Subtitle",
"type": "string",
},
"image": {
"title": "Image",
"type": "object",
"properties": {
"src": {
"title": "Image",
"type": "string",
"widget": {
"ui:widget": "media-gallery",
"restrictMediaTypes": {
"video": true,
"image": ["png", "jpg", "jpeg", "gif", "svg", "webp"],
},
},
},
"alt": {
"title": "Alternative Label",
"type": "string",
},
},
},
"link": {
"title": "Call to Action",
"type": "object",
"required": ["text", "url"],
"properties": {
"text": {
"title": "Text",
"type": "string",
},
"url": {
"title": "URL",
"type": "string",
},
"linkTargetBlank": {
"title": "Open link in new window?",
"type": "boolean",
"default": false,
},
},
},
"colorVariant": {
"title": "Color variant",
"type": "string",
"enumNames": ["Main", "Light", "Accent"],
"enum": ["main", "light", "accent"],
},
"showBadge": {
"title": "Show discount badge?",
"type": "boolean",
"default": true,
},
"items": {
"title": "Highlight Items",
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": {
"title": "Item",
"type": "object",
"required": ["label"],
"properties": {
"label": {
"title": "Label",
"type": "string",
},
"icon": {
"title": "Icon",
"type": "string",
"enumNames": ["Truck", "Gift", "Shield Check"],
"enum": ["Truck", "Gift", "ShieldCheck"],
},
},
},
},
},
}Mandatory Workflow for New Custom Sections
Follow this EXACT sequence. Do NOT skip steps.
Phase 1: Planning (BEFORE writing code)
- [ ] Check if similar component exists:
ls src/components/ - [ ] Verify CMS schema names:
ls cms/faststore/components/ - [ ] Choose unique component name (PascalCase)
Phase 2: Component Creation
- [ ] Create folder:
mkdir -p src/components/sections/<Name>(orsrc/components/<Name>/for non-section sub-components) - [ ] Create React component at
src/components/sections/<Name>/<Name>.tsxwith TypeScript interfaces - The root element MUST be
<section className={\section ${styles.mySection}\}>—sectionclass always comes first - Immediately inside the
<section>, add<div className="layout__content">to wrap all content - [ ] Create styles at
src/components/sections/<Name>/<name>.module.scss - Wrap all styles in a single class
- Import as CSS module in the component
- If the section uses
@faststore/uicomponents, import their stylesheets manually in the.module.scss - [ ] RUN LINTER:
ReadLintson new files - [ ] FIX ALL ERRORS before proceeding
Phase 3: CMS Schema & Registration
- [ ] Create JSONC schema at
cms/faststore/components/cms_component__<Name>.jsoncfollowing the Native Component Pattern Reference - [ ] Verify
$componentKeymatches exactly - [ ] Register in
src/components/index.tsxwith an object key identical to$componentKey - [ ] RUN LINTER on
index.tsx
Phase 4: Schema Management (SAME SESSION)
- [ ] Generate:
vtex content generate-schema -o cms/faststore/schema.json - [ ] Verify:
grep -A 5 '"<ComponentName>"' cms/faststore/schema.json(search for the$componentKeystring) - If it does not appear, fix JSONC or registration — do not edit
schema.jsonmanually - [ ] Account check (before upload): read
api.storeIdfromdiscovery.config.jsand runvtex whoami— confirm both match. If they differ, ask the user to runvtex login <correct-account>and stop. - [ ] Ask the user: _"The schema will be uploaded to account `<store-id>`. Do you want to proceed?"_ — wait for confirmation before continuing.
- [ ] Upload:
vtex content upload-schema cms/faststore/schema.json(required — without upload, the CMS editor will not see new or updated section definitions) - [ ] Confirm upload success message
Phase 5: Validation & Deployment
- [ ] No linter errors remain
- [ ] Schema uploaded successfully
- [ ] Component key appears in
schema.json - [ ] Add the section to the desired page via Admin → Storefront → Content (unless your project relies on
pages/*.jsoncand your team's publish process covers composition) - [ ] Document usage (optional but recommended)
🛑 STOP at first error. Fix before proceeding.
Section Scopes
Sections can be scoped to specific page types using "requiredScopes":
{
"$extends": ["#/$defs/base-component"],
"$componentKey": "QuickFilter",
"$componentTitle": "QuickFilter",
"requiredScopes": ["plp", "search"],
"type": "object",
"description": "Quick Filter section for search pages",
"required": [],
"properties": {}
}Section registration ≠ Section rendering
`upload-schema` registers section definitions in Headless CMS (they show up in the editor palette). That is not the same as placing the section on the home page or another route.
To render a new section on a page (typical case):
1. Go to Admin → Storefront → Content (e.g. "All content") 2. Select the page type (e.g., Home, Product List Page) 3. Add the section to that page's layout 4. Save and publish
If your project uses *`cms/faststore/pages/.jsonc` to version page composition, follow that policy — otherwise assume Content** in Admin is where the section gets onto the live page.
In dev mode, page content still comes from the CMS API. If the section is registered in the editor but missing on the storefront, it often was never added to that page's content (or not published).
End-to-end agent workflow
Assume VTEX CLI is installed globally — invoke vtex directly (not npx vtex).
From the project root:
1. Generate (canonical command — matches the storefront skill):
vtex content generate-schema -o cms/faststore/schema.json 2. Validate — for new or renamed sections, grep or read cms/faststore/schema.json and confirm the "$componentKey" is present.
3. Upload (mandatory for the CMS to pick up schema changes):
vtex content upload-schema cms/faststore/schema.jsonThe store ID you enter at prompts should match api.storeId in discovery.config.js.
Pre-upload account verification (MANDATORY)
Before every `upload-schema` execution, the agent must verify that the currently logged-in VTEX account matches the project's target store. Uploading to the wrong account overwrites CMS schema in the wrong store — this is not reversible without manual intervention.
Steps:
1. Read the expected store ID from the project config:
node -e "console.log(require('./discovery.config.js').api.storeId)"2. Read the currently logged-in account from the VTEX CLI:
vtex whoamiThe output includes the account name (e.g. Logged into account: mystore). Extract the account name.
3. Compare the two values. If they do not match, stop immediately and tell the user:
⚠️ The VTEX CLI is logged into account `<logged-account>`, butdiscovery.config.jshasapi.storeIdset to `<expected-store-id>`. Please runvtex login <expected-store-id>or switch to the correct account before uploading.
Do not proceed with upload-schema until the accounts match.
4. Even when accounts match, the agent must ask the user for explicit confirmation before uploading:
The schema will be uploaded to account `<store-id>`. Do you want to proceed? (yes/no)
Wait for the user's response. Only proceed if the user confirms.
Non-interactive upload (automatic - USE ONLY AFTER ACCOUNT VERIFICATION)
When uploading schema in an automated workflow, ALWAYS use expect to handle prompts automatically. This block must only run after the pre-upload account verification above has passed and the user has confirmed.
# Export store ID from discovery.config.js
export STORE_ID=$(node -e "console.log(require('./discovery.config.js').api.storeId)")
# Run upload with expect to auto-answer prompts
# IMPORTANT: use single quotes so Tcl does not misinterpret $ tokens
# (e.g. $id) that appear in CLI output. $env(STORE_ID) is Tcl syntax
# evaluated by the Tcl interpreter, not by bash.
expect -c '
spawn vtex content upload-schema cms/faststore/schema.json
expect "store ID"
send "faststore\r"
expect -re "uploaded|confirm"
send "y\r"
expect -re "Are you sure|confirm"
send "y\r"
expect eof
' 2>&1Never use double quotes around the expect -c argument — CLI output often contains $id and other $-prefixed tokens that Tcl interprets as variable references inside double-quoted strings, causing can't read "id": no such variable errors. Single quotes pass the script literally to Tcl, where $env(STORE_ID) is evaluated correctly by the Tcl interpreter.
Agents must report the exact prompt or error if login, workspace, store ID, or confirmation blocks upload, and tell the human the next step (vtex login, correct account, etc.).
Without `upload-schema`, new or updated sections will not appear in the Headless CMS editor.
Important: CMS Sections Only Receive CMS-Defined Props
CMS sections receive props from the schema properties defined in their .jsonc file — these are the fields the editor fills in the CMS admin.
Do NOT expect sections to receive props passed programmatically from parent page components. If a section needs data beyond what the CMS editor provides (e.g., product data, search results), it must read from:
- Page context hooks (
usePDP(),usePLP(),usePage(), etc.) - Custom GraphQL queries via
useQuery/useLazyQuery
FastStore GraphQL API Extensions
FastStore exposes a GraphQL BFF layer that proxies between your storefront and the VTEX platform APIs. There are two extension mechanisms:
1. VTEX extensions (src/graphql/vtex/) — Extend existing FastStore API types (e.g., StoreProduct) with additional fields resolved from VTEX root data already available in the resolver context. 2. Third-party extensions (src/graphql/thirdParty/) — Define entirely new types, queries, and mutations that call external APIs.
Directory Structure
src/graphql/
├── vtex/ # Extensions to existing FastStore/VTEX types
│ ├── typeDefs/
│ │ └── product.graphql # Schema extensions (extend type StoreProduct, etc.)
│ └── resolvers/
│ ├── product.ts # Resolver for extended fields
│ └── index.ts # Aggregates all VTEX resolvers
└── thirdParty/ # New types, queries, and mutations from external APIs
├── typeDefs/
│ ├── query.graphql
│ └── contactForm.graphql
└── resolvers/
├── queries.ts
├── contactForm.ts
└── index.ts---
Extending Existing VTEX Types
Use when you need to add fields to types FastStore already provides (e.g., adding installment data to StoreProduct).
Step 1 — Define the Schema Extension
# src/graphql/vtex/typeDefs/product.graphql
type Installments {
installmentPaymentSystemName: String!
installmentValue: Float!
installmentInterest: Float!
installmentNumber: Float!
}
extend type StoreProduct {
availableInstallments: [Installments!]!
}Use extend type <ExistingType> to add fields to FastStore's built-in types.
Step 2 — Create the Resolver
// src/graphql/vtex/resolvers/product.ts
import type { StoreProductRoot } from "@faststore/core/api";
const productResolver = {
StoreProduct: {
availableInstallments: (root: StoreProductRoot) => {
const installments = root.sellers?.[0]?.commertialOffer?.Installments;
if (!installments?.length) return [];
return installments.map((installment) => ({
installmentPaymentSystemName: installment.PaymentSystemName,
installmentValue: installment.Value,
installmentInterest: installment.InterestRate,
installmentNumber: installment.NumberOfInstallments,
}));
},
},
};
export default productResolver;Import root types from @faststore/core/api (e.g., StoreProductRoot, StoreCollectionRoot).
Step 3 — Register the Resolver
// src/graphql/vtex/resolvers/index.ts
import { default as StoreProductResolver } from "./product";
const resolvers = { ...StoreProductResolver };
export default resolvers;Step 4 — Add Fragments to Include New Fields in Page Queries
Fragment filenames must match the query they extend:
// src/fragments/ServerProduct.ts
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ServerProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);// src/fragments/ClientProduct.ts — must mirror ServerProduct
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ClientProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);Fragment Naming Convention
| Filename | Extends query for |
|---|---|
ServerProduct.ts | Server-side PDP query |
ClientProduct.ts | Client-side PDP query |
ClientProductGallery.ts | Client-side PLP query |
ClientManyProducts.ts | — |
ClientSearchSuggestions.ts | Search autocomplete query |
ClientShippingSimulation.ts | Shipping simulation query |
ClientTopSearchSuggestions.ts | Top search query |
ClientCollectionPage.ts | Client-side PLP/collection query |
ServerCollectionPage.ts | Server-side PLP/collection query |
Consuming VTEX Extensions in React
VTEX type extensions are automatically available through FastStore's built-in page hooks — no extra client-side query needed:
import { usePDP } from "@faststore/core";
function ProductInstallments() {
const context = usePDP();
const installment = context?.data?.product?.availableInstallments[0];
if (!installment || installment.installmentInterest !== 0) return null;
return (
<span>
{installment.installmentNumber} interest-free installments
of ${installment.installmentValue}
</span>
);
}---
Third-Party Extensions — New Queries
Use when you need to fetch data from external (non-VTEX) APIs.
Step 1 — Define the Schema
# src/graphql/thirdParty/typeDefs/query.graphql
type CEP {
cep: String!
logradouro: String!
bairro: String!
localidade: String!
uf: String!
estado: String!
regiao: String!
ddd: String!
complemento: String
unidade: String
ibge: String
gia: String
siafi: String
}
extend type Query {
searchCEP(CEP: String!): CEP!
}Use extend type Query to add new query fields.
Step 2 — Create the Resolver
// src/graphql/thirdParty/resolvers/queries.ts
import { Query } from "@faststore/core/api";
export default {
Query: {
searchCEP: async (_: unknown, { CEP }: { CEP: string }): Promise<Query["searchCEP"]> => {
const resp = await fetch(`http://viacep.com.br/ws/${CEP}/json`);
return resp.json();
},
},
};Step 3 — Register
// src/graphql/thirdParty/resolvers/index.ts
import queriesResolver from "./queries";
const resolvers = { ...queriesResolver };
export default resolvers;---
Third-Party Extensions — New Mutations
Use when you need to send data to external services (form submissions, writes, etc.).
Step 1 — Define the Schema
# src/graphql/thirdParty/typeDefs/contactForm.graphql
type ContactFormResponse {
message: String!
}
input ContactFormInput {
name: String!
email: String!
subject: String!
message: String!
}
# Use `type Mutation` for the first mutation definition,
# `extend type Mutation` if another file already defines it.
type Mutation {
submitContactForm(input: ContactFormInput!): ContactFormResponse
}Step 2 — Create the Resolver
// src/graphql/thirdParty/resolvers/contactForm.ts
type SubmitContactFormData = {
input: { name: string; email: string; subject?: string; message: string };
};
const contactFormResolver = {
Mutation: {
submitContactForm: async (_: never, data: SubmitContactFormData) => {
const { input } = data;
try {
const response = await fetch("https://your-api-endpoint.com/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!response.ok) throw new Error("Error while sending the message");
return { message: "Your message was sent successfully!" };
} catch (error) {
return { message: error };
}
},
},
};
export default contactFormResolver;---
Consuming Custom Queries and Mutations in React
Third-party queries and mutations require explicit hooks from @faststore/core/experimental.
Imports
import { gql } from "@faststore/core/api";
import { useQuery_unstable as useQuery } from "@faststore/core/experimental";
import { useLazyQuery_unstable as useLazyQuery } from "@faststore/core/experimental";useQuery — Auto-Executing Queries
Fires on mount, re-executes when variables change. SWR-powered (caching, revalidation, deduplication).
const SEARCH_CEP_QUERY = gql`
query getCEPQuery($cep: String!) {
searchCEP(CEP: $cep) {
logradouro
bairro
localidade
uf
}
}
`;
function AddressLookup({ cep }: { cep: string }) {
const { data, error } = useQuery(SEARCH_CEP_QUERY, { cep });
if (error) return <p>Failed to load.</p>;
if (!data) return <p>Loading...</p>;
return <address>{data.searchCEP.logradouro}, {data.searchCEP.localidade}</address>;
}useLazyQuery — Deferred / Imperative Execution
Returns [execute, response]. Use for mutations and user-triggered queries.
const SUBMIT_CONTACT_FORM = gql`
mutation SubmitContactForm($input: ContactFormInput!) {
submitContactForm(input: $input) { message }
}
`;
function ContactForm() {
const [execute, { data, error }] = useLazyQuery(SUBMIT_CONTACT_FORM, {
input: { name: "", email: "", subject: "", message: "" },
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await execute({ input: formData });
};
// ...
}gql Tag Rules
1. Operation names matter — queries must end with Query (e.g., getCEPQuery) for FastStore to use HTTP GET; mutations default to POST 2. One operation per `gql` tag 3. `gql` calls must be at module scope — not inside component bodies
---
Quick Reference
| Goal | Schema location | Resolver location | Consumption |
|---|---|---|---|
| Add fields to existing FastStore type | src/graphql/vtex/typeDefs/*.graphql | src/graphql/vtex/resolvers/ | usePDP(), usePLP(), etc. |
| New query to external API | src/graphql/thirdParty/typeDefs/*.graphql | src/graphql/thirdParty/resolvers/ | useQuery |
| New mutation to external API | src/graphql/thirdParty/typeDefs/*.graphql | src/graphql/thirdParty/resolvers/ | useLazyQuery |
Checklist
1. Create the .graphql schema file in the appropriate typeDefs/ directory 2. Create the TypeScript resolver file in the matching resolvers/ directory 3. Register the resolver by spreading it into the corresponding resolvers/index.ts 4. For third-party queries/mutations: define a gql-tagged operation at module scope in your component 5. Use useQuery (auto-execute) or useLazyQuery (execute on demand) to consume the data 6. Restart the dev server — schema changes require a rebuild to regenerate types
FastStore v3 → v4 Migration
A reusable guide for migrating any VTEX FastStore storefront from v3 to v4. Written from two real migrations; all store/account names below are placeholders — substitute the values for the store being migrated.
Conventions used here:
<store>— the storefront repo being migrated.<store>is assumed to be a yarn-workspaces monorepo with the FastStore app
under packages/discovery (a single-package store has the same files at the repo root — adjust paths accordingly).
@vtex/faststore-plugin-buyer-portalis the B2B plugin; a store without it
simply skips every plugin-related step.
---
0. Outcome & the two install modes
The goal: yarn build and yarn dev both succeed on FastStore v4.
Keep these two setups separate:
- Deployable install (what gets committed / what CI and production use) —
package.json pins published package versions; a plain yarn install succeeds with no symlinks and no sibling checkouts. This is the committed state.
- Local multi-repo testing (optional, never committed) — validating the
store against unpublished faststore / plugin source via a symlink layer applied on top of a normal install. See §9. Revert to the deployable state before committing.
---
1. Node 24 is mandatory
FastStore v4's dependency tree (e.g. eslint-visitor-keys@5) declares engines.node of >=20.19 || >=22.13 || >=24. A plain yarn install on an older Node (e.g. 20.12) fails with Found incompatible module.
- Use Node 24 for install, build and dev.
- Set
volta.nodeto"24.0.2"(or the latest Node 24 patch) inpackages/discovery/package.json. - Set
experimental.nodeVersion: 24indiscovery.config.js.
---
2. Deployable package.json
The store declares only the VTEX packages and the framework — never hand-list @faststore/core's transitive dependencies; they arrive through @faststore/cli.
packages/discovery/package.json → dependencies (keep any store-specific app dependencies — e.g. crypto-js, draft-js — alongside these):
{
"@faststore/cli": "<published v4 release>",
"@vtex/faststore-plugin-buyer-portal": "<published v4 release>",
"graphql": "^16.11.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.24.1"
}Important: do not declare next in dependencies — Next.js is now managed internally by @faststore/cli, and declaring it separately causes version conflicts. Also update typescript in devDependencies to ^5.9.3.
graphql must be declared explicitly because it is a peerDependency of @faststore/cli — yarn v1 does not install peer dependencies automatically.
Root package.json: @faststore/cli (devDependency) + @vtex/faststore-plugin-buyer-portal (dependency).
Monorepo stores only: add @inquirer/type to the resolutions field in the root package.json to avoid version conflicts with the inquirer package:
{
"resolutions": {
"@inquirer/type": "^1.5.5"
}
}Pin npm releases, not pkg.pr.new / pkg.csb.dev tarballs. The faststore v4 monorepo uses pnpm catalog: / workspace:* specs. npm publish (via pnpm publish) resolves these to concrete versions, so npm releases install cleanly. pkg.pr.new tarballs do NOT — they keep catalog: unresolved and yarn install fails with Couldn't find any versions ... matches "catalog:".
@faststore/cli transitively brings @faststore/core, which brings @faststore/ui, @faststore/api, @faststore/sdk, @faststore/diagnostics, @faststore/lighthouse, @faststore/components, plus the v4 third-party tree (Next 16, GraphQL 16, etc.).
Remove every v3-only hand-listed transitive dependency from
packages/discovery/package.json— they now arrive via@faststore/cli.
---
3. discovery.config.js must be a plain config
v3 stores often start discovery.config.js with Node code:
const path = require("path");
const dotenv = require("dotenv");
dotenv.config({ path: path.resolve(__dirname, ".env") });In v4 this file is pulled into the client/instrumentation bundle, where webpack cannot resolve Node built-ins — Module not found: Can't resolve 'path' from dotenv — and routes 500.
- Remove the
require("path")/require("dotenv")/dotenv.config()
lines. Next.js loads .env / .env.local automatically; reading process.env.NEXT_PUBLIC_* directly is fine.
- Remove any v3 `webpack()` callback. FastStore v4 core owns the webpack
config; in v3, core never invoked storeConfig.webpack, so that callback was already dead code. (Do not try to re-enable it — see §7.)
- Set
experimental.nodeVersion: 24. - If the store consumes linked/transpiled local packages, add
experimental.transpilePackages: [ ... ].
---
4. SCSS migration: @import → @use / @forward
v4 FastStore uses the Dart Sass module system. @import is deprecated (removed in Dart Sass 3.0). Treat this as a repo-wide pass over every .scss file in the store.
4.1 Namespaced mixins & functions
Shared mixins/functions are reached through a namespaced module:
@use "@faststore/ui/src/styles/base/utilities" as u; // FIRST line(s) of the file
[data-fs-foo] {
@include u.media(">=notebook") { ... } // not @include media(...)
top: u.rem(9px); // not rem(9px)
@include u.layout-content; // not @include layout-content
}- All
@userules must come before every other rule (including CSS
@import and selectors). Group them at the very top of the file.
- A bare
@use "<file>";still emits the loaded file's CSS; only its Sass
members become namespaced. Add as <ns> only when you need its members.
4.2 Top-level @import → @use
@import "~pkg/themes/_buttons.scss"; // before
@use "pkg/themes/_buttons.scss"; // after — drop the legacy `~` prefixPlain CSS @import (URL ending in .css) is not deprecated — leave those as-is.
4.3 Nested @import must STAY @import
A @use rule cannot be nested inside a selector. The naive fix (hoist the import to the top level) breaks CSS-Modules files (*.module.scss): hoisting @use ".../Loader/styles.scss" lifts [data-fs-loader] to the top level and css-loader rejects it — Selector "[data-fs-loader]" is not pure. @use-ing several .../styles.scss files also collides on the default namespace styles (There's already a module with namespace "styles").
So the rule is:
- Top-level `@import` → `@use`.
- Nested `@import` (inside a selector) → leave as `@import`. Dart Sass still
supports it (deprecation warning only), and keeping it nested preserves the local-class scoping that makes the inner selectors "pure". A 100% @import purge is not achievable for CSS-Modules files — and that is fine.
4.4 Custom breakpoints → standard FastStore breakpoints
v3 stores override include-media breakpoints by re-declaring a global $breakpoints map (typically in a custom-mixins.scss). That global-shadowing trick is dead under the module system: include-media is configured once by @faststore/ui/.../utilities.scss (@use "~include-media" with (...)) and cannot be reconfigured.
Fix: drop the non-standard breakpoints from the theme and remap each media() call to the nearest standard FastStore breakpoint (phone, phonemid, tablet, notebook, desktop). Example remaps: phonelg → tablet, notebooksm → notebook. Do not edit @faststore/ui's breakpoint map — it is shared, and the store's CMS schema only knows the standard breakpoints. A leftover custom-mixins.scss becomes a thin @forward "@faststore/ui/src/styles/base/utilities";.
4.5 "Module already loaded" — dead / duplicate imports
This module was already loaded, so it can't be configured using "with".utilities.scss does @use "~include-media" with (...); loading it as two different module instances configures include-media twice. The usual cause is a dead theme partial (e.g. an _base.scss that only @uses utilities and re-declares $breakpoints — a no-op in v4) imported through a different specifier than the rest of the store. Fix: drop the dead @use — verify the partial actually contributes CSS/members before keeping it.
4.6 Per-file checklist
1. Move/add @use "@faststore/ui/src/styles/base/utilities" as u; to the top. 2. @include media(...) → @include u.media(...); @include layout-content → @include u.layout-content; the FastStore rem(...) function → u.rem(...). 3. Top-level @import → @use (drop ~); nested @import stays (§4.3). 4. Remap non-standard breakpoints (§4.4); drop dead imports (§4.5). 5. Rebuild and check the Sass output.
---
5. GraphQL import migration
@faststore/graphql-utils is deprecated in v4. The gql tag used for GraphQL documents must be imported from @faststore/core/api instead.
Search for all usages in src/:
grep -r "faststore/graphql-utils" src/Replace every occurrence:
// before
import { gql } from '@faststore/graphql-utils'
// after
import { gql } from '@faststore/core/api'If the grep returns no matches, skip this step.
---
6. v3 patches
patch-package patches are version-tagged (@faststore+core+<v3>.patch) and will not apply to v4. Inspect each:
- Debug / instrumentation-only patches (verbose logging gated on an env
flag, pass-through when off) — move out of patches/ to a sibling folder such as ../.patches-disabled-v3/; nothing to reapply.
- Functional patches — re-evaluate whether v4 still needs the fix; if so,
recreate it against the v4 package.
patch-package scans patches/ recursively, so a patches/.disabled/ subfolder is still picked up — move stale patches outside patches/.
---
6. Tooling gotchas
- corepack signature error for
pnpm/yarn: prefix commands with
COREPACK_INTEGRITY_KEYS=0 (an outdated corepack can't verify newer signatures).
- turbo + nested git worktree: turbo walks up past a worktree (whose
.git
is a file) and mis-detects the repo root, so a root turbo build reports 0 tasks. Build the store package directly: cd packages/discovery && yarn build. Normal checkouts are unaffected.
---
7. @vtex/diagnostics-nodejs optional peers
@vtex/diagnostics-nodejs imports instrumentation for server frameworks FastStore does not use (@opentelemetry/instrumentation-koa, @opentelemetry/instrumentation-nestjs-core, @nestjs/core, fastify-plugin). They are optional peers and are not installed.
- In
next buildthey are harmless warnings — the server bundle externalises
node_modules, so the build succeeds.
- In
next devthey can become fatalModule not founderrors and 500 the
page.
If next dev 500s on this, stub the missing modules to false in webpack. The store's discovery.config.js webpack() callback is not invoked by v4 core, so the alias must live in @faststore/core's own packages/core/next.config.js webpack() callback (only relevant when running a linked local faststore clone — see §9):
config.resolve.alias = {
...config.resolve.alias,
'@opentelemetry/instrumentation-koa': false,
'@opentelemetry/instrumentation-nestjs-core': false,
'@opentelemetry/instrumentation-fastify': false,
'@opentelemetry/instrumentation-express': false,
'@nestjs/core': false,
'fastify-plugin': false,
}Do not makecore/next.config.jsforwardstoreConfig.webpack— that
applies the store callback to the instrumentation compilation and breaksthe instrumentation hook. Put shared webpack fixes directly in core's
callback.
---
8. Verification
1. cd packages/discovery && COREPACK_INTEGRITY_KEYS=0 yarn build — expect generate + GraphQL codegen + next build to succeed, routes printed, .next copied. No @import errors (deprecation warnings from nested imports are expected), no "module already loaded", no "not pure" selectors. 2. COREPACK_INTEGRITY_KEYS=0 yarn dev — homepage 200, POST /api/graphql → {"data":{"__typename":"Query"}} 200, private routes redirect (30x) to login. 3. Spot-check responsive styling (the remapped breakpoints) and any placeholder-@extend buttons.
---
9. Optional: local multi-repo testing (never committed)
Needed only while validating the store against unpublished faststore or plugin source (e.g. the v4 branches before they are released). Skip entirely once published v4 versions exist — which is the normal, committed state.
The technique: after a normal yarn install, overlay symlinks so the store and the plugin resolve @faststore/* to a local faststore monorepo checkout:
- Symlink every
@faststore/*package (`api cli core components diagnostics
graphql-utils lighthouse sdk ui) into the store's node_modules **and** the plugin's node_modules (otherwise the plugin pulls its own @faststore/ui and you hit the §4.5 duplicate-utilities.scss` error).
- Repoint the CLI bin:
node_modules/.bin/faststore → ../@faststore/cli/bin/run.js
(the v4 path is bin/run.js, was bin/run in v3).
- Dedupe singletons (
graphql,react,react-dom) to the single copy the
faststore packages share, or yarn dev fails with Duplicate "graphql" modules / React "invalid hook call".
- Use the
link:protocol for the plugin (link:survivesyarn install;
yarn link does not).
Build the faststore monorepo first (pnpm install && pnpm build). Drive the symlink overlay from an idempotent, `postinstall`-safe script (exits 0 when sibling checkouts are absent, so deploy/CI is unaffected).
Before committing, revert to the deployable state: pin published versions in package.json, remove any link: entries / postinstall hook / link script, and rm -rf node_modules && yarn install so no symlinks remain.
---
Appendix — migration checklist
- [ ] Node 24 (
volta.nodemust be a full semver e.g. "24.0.2",experimental.nodeVersion: 24). - [ ]
package.json(root + discovery):@faststore/cli+ plugin pinned to
published v4 releases; v3 transitive deps removed; next removed from dependencies (managed by cli); graphql ^16.11.0 and react-router-dom ^6.24.1 added; typescript ^5.9.3 in devDependencies.
- [ ]
discovery.config.js: nopath/dotenvrequires; nowebpack()
callback; nodeVersion: 24; transpilePackages if needed.
- [ ] Every
.scss: top-level@import→@use; nested@importkept;
@include media/layout-content namespaced to u.*; non-standard breakpoints remapped to standard ones; dead theme imports dropped.
- [ ]
custom-mixins.scss(if present) → thin@forwardof utilities. - [ ] v3
patch-packagepatches assessed and stale ones moved out of
patches/.
- [ ]
yarn buildandyarn devverified (§8). - [ ] Local-linking machinery (§9) reverted before committing.
- [ ] CMS type detected via
contentSourceindiscovery.config.js(§11.1). - [ ] CMS sync instructions shown to user (§10 next steps): case detected from
discovery.config.js+cms/faststore/; commands presented for manual execution (never run automatically). - [ ] New CMS fields configured in Admin → Storefront → Headless CMS / Content and pages republished (§11.4 — human step).
- [ ] Tested locally with
yarn dev— no empty labels/buttons/toasts. - [ ] Only then: v4 deployed to production + Node.js v24 set in WebOps.
---
10. Post-migration summary (mandatory output)
**MANDATORY prerequisite — do NOT display this summary until yarn buildpasses.**
>
Before showing the summary, run (using Node 24):
```bash
yarn install
yarn build
```
Fix any build errors first. Only after a successful build should you
proceed to display the summary below.
After the build passes, display the following two sections.
---
What was done
A table covering every file touched and the change applied. Adapt rows to what actually changed; mark items that were not applicable as —.
| File | Change | Status |
|---|---|---|
package.json | @faststore/cli bumped to v4; next removed; graphql, react-router-dom added; typescript bumped to ^5.9.3; volta.node set to 24 | ✅ Done |
discovery.config.js | experimental.nodeVersion → 24 | ✅ Done |
src/**/*.scss | Top-level @import → @use; @include media/layout-content namespaced to u.* | ✅ Done |
patches/ | Stale v3 patches assessed / moved | ✅ Done / N/A |
---
Important next steps
1. CMS sync (run manually in your terminal)
Do NOT run `vtex content` commands automatically. These require
interactive authentication and may have CLI plugin issues in Homebrew
environments.
Check discovery.config.js and cms/faststore/ to identify the case, then present the matching instructions to the user.
Headless CMS (legacy) — contentSource field absent in discovery.config.js:
vtex login <accountName>
yarn cms-syncIfcms-syncerrors withCannot find module 'vtex', run
vtex plugins install @vtex/cli-plugin-cmsorvtex update.
Content Platform (CP) — contentSource: { type: 'CP' } present. Identify the sub-case by inspecting cms/faststore/:
| Case | Signal | Commands to run |
|---|---|---|
| A — no custom schemas | No .jsonc files, no components/ folder | Create cms/faststore/schema.json with { "$base": "vtex.faststore" }, then vtex content upload-schema cms/faststore/schema.json |
| B — already split | cms/faststore/components/*.jsonc exists | vtex content generate-schema cms/faststore/components cms/faststore/pages -o cms/faststore/schema.json then vtex content upload-schema cms/faststore/schema.json |
| C — legacy format | Only sections.json / content-types.json | Split first (see §11), then generate + upload |
For the full command listing of each case see §11.
---
2. Fill in new CMS fields and republish
After the CMS sync, v4 exposes new configurable fields that were previously hardcoded. Configure them in Admin → Storefront → Headless CMS / Content and republish the affected pages:
| Page | Fields to configure |
|---|---|
| All pages | Navbar → invalidQuantityToast, collapseSearchAriaLabel |
| Home (product shelf) | ProductCard / ProductCardContent → buttonLabel, outOfStockLabel, includeTaxesLabel, sponsoredLabel |
| PLP | Breadcrumb → Fallback label, ProductGallery → sortBySelector, Filter → FilterSlider / FilterDesktop labels, ProductCard / ProductCardContent |
| Search | SearchInput, SearchTop, SearchHistory, EmptyGallery → labels, ProductCard / ProductCardContent |
| PDP | Breadcrumb → Fallback label, ProductDetails → invalidQuantityToast / buyButtonTitle |
| Cart | EmptyCart → title / buttonLabel |
Full field reference: developers.vtex.com → Upgrading FastStore to v4
Do not deploy v4 to production before filling these fields — they render
blank until configured.
---
3. Update Node.js v24 in WebOps
In VTEX Admin → Storefront → FastStore WebOps → Settings → Node.js version → set to v24 → Save → trigger a new deploy.
---
4. Check Sass @import deprecation warnings
@import inside selector blocks emits Dart Sass deprecation warnings.
---
11. CMS sync — command reference
This section is a command reference. The agent must not run these commands automatically — always present them to the user to run manually.
11.1 Headless CMS (legacy)
vtex login <accountName>
yarn cms-synccms-sync is safe while v3 is live — it only pushes the schema.
11.2 Content Platform — Case A (no custom schemas)
# 1. Create the minimal schema (if not already present)
# cms/faststore/schema.json content:
# { "$base": "vtex.faststore" }
# (use "vtex.faststore@4.1.0" to pin an explicit version)
vtex login <accountName>
vtex content upload-schema cms/faststore/schema.json
# The local schema.json can be deleted after upload11.3 Content Platform — Case B (components already in .jsonc format)
vtex login <accountName>
vtex content generate-schema cms/faststore/components cms/faststore/pages \
-o cms/faststore/schema.json
vtex content upload-schema cms/faststore/schema.json11.4 Content Platform — Case C (legacy sections.json)
vtex login <accountName>
vtex content split-components -i cms/faststore/sections.json \
-o cms/faststore/components
vtex content split-content-types -i cms/faststore/content-types.json \
-s cms/faststore/sections.json \
-o cms/faststore/pages
vtex content generate-schema cms/faststore/components cms/faststore/pages \
-o cms/faststore/schema.json
vtex content upload-schema cms/faststore/schema.jsonFastStore GraphQL BFF — API Reference
FastStore exposes a GraphQL BFF (Back-For-Front) layer that proxies between your storefront and VTEX platform APIs.
The full type reference is in references/REFERENCE.md.
Root Query Fields (Summary)
| Field | Return Type | Description |
|---|---|---|
product(locator) | StoreProduct! | Product details by locator |
collection(slug) | StoreCollection! | Collection details by slug |
search(first, after, sort, term, selectedFacets) | StoreSearchResult! | Product/facet/suggestion search |
allProducts(first, after) | StoreProductConnection! | All products |
products(productIds) | [StoreProduct!]! | Products by IDs |
allCollections(first, after) | StoreCollectionConnection! | All collections |
shipping(items, postalCode, country) | ShippingData | Shipping simulation |
sellers(postalCode, country, ...) | SellersData | Available sellers |
profile(id) | Profile | Profile information |
userOrder(orderId) | UserOrderResult | Order details (auth required) |
listUserOrders(...) | UserOrderListMinimalResult | Order list (auth required) |
userDetails | StoreUserDetails! | Current user details (auth required) |
pickupPoints(geoCoordinates) | PickupPoints | Nearby pickup points |
Root Mutation Fields (Summary)
| Field | Return Type | Description |
|---|---|---|
validateCart(cart, session) | StoreCart | Validate/sync cart with platform |
validateSession(session, search) | StoreSession | Update web session |
subscribeToNewsletter(data) | PersonNewsletter | Newsletter subscription |
cancelOrder(data) | UserOrderCancel | Cancel a user order |
Key Types at a Glance
- `StoreProduct` — A VTEX SKU. Contains
name,sku,slug,brand,offers,image,description,seo,additionalProperty,isVariantOf, and more. - `StoreOffer` — Offer for a product from a seller. Contains
price,sellingPrice,listPrice,availability,seller,quantity. - `StoreSearchResult` — Search results with
products,facets,suggestions, andmetadata. - `StoreSession` — Session state:
locale,currency,country,channel,person,postalCode. - `StoreCart` — Shopping cart:
order(withacceptedOfferarray) andmessages. - `StoreFacetBoolean` / `StoreFacetRange` — Search facets with keys, labels, and values.
Runtime shape vs Schema shape
GraphQL responses may differ from the schema definition:
- Union/interface types add
__typenameat runtime (e.g.,StoreFacet→__typename: "StoreFacetBoolean"or"StoreFacetRange"). The schema fieldtype: StoreFacetTypemay NOT appear in the response if the query uses inline fragments (... on) instead of requestingtypedirectly.
Common example:
// ❌ Wrong — `type` field may not exist at runtime
const booleanFacets = facets.filter((f) => f.type === "BOOLEAN");
// ✅ Correct — use __typename from the GraphQL response
const booleanFacets = facets.filter(
(f) => f.__typename === "StoreFacetBoolean",
);Using the gql tag
The gql tag from @faststore/core/api is statically extracted at build time by the FastStore CLI pipeline. It has specific usage restrictions:
✅ Where gql works:
- API extension fragments in
src/fragments/(e.g.,ServerProduct.ts,ClientProduct.ts) - Third-party mutations/queries in
src/graphql/thirdParty/
❌ Where gql does NOT work:
- Inside custom section components for standalone queries against built-in root queries (
search,product,collection)
Why it fails in components:
The FastStore CLI's GraphQL optimization step runs at build time and only processes gql tags in specific locations. Using it in a component for a new query will break the build with:
"GraphQL was not optimized and TS files were not updated"✅ Correct approach for custom sections:
Read data from page context hooks instead of creating new queries:
// ❌ Wrong — will break the build
import { gql } from "@faststore/core/api";
export default function MySection() {
const query = gql(`
query MySearch($term: String!) {
search(term: $term) { ... }
}
`);
// This will fail at build time
}
// ✅ Correct — read from page context
import { usePage } from "@faststore/core";
export default function MySection() {
const context = usePage<PLPContext>();
const searchData = context?.data?.search;
// Data is already available from the page query
}See references/section-overrides-and-custom-sections.md for more details on creating custom sections that consume data from page context.
Extending Types
Use extend type <TypeName> in src/graphql/vtex/typeDefs/*.graphql to add fields to any built-in type:
extend type StoreProduct {
customField: String!
}See extending-graphql-with-custom-resolvers for the complete extension guide.
Sort Options (StoreSort enum)
| Value | Description |
|---|---|
price_desc | Price: high to low |
price_asc | Price: low to high |
orders_desc | Most orders first |
name_desc | Name: Z to A |
name_asc | Name: A to Z |
release_desc | Newest first |
discount_desc | Biggest discount first |
score_desc | Best score first |
Full Type Reference
See references/REFERENCE.md for the complete field-by-field reference for all types.
API Extensions — GraphQL
FastStore provides two extension mechanisms for GraphQL:
1. VTEX extensions (src/graphql/vtex/) — extend the existing FastStore API types with new fields. The resolvers have access to the VTEX platform data that comes from the root object. 2. Third-party extensions (src/graphql/thirdParty/) — define entirely new types, queries, and mutations that call external APIs.
###1 Extending the VTEX Schema (adding fields to existing types)
Step 1: Define the new type and extend the existing type
# src/graphql/vtex/typeDefs/product.graphql
# Extends the native StoreProduct type with installment data.
# The "extend type" syntax adds fields to an existing FastStore API type.
type Installments {
installmentPaymentSystemName: String!
installmentValue: Float!
installmentInterest: Float!
installmentNumber: Float!
}
extend type StoreProduct {
"""
Retrieve available installments data extending StoreProduct
"""
availableInstallments: [Installments!]!
}Step 2: Write the resolver
// src/graphql/vtex/resolvers/product.ts
// Resolves the "availableInstallments" field added to StoreProduct.
// The `root` parameter contains the raw VTEX catalog data for the product,
// which includes seller information, commercial offers, and installment plans.
import type { StoreProductRoot } from "@faststore/core/api";
// StoreProductRoot: TypeScript type representing the raw VTEX product data
// that FastStore passes to StoreProduct resolvers.
const productResolver = {
StoreProduct: {
availableInstallments: (root: StoreProductRoot) => {
// Access installments from the first seller's commercial offer.
// The VTEX API nests this data under sellers[].commertialOffer.Installments.
// <!-- TODO: "commertialOffer" appears to be a known typo in the VTEX API
// (should be "commercialOffer"). Confirm this is intentional and not a bug. -->
const installments = root.sellers?.[0]?.commertialOffer?.Installments;
if (!installments.length) {
return [];
}
// Map the raw VTEX installment shape to our GraphQL schema shape.
return installments.map((installment) => ({
installmentPaymentSystemName: installment.PaymentSystemName,
installmentValue: installment.Value,
installmentInterest: installment.InterestRate,
installmentNumber: installment.NumberOfInstallments,
}));
},
},
};
export default productResolver;Step 3: Export from the resolver index
// src/graphql/vtex/resolvers/index.ts
// Aggregates all VTEX API extension resolvers into a single export.
// FastStore CLI reads this file to merge resolvers into the API.
import { default as StoreProductResolver } from "./product";
const resolvers = {
...StoreProductResolver,
};
export default resolvers;Step 4: Add fragments to include the new fields in page queries
FastStore uses a fragment-based system to extend the data fetched on each page. Fragment filenames must match the query they extend.
// src/fragments/ServerProduct.ts
// Server-side fragment: included in the initial server-rendered HTML.
// The filename "ServerProduct" tells FastStore to merge this fragment
// into the server-side product query for the PDP.
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ServerProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);
// Docs: https://developers.vtex.com/docs/guides/faststore/api-extensions-extending-queries-using-fragments// src/fragments/ClientProduct.ts
// Client-side fragment: used for client-side data fetching (e.g., SWR revalidation).
// Must include the same fields as the server fragment so client and server data match.
import { gql } from "@faststore/core/api";
export const fragment = gql(`
fragment ClientProduct on Query {
product(locator: $locator) {
availableInstallments {
installmentPaymentSystemName
installmentValue
installmentInterest
installmentNumber
}
}
}
`);Fragment naming convention
| Filename | Extends query for |
|---|---|
ServerProduct.ts | Server-side PDP query |
ClientProduct.ts | Client-side PDP query |
ClientProductGallery.ts | Client-side PLP query |
ClientManyProducts.ts | |
ClientSearchSuggestions.ts | Search autocomplete query |
ClientShippingSimulation.ts | Shipping simulation query |
ClientTopSearchSuggestions.ts | Top search query |
ClientCollectionPage.ts | Client-side PLP/collection query |
ClientTopSearchSuggestions.ts | Top search suggestions query |
ServerCollectionPage.ts | Server-side PLP/collection query |
ServerProduct.ts | Server-side product query |
###2 Third-Party Extensions (new types and mutations)
Step 1: Define new GraphQL types
# src/graphql/thirdParty/typeDefs/contactForm.graphql
# Defines a completely new mutation for submitting a contact form.
# This is NOT extending an existing type — it's a new root Mutation field.
type ContactFormResponse {
message: String!
}
input ContactFormInput {
name: String!
email: String!
subject: String!
message: String!
}
type Mutation {
submitContactForm(input: ContactFormInput!): ContactFormResponse
}Step 2: Write the resolver
// src/graphql/thirdParty/resolvers/contactForm.ts
// Server-side resolver for the submitContactForm mutation.
// This runs on the Node.js server, so it can make authenticated API calls
// that shouldn't be exposed to the browser.
type SubmitContactFormData = {
input: {
name: string;
email: string;
subject?: string;
message: string;
};
};
const contactFormResolver = {
Mutation: {
submitContactForm: async (_: never, data: SubmitContactFormData) => {
const { input } = data;
try {
// POST to the VTEX Master Data API (Data Entities).
// This is a server-side call — the API key/credentials are managed
// by the VTEX platform, not exposed to the client.
// <!-- TODO: The URL is hardcoded to "playground.vtexcommercestable.com.br".
// In production, this should be dynamic based on discovery.config.js api settings.
// Confirm if there's a recommended way to access the store config from resolvers. -->
const response = await fetch(
"https://playground.vtexcommercestable.com.br/api/dataentities/ContactForm/documents?_schema=contactForm",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
},
);
if (!response.ok) {
throw new Error("Error while sending the message");
}
return { message: "Your message was sent successfully!" };
} catch (error) {
return { message: error };
}
},
},
};
export default contactFormResolver;Step 3: Export from the resolver index
// src/graphql/thirdParty/resolvers/index.ts
// Aggregates all third-party resolvers.
// FastStore CLI reads this file to register them with the GraphQL server.
import contactFormResolver from "./contactForm";
const resolvers = {
...contactFormResolver,
};
export default resolvers;###3 Consuming extended data in components
// src/components/BuyButtonWithDetails/BuyButtonWithDetails.tsx
// Custom BuyButton that displays installment information fetched via API extensions.
import { usePDP } from "@faststore/core";
// usePDP: Hook that provides all PDP data, including extended fields from fragments.
// The data shape includes both native FastStore fields and your custom extensions.
import { Button as UIButton, ButtonProps } from "@faststore/ui";
import { priceFormatter } from "../../utils/priceFormatter";
import styles from "./buy-button-with-details.module.scss";
export function BuyButtonWithDetails(props: ButtonProps) {
// usePDP() returns the full PDP context including data from ServerProduct/ClientProduct fragments.
const context = usePDP();
// Access the custom "availableInstallments" field we added via the VTEX API extension.
const installment = context?.data?.product?.availableInstallments[0];
const interestFree = installment.installmentInterest === 0 ?? false;
return (
<section className={styles.buyButtonWithDetails}>
{interestFree && (
<span>
{`${installment.installmentNumber} interest-free installment(s)`}
<br />
{`of ${priceFormatter(installment.installmentValue)} with ${
installment.installmentPaymentSystemName
}`}
</span>
)}
{/* Spread native ButtonProps so the component remains compatible
with the slot it replaces in ProductDetailsSection. */}
<UIButton {...props} variant="primary">
Buy Button
</UIButton>
</section>
);
}
export default BuyButtonWithDetails;---
Built-in VTEX Type Reference
These are the types already provided by the FastStore GraphQL API. Use extend type <TypeName> in your VTEX extension schemas to add fields to any of them.
Root Query
The top-level Query type exposes these fields:
| Field | Arguments | Return Type | Description |
|---|---|---|---|
product | locator: [IStoreSelectedFacet!]! | StoreProduct! | Returns the details of a product based on the specified locator. |
collection | slug: String! | StoreCollection! | Returns the details of a collection based on the collection slug. |
search | first: Int!, after: String, sort: StoreSort, term: String, selectedFacets: [IStoreSelectedFacet!], sponsoredCount: Int | StoreSearchResult! | Returns the result of a product, facet, or suggestion search. |
allProducts | first: Int!, after: String | StoreProductConnection! | Returns information about all products. |
products | productIds: [String!]! | [StoreProduct!]! | Returns information about selected products. |
allCollections | first: Int!, after: String | StoreCollectionConnection! | Returns information about all collections. |
shipping | items: [IShippingItem!]!, postalCode: String!, country: String! | ShippingData | Returns information about shipping simulation. |
redirect | term: String, selectedFacets: [IStoreSelectedFacet!] | StoreRedirect | Returns if there's a redirect for a search. |
sellers | postalCode: String, geoCoordinates: IGeoCoordinates, country: String!, salesChannel: String | SellersData | Returns a list of sellers available for a specific localization. |
profile | id: String! | Profile | Returns information about the profile. |
productCount | term: String | ProductCountResult | Returns the total product count based on location. |
userOrder | orderId: String! | UserOrderResult | Returns the details of a user order. Requires auth. |
listUserOrders | page: Int, perPage: Int, status: [String], dateInitial: String, dateFinal: String, text: String, clientEmail: String, pendingMyApproval: Boolean | UserOrderListMinimalResult | Returns the list of orders the user can view. Requires auth. |
userDetails | — | StoreUserDetails! | Returns the current user details. Requires auth. |
accountProfile | — | StoreAccountProfile! | Returns the account profile for the authenticated user. Requires auth. |
validateUser | — | ValidateUserData | Returns information about user validation. Requires auth. |
pickupPoints | geoCoordinates: IStoreGeoCoordinates | PickupPoints | Returns a list of pickup points near the given coordinates. |
Root Mutation
| Field | Arguments | Return Type | Description |
|---|---|---|---|
validateCart | cart: IStoreCart!, session: IStoreSession | StoreCart | Checks for changes between the UI cart and the platform cart. |
validateSession | session: IStoreSession!, search: String! | StoreSession | Updates a web session with the specified values. |
subscribeToNewsletter | data: IPersonNewsletter! | PersonNewsletter | Subscribes a new person to the newsletter list. |
cancelOrder | data: IUserOrderCancel! | UserOrderCancel | Cancels a user order. |
processOrderAuthorization | data: IProcessOrderAuthorization! | ProcessOrderAuthorizationResponse | Process order authorization (approve/reject). |
---
StoreProduct
The main product type. Equivalent to a VTEX SKU.
| Field | Type | Description |
|---|---|---|
seo | StoreSeo! | Meta tag data. |
breadcrumbList | StoreBreadcrumbList! | Chain of linked web pages ending with the current page. |
slug | String! | Corresponding collection URL slug. |
name | String! | Product name. |
productID | String! | Product ID (e.g., ISBN or similar global IDs). |
brand | StoreBrand! | Product brand. |
description | String! | Product description. |
image | [StoreImage!]! | Array of images. Accepts context: String and limit: Int arguments. |
offers | StoreAggregateOffer! | Aggregate offer information. |
sku | String! | Stock Keeping Unit (merchant-specific ID). |
gtin | String! | Global Trade Item Number. |
review | [StoreReview!]! | Array with review information. |
aggregateRating | StoreAggregateRating! | Aggregate ratings data. |
isVariantOf | StoreProductGroup! | Indicates the product group related to this product. |
additionalProperty | [StorePropertyValue!]! | Array of additional properties. |
releaseDate | String! | The product's release date (ISO 8601). |
unitMultiplier | Float | SKU unit multiplier. |
advertisement | Advertisement | Advertisement information about the product. |
hasSpecifications | Boolean | Indicates whether the product has specifications. |
skuSpecifications | [SkuSpecification!]! | The specifications of a product. |
specificationGroups | [SpecificationGroup!]! | The specifications of a group of SKUs. |
deliveryPromiseBadges | [DeliveryPromiseBadge] | Delivery promise product badges. |
StoreProductGroup
Product groups are catalog entities that may contain variants. Equivalent to VTEX Products.
| Field | Type | Description |
|---|---|---|
hasVariant | [StoreProduct!]! | Array of variants related to the product group. |
productGroupID | String! | Product group ID. |
name | String! | Product group name. |
additionalProperty | [StorePropertyValue!]! | Array of additional properties. |
skuVariants | SkuVariants | Data structures for handling different SKU variant properties. |
StoreOffer
Offer information for a product.
| Field | Type | Description |
|---|---|---|
listPrice | Float! | Displayed as the "from" price in promotions. |
listPriceWithTaxes | Float! | List price with current taxes. |
sellingPrice | Float! | Computed price before applying coupons, taxes, or benefits. |
priceCurrency | String! | ISO code of the currency used for the offer prices. |
price | Float! | Also known as spot price. |
priceWithTaxes | Float! | Spot price with taxes. |
priceValidUntil | String! | Next date when price is scheduled to change. |
itemCondition | String! | Offer item condition. |
availability | String! | Offer item availability. |
seller | StoreOrganization! | Seller responsible for the offer. |
itemOffered | StoreProduct! | Information on the item being offered. |
quantity | Int! | Number of items offered. |
StoreAggregateOffer
Aggregate offer information for a given SKU across multiple sellers.
| Field | Type | Description |
|---|---|---|
highPrice | Float! | Highest price among all sellers. |
lowPrice | Float! | Lowest price among all sellers. |
lowPriceWithTaxes | Float! | Lowest price among all sellers with current taxes. |
offerCount | Int! | Number of sellers selling this SKU. |
priceCurrency | String! | ISO code of the currency used for the offer prices. |
offers | [StoreOffer!]! | Array with information on each available offer. |
StoreCollection
Product collection information.
| Field | Type | Description |
|---|---|---|
seo | StoreSeo! | Meta tag data. |
breadcrumbList | StoreBreadcrumbList! | Breadcrumb list for navigation. |
meta | StoreCollectionMeta! | Collection meta information (selected facets). |
id | ID! | Collection ID. |
slug | String! | Collection URL slug. |
type | StoreCollectionType! | Collection type (Department, Category, SubCategory, Brand, Cluster, Collection). |
StoreSearchResult
Search result data.
| Field | Type | Description |
|---|---|---|
products | StoreProductConnection! | Search result products (with pagination). |
facets | [StoreFacet!]! | Array of search result facets. |
suggestions | StoreSuggestions! | Search result suggestions. |
metadata | SearchMetadata | Search result metadata (misspelling, fuzzy, logical operator). |
StoreSeo
Search Engine Optimization tags data.
| Field | Type | Description |
|---|---|---|
title | String! | Title tag. |
titleTemplate | String! | Title template tag. |
description | String! | Description tag. |
canonical | String! | Canonical tag. |
StoreBreadcrumbList
Breadcrumb navigation list.
| Field | Type | Description |
|---|---|---|
itemListElement | [StoreListItem!]! | Array with breadcrumb elements. |
numberOfItems | Int! | Number of breadcrumbs in the list. |
StoreListItem
Single breadcrumb item.
| Field | Type | Description |
|---|---|---|
item | String! | List item value. |
name | String! | Name of the list item. |
position | Int! | Position of the item in the list. |
StoreBrand
| Field | Type | Description |
|---|---|---|
name | String! | Brand name. |
StoreImage
| Field | Type | Description |
|---|---|---|
url | String! | Image URL. |
alternateName | String! | Alias for the image. |
StorePropertyValue
Properties associated with products and product groups.
| Field | Type | Description |
|---|---|---|
propertyID | String! | Property ID. |
value | ObjectOrString! | Property value (may be a string or stringified object). |
name | String! | Property name. |
valueReference | ObjectOrString! | Specifies the nature of the value. |
StoreReview
| Field | Type | Description |
|---|---|---|
reviewRating | StoreReviewRating! | Review rating information. |
author | StoreAuthor! | Review author. |
StoreReviewRating
| Field | Type | Description |
|---|---|---|
ratingValue | Float! | Rating value. |
bestRating | Float! | Best rating value. |
StoreAggregateRating
| Field | Type | Description |
|---|---|---|
ratingValue | Float! | Value of the aggregate rating. |
reviewCount | Int! | Total number of ratings. |
StoreOrganization (Seller)
| Field | Type | Description |
|---|---|---|
identifier | String! | Organization / Seller ID. |
StoreSession
Session information.
| Field | Type | Description |
|---|---|---|
locale | String! | Session locale. |
currency | StoreCurrency! | Session currency. |
country | String! | Session country. |
channel | String | Session channel. |
deliveryMode | StoreDeliveryMode | Session delivery mode. |
addressType | String | Session address type. |
city | String | Session city. |
postalCode | String | Session postal code. |
geoCoordinates | StoreGeoCoordinates | Session geo coordinates. |
person | StorePerson | Session person. |
b2b | StoreB2B | B2B information. |
marketingData | StoreMarketingData | Marketing information. |
refreshAfter | String | Refresh token expiry. |
StorePerson
Client profile data.
| Field | Type | Description |
|---|---|---|
id | String! | Client ID. |
email | String! | Client email. |
givenName | String! | Client first name. |
familyName | String! | Client last name. |
StoreCurrency
| Field | Type | Description |
|---|---|---|
code | String! | Currency code (e.g., USD). |
symbol | String! | Currency symbol (e.g., $). |
StoreCart
Shopping cart information.
| Field | Type | Description |
|---|---|---|
order | StoreOrder! | Order information. |
messages | [StoreCartMessage!]! | List of shopping cart messages. |
StoreOrder
| Field | Type | Description |
|---|---|---|
orderNumber | String! | ID of the order in VTEX Order Management. |
acceptedOffer | [StoreOffer!]! | Array with information on each accepted offer. |
shouldSplitItem | Boolean | Indicates whether items with attachments should be split. |
StorePageInfo
Pagination information returned in connection queries.
| Field | Type | Description |
|---|---|---|
hasNextPage | Boolean! | Whether there is at least one more page after the current. |
hasPreviousPage | Boolean! | Whether there is at least one more page before the current. |
startCursor | String! | Cursor corresponding to the first possible item. |
endCursor | String! | Cursor corresponding to the last possible item. |
totalCount | Int! | Total number of items (not pages). |
SkuVariants
Variant handling data structures.
| Field | Type | Description |
|---|---|---|
activeVariations | ActiveVariations | SKU property values for the current SKU. |
allVariantsByName | VariantsByName | All available options for each SKU variant property, indexed by name. |
slugsMap | SlugsMap | Maps property value combinations to their respective SKU slug. Accepts dominantVariantName: String. |
availableVariations | FormattedVariants | Available options for each varying SKU property. Accepts dominantVariantName: String. |
allVariantProducts | [StoreProduct!] | All available variant products. |
SkuSpecification
| Field | Type | Description |
|---|---|---|
field | SKUSpecificationField! | Specification field metadata. |
values | [SKUSpecificationValue!]! | Specification values. |
SpecificationGroup
| Field | Type | Description |
|---|---|---|
name | String! | Group name. |
originalName | String! | Original group name. |
specifications | [Specification!]! | Specifications in this group. |
ShippingData
Shipping simulation information.
| Field | Type | Description |
|---|---|---|
items | [LogisticsItem] | List of logistics items. |
logisticsInfo | [LogisticsInfo] | List of logistics info. |
messages | [MessageInfo] | List of messages. |
address | Address | Address information. |
StoreFacetBoolean
Search facet with boolean values.
| Field | Type | Description |
|---|---|---|
key | String! | Facet key. |
label | String! | Facet label. |
values | [StoreFacetValueBoolean!]! | Array with information on each facet value. |
StoreFacetRange
Search facet with range values.
| Field | Type | Description |
|---|---|---|
key | String! | Facet key. |
label | String! | Facet label. |
min | StoreFacetValueRange! | Minimum facet range value. |
max | StoreFacetValueRange! | Maximum facet range value. |
StoreMarketingData
| Field | Type | Description |
|---|---|---|
utmCampaign | String | UTM campaign parameter. |
utmMedium | String | UTM medium parameter. |
utmSource | String | UTM source parameter. |
utmiCampaign | String | Internal UTM campaign. |
utmiPart | String | Internal UTM part. |
utmiPage | String | Internal UTM page. |
Enums
StoreSort
Product search results sorting options.
| Value | Description |
|---|---|
price_desc | Sort by price, highest to lowest. |
price_asc | Sort by price, lowest to highest. |
orders_desc | Sort by orders, highest to lowest. |
name_desc | Sort by name, reverse alphabetical. |
name_asc | Sort by name, alphabetical. |
release_desc | Sort by release date, newest first. |
discount_desc | Sort by discount value, highest to lowest. |
score_desc | Sort by product score, highest to lowest. |
StoreCollectionType
| Value | Description |
|---|---|
Department | First level of product categorization. |
Category | Second level of product categorization. |
SubCategory | Third level of product categorization. |
Brand | Product brand. |
Cluster | Product cluster. |
Collection | Product collection. |
StoreStatus
Shopping cart message status.
| Value | Description |
|---|---|
INFO | Informational message. |
WARNING | Warning message. |
ERROR | Error message. |
FastStore Third-Party Scripts
Overview
The file src/scripts/ThirdPartyScripts.tsx exports a component that gets automatically injected into the document <head> by FastStore Core.
Scripts are injected in a worker thread asynchronously using the @builder.io/partytown library to prevent blocking the main render thread.
File Location
src/scripts/ThirdPartyScripts.tsxThis file is automatically picked up by FastStore CLI and injected into the document head during build. You do not need to import or reference it manually.
Usage
Export a default React component that returns any JSX valid inside <head>:
// src/scripts/ThirdPartyScripts.tsx
const ThirdPartyScripts = () => {
return (
<>
{/* Site verification meta tag */}
<meta
name="google-site-verification"
content="your-verification-token-here"
/>
{/* Custom script */}
<script
dangerouslySetInnerHTML={{
__html: `
window.myThirdPartyLib = window.myThirdPartyLib || {};
`,
}}
/>
</>
);
};
export default ThirdPartyScripts;Common Use Cases
Site Verification Tag
const ThirdPartyScripts = () => (
<meta
name="google-site-verification"
content="xRwnzq5B91_3hXsAxoKXfxRwMWk2wsaNwInIjiibTx0"
/>
);
export default ThirdPartyScripts;Multiple Head Elements
const ThirdPartyScripts = () => (
<>
<meta name="google-site-verification" content="..." />
<meta name="facebook-domain-verification" content="..." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
</>
);
export default ThirdPartyScripts;Notes
- Google Tag Manager does not need to be added here — it is configured via
discovery.config.jsusinganalytics.gtmContainerIdand injected automatically by FastStore Core. - The component renders into
<head>, so only elements valid in<head>should be returned. - Scripts run in a Partytown worker thread, which means they are sandboxed from the main thread for performance. Ensure any third-party scripts you add are compatible with this approach.
FastStore Global Sections Catalog
These sections are included by default in every FastStore project. Use getOverriddenSection from @faststore/core to override any of their inner component slots.
See section-overrides-and-custom-sections for override implementation patterns.
Available Global Sections
- Alert
- BannerText
- Breadcrumb
- CrossSellingShelf
- EmptyState
- Hero
- Navbar
- Newsletter
- ProductDetails
- ProductGallery
- ProductShelf
- RegionBar
- Search
- Footer
- Incentives
- ProductTiles
- Children
- BannerNewsletter
- CartSidebar
- RegionModal
- RegionPopover
Sections and Their Overridable Component Slots
Alert
AlertIcon
BannerText
BannerTextBannerTextContent
Breadcrumb
BreadcrumbIcon
CrossSellingShelf
ProductShelf__experimentalCarousel__experimentalProductCard
EmptyState
EmptyState
Hero
HeroHeroImageHeroHeader
Navbar
NavbarNavbarLinksNavbarLinksListNavbarSliderNavbarSliderHeaderNavbarSliderContentNavbarSliderFooterNavbarHeaderNavbarRowNavbarButtonsIconButton_experimentalButtonSignIn__experimentalSKUMatrixSidebar
Newsletter
ButtonHeaderIconInputFieldEmailInputFieldNameNewsletterNewsletterAddendumNewsletterContentNewsletterFormNewsletterHeaderToastIconErrorToastIconSuccess
ProductDetails
ProductTitleDiscountBadgeBuyButtonIconProductPriceQuantitySelectorSkuSelectorShippingSimulationImageGalleryImageGalleryViewerSKUMatrixSKUMatrixTriggerSKUMatrixSidebar__experimentalImageGalleryImage__experimentalImageGallery__experimentalShippingSimulation__experimentalSKUMatrixSidebar__experimentalNotAvailableButton__experimentalProductDescription__experimentalProductDetailsSettings
ProductGallery
MobileFilterButtonFilterIconPrevIconResultsCountSkeletonSortSkeletonFilterButtonSkeletonToggleFieldProductComparisonProductComparisonSidebarProductComparisonToolbarLinkButtonPrevLinkButtonNext__experimentalFilterDesktop__experimentalFilterSlider__experimentalProductCard__experimentalEmptyGallery__experimentalProductComparisonSidebar
Note: Overriding__experimentalFilterDesktopor__experimentalFilterSlidermakes your component fully responsible for the filter/search feature implementation.
ProductShelf
ProductShelf__experimentalCarousel__experimentalProductCard
RegionBar
RegionBarLocationIconButtonIconFilterButtonIcon
FastStore Search & Facets Reference
Common Pitfalls
Accessing facets in custom PLP/Search sections
- Facets are NOT available in the server-side page data (
__NEXT_DATA__). They come from the client-sideuseProductGalleryQueryinside theProductListingtemplate, which deep-merges them into thePageProvidercontext. - After client-side hydration, facets are accessible via
usePage():
const context = usePage<PLPContext | SearchPageContext>();
const facets = (context as any)?.data?.search?.facets ?? [];- The component will initially render with no facets (returns
null), then re-render once the client-side query completes and the context updates.
Facet type discrimination
- GraphQL facets use
__typenamefor type discrimination, NOT atypefield: __typename === "StoreFacetBoolean"→ hasvalues[]with{ value, label, selected, quantity }__typename === "StoreFacetRange"→ price ranges, novaluesarray- Do NOT filter with `f.type === "BOOLEAN"` — this field does not exist on the runtime object.
useSearch() API — Zustand store
The useSearch() hook from @faststore/sdk returns a Zustand global store, not the React Context documented in older references. Key differences:
| ✅ Correct (Zustand store) | ❌ Wrong (old Context API) |
|---|---|
const { state, setState } = useSearch() | const { setFacet, removeFacet } = useSearch() |
state.selectedFacets | Direct method calls on the hook return |
To toggle a facet, import the standalone utility toggleFacet from @faststore/sdk:
import { useSearch, toggleFacet } from "@faststore/sdk";
const { state, setState } = useSearch();
const newFacets = toggleFacet(state.selectedFacets, { key, value });
setState({ ...state, selectedFacets: newFacets, page: 0 });Available utilities from @faststore/sdk:
toggleFacet(facets, facet)→ add if absent, remove if presentsetFacet(facets, facet, unique?)→ add a facetremoveFacet(facets, facet)→ remove a facettoggleFacets(facets, facets[])→ toggle multiple at once
Page Context Data Flow (PLP/Search)
The PLP page context is built in two phases:
1. Server-side (getStaticProps): ServerCollectionPageQuery → returns collection.seo, collection.breadcrumbList, collection.metaData. No facets. 2. Client-side (useProductGalleryQuery): ClientProductGalleryQuery → returns search.products, search.facets, search.metadata.
The ProductListing template merges both via deepmerge into the PageProvider context:
// From @faststore/core ProductListing.tsx
const { data: pageProductGalleryData } = useProductGalleryQuery({
term,
sort,
selectedFacets,
itemsPerPage,
});
const context = {
data: {
...deepmerge(
{ ...server },
{ ...pageProductGalleryData },
{ arrayMerge: overwriteMerge },
),
pages,
},
globalSettings,
} as PLPContext;After client-side hydration: usePage().data.search.facets is available.
PLPContext type
interface PLPContext {
data?: ServerCollectionPageQueryQuery & // server: collection, seo
ClientProductGalleryQueryQuery & { pages: ClientManyProductsQueryQuery[] }; // client: search.facets, search.products
globalSettings?: Record<string, unknown>;
}Accessing facets in a custom section
import { usePage } from "@faststore/core";
import type { PLPContext, SearchPageContext } from "@faststore/core";
const context = usePage<PLPContext | SearchPageContext>();
const facets = (context as any)?.data?.search?.facets ?? [];Important: On initial server render, facets will be empty. The component should handle this gracefully (e.g., return null). After client-side hydration and the ProductGalleryQuery completes, React will re-render the component with facets.
Facet type discrimination
GraphQL facets use __typename, NOT a type field:
__typename | Description | Has values[]? |
|---|---|---|
StoreFacetBoolean | Checkbox-style facets (brand, size) | Yes: { value, label, selected, quantity } |
StoreFacetRange | Range facets (price) | No |
const booleanFacets = facets.filter(
(f) => f.__typename === "StoreFacetBoolean" && f.values?.length > 0,
);useSearch() — Zustand store API
The useSearch() hook from @faststore/sdk returns a Zustand global store:
import { useSearch } from "@faststore/sdk";
const { state, setState } = useSearch();
// state.selectedFacets — { key: string, value: string }[]
// state.sort — StoreSort enum string
// state.term — search term or null
// state.page — current page index
// setState(partial) — merges partial state into current`setFacet` and `removeFacet` are NOT methods on the hook return object. They are standalone utility functions.
Facet toggle utilities
Imported as standalone functions from @faststore/sdk:
import {
toggleFacet,
setFacet,
removeFacet,
toggleFacets,
} from "@faststore/sdk";All are pure functions: (currentFacets[], facet) => newFacets[]
import { useSearch, toggleFacet } from "@faststore/sdk";
const { state, setState } = useSearch();
function handleToggle(key: string, value: string) {
const newFacets = toggleFacet(state.selectedFacets, { key, value });
setState({ ...state, selectedFacets: newFacets, page: 0 });
}| Function | Behavior |
|---|---|
toggleFacet(facets, facet) | Add if absent, remove if present |
setFacet(facets, facet, unique?) | Add a facet (if unique, replaces same-key) |
removeFacet(facets, facet) | Remove a facet by value |
toggleFacets(facets, facets[]) | Toggle multiple facets at once |
@faststore/sdk — exports map
| Export | Type | Usage |
|---|---|---|
useSearch | Hook (Zustand store) | const { state, setState } = useSearch() |
SearchProvider | React Component | Wraps PLP/Search pages (already handled by framework) |
toggleFacet | Pure function | toggleFacet(facets[], facet) → newFacets[] |
setFacet | Pure function | setFacet(facets[], facet, unique?) → newFacets[] |
removeFacet | Pure function | removeFacet(facets[], facet) → newFacets[] |
toggleFacets | Pure function | toggleFacets(facets[], facets[]) → newFacets[] |
parseSearchState | Pure function | Parses URL into search state object |
formatSearchState | Pure function | Serializes search state into URL |
sendAnalyticsEvent | Function | Dispatch analytics events |
useAnalyticsEvent | Hook | Subscribe to analytics events |
Common mistake: setFacet and removeFacet are NOT methods on the useSearch() return object. They are standalone pure functions that take and return facet arrays.
#!/usr/bin/env bash
#
# DEPRECATED — Headless CMS schema publishing
# Prefer running from the project root (global VTEX CLI):
# vtex content generate-schema -o cms/faststore/schema.json
# vtex content upload-schema cms/faststore/schema.json
# Do not use yarn/npm cms-sync or faststore cms-sync for this workflow.
# See references/cms-schema-and-section-registration.md and skill.md.
#
# Generate final schema.json for Headless CMS (canonical flags — matches skill.md)
vtex content generate-schema -o cms/faststore/schema.json
# Upload final schema.json to Headless CMS (expects interactive prompts or use expect — see reference)
expect -c 'spawn vtex content upload-schema cms/faststore/schema.json; expect "store ID"; send "faststore\r"; expect "uploaded with"; send "y\r"; expect "Are you sure"; send "y\r"; expect eof' 2>&1