Sales app extensibility

01What is it?
Apply when building, customizing, or deploying extensions for VTEX Sales App. Covers the complete 7-step workflow from prerequisite checks through code generation to deployment, including extension points (cart, PDP. What sets it apart is how it narrows go-to-market work into one specific workflow rather than a broad, generic prompt.
02Inputs
Context for go-to-market work: your goals, audience, constraints, and any source material the skill asks for.
03Output
A ready-to-use result for go-to-market work: the analysis, copy, or recommendations the agent produces.
Install-only

Install as a package

Installs this one skill package for your coding agent, including any supporting files that skill ships with — not every skill in the repository. Read the tutorial.

Terminal
$ npx skills add vtex/skills --skill sales-app-extensibility

Skill instructions

The instruction file for this skill. The skill also includes other files you need to install to use it.

SKILL.md

Sales App Extensibility

When this skill applies

Use this skill when building, customizing, or deploying extensions for VTEX Sales App.

  • Adding features to the cart page (promotions, loyalty, services)
  • Adding features to the Product Detail Page (badges, recommendations, warranties)
  • Adding features to the menu (user profile, navigation, metrics)
  • Integrating external APIs into Sales App extensions
  • Generating, scaffolding, or validating extension code for Sales App

Do not use this skill for:

  • Regular FastStore storefront customization (use faststore-storefront)
  • Building VTEX IO apps (use vtex-io-* skills)
  • Sales App core development or framework modifications

Prerequisite: FastStore project

The project root must contain: biome.json, faststore.json, package.json, tsconfig.json, turbo.json.

If any are missing, STOP. The user must install FastStore first:

npx @vtex/fsp-cli init
# Prompt: "What is the application name?" → enter name or press Enter for default
cd <application-name> && yarn

Documentation: https://beta.fast.store/getting-started

Prerequisite: Sales App module

Inside the FastStore project, a Sales App workspace must exist (typically at packages/sales-app) with src/, package.json, and tsconfig.json. Check root package.json "workspaces" for a path containing sales-app. If missing, STOP:

yarn add @vtex/sales-app -D -W
npx fsp create
# Prompts: account name → "Sales App" → path (default or custom)
# Then add the path to root package.json "workspaces" array
yarn install

Documentation: https://beta.fast.store/sales-app/setting-up

Do NOT proceed to discovery or code generation until both prerequisites are confirmed.

Decision rules

Follow the mandatory 7-step workflow (Steps 0–6) in order. Do not skip steps.

StepPurposeGate
0Check prerequisitesFastStore + Sales App installed → proceed; otherwise STOP
1DiscoveryUnderstand what the user wants to build
2Requirements & PlanMap requirements → generate plan → wait for user approval
3Code Generation & ValidationGenerate Component.tsx + Component.css (plain CSS, never .module.css) + index.tsx → validate
4DocumentationGenerate docs/<ExtensionName>.md explaining the extension
5Local TestingProvide dev commands and URLs
6Build & DeployBuild command → deployment guide

Extension point selection

Extension PointCategoryAvailable HooksLayout Shift
cart.cart-list.afterCartuseCart, useExtensionNo
cart.cart-item.afterCartuseCart, useCartItem, useExtensionYes
cart.order-summary.afterCartuseCart, useExtensionYes
pdp.sidebar.beforePDPusePDP, useCart, useExtensionYes
pdp.sidebar.afterPDPusePDP, useCart, useExtensionYes
pdp.content.afterPDPusePDP, useCart, useExtensionYes
menu.itemMenuuseExtensionNo
menu.drawer-contentMenuuseCurrentUser, useExtensionNo

Hook availability

  • useCart → all cart + PDP extensions
  • useCartItemcart.cart-item.after only
  • useCurrentUsermenu.drawer-content only
  • usePDP → PDP extensions only
  • useExtension → all extension points

Template selection

  • No API + no hooks → simple template
  • No API + hooks → hook template
  • API + no auth → API template
  • API + VTEX IO proxy → IO proxy template (recommended)
  • API + direct auth → direct auth template (insecure, warn user)
  • API doc provided → generate TypeScript interfaces from extracted response shapes; no API doc → use ${DATA_INTERFACE} placeholder

API authentication strategy

  1. Recommended: VTEX IO Proxy App — IO app stores keys server-side. Extension uses credentials: 'include' with a relative path (/_v/my-api/data).
  2. Insecure: Direct Auth — Keys in frontend code, visible in browser. Testing/development only.

Hard constraints

Constraint: Component must return JSX.Element, never null

defineExtensions expects ExtensionPointComponent which returns Element, not Element | null.

Why this matters Returning null causes a TypeScript compilation error. The build will fail.

Detection return null in any component registered with defineExtensions.

Correct

export function MyExtension(): JSX.Element {
  if (!data) return (<></>);
  return <div>{data.value}</div>;
}

Wrong

export function MyExtension(): JSX.Element | null {
  if (!data) return null;
  return <div>{data.value}</div>;
}

Constraint: Guard optional properties before use

CartItem.manualPrice (number | undefined), CartItem.productRefId (string | undefined), CartItem.attachments (Attachment[] | undefined) must be guarded.

Why this matters TypeScript strict mode rejects accessing possibly-undefined values.

Detection item.manualPrice, item.productRefId, or item.attachments without ?., ??, &&, or != null.

Correct

const price = item.manualPrice ?? item.sellingPrice;
const refId = item.productRefId ?? 'N/A';
const count = item.attachments?.length ?? 0;

Wrong

const price = item.manualPrice;
const refId = item.productRefId.toUpperCase();
const count = item.attachments.length;

Constraint: useCartItem().item may be undefined

item from useCartItem() is CartItem | undefined.

Why this matters Accessing properties on undefined causes a runtime crash.

Detection Destructured item from useCartItem() used without if (!item) guard.

Correct

const { item } = useCartItem();
if (!item) return (<></>);
return <div>{item.name}</div>;

Wrong

const { item } = useCartItem();
return <div>{item.name}</div>;

Constraint: defineExtensions is required in index.tsx

Entry point must use defineExtensions from @vtex/sales-app.

Why this matters Without it, the build succeeds but no extensions render.

Detection Missing defineExtensions import or call in index.tsx.

Correct

import { defineExtensions } from '@vtex/sales-app';
import { MyExtension } from './components/MyExtension';
export default defineExtensions({ 'cart.cart-list.after': MyExtension });

Wrong

import { MyExtension } from './components/MyExtension';
export default { 'cart.cart-list.after': MyExtension };

Constraint: Extension point names must match exactly

IDs are a fixed set. Non-existent names silently fail.

Why this matters The extension renders nowhere. No build error — invisible at runtime.

Detection Any name not in: cart.cart-list.after, cart.cart-item.after, cart.order-summary.after, pdp.sidebar.before, pdp.sidebar.after, pdp.content.after, menu.item, menu.drawer-content.

Correct

defineExtensions({ 'cart.cart-list.after': MyExtension });

Wrong

defineExtensions({ 'cart.list.after': MyExtension });

Constraint: Hooks must be used in compatible extension points

Each hook has an available_in restriction.

Why this matters Hook context is only mounted for specific extension points. Using elsewhere throws runtime errors.

Detection useCartItem in PDP/menu. useCurrentUser in cart. usePDP in menu/cart-list.

Correct

// useCartItem in cart.cart-item.after ✓
defineExtensions({ 'cart.cart-item.after': ItemWarranty });

Wrong

// useCartItem in pdp.sidebar.after ✗ — will fail at runtime
defineExtensions({ 'pdp.sidebar.after': ItemWarranty });

Preferred pattern

Workflow overview

Step 0 — Check FastStore + Sales App prerequisites. STOP if missing.

Step 1 — Discovery. Detect use case from keywords, ask follow-up questions, determine API auth strategy. If the user provides API documentation (URL, OpenAPI/Swagger file, Markdown, or inline text), ingest it to extract endpoint details and response shapes — skip the equivalent manual questions. Validate extracted information with the user. Load the discovery reference (references/discovery-and-use-cases.md) for detailed question flows and the API Documentation Ingestion section.

Step 2 — Map requirements to extension point + hooks + template. Present plan listing the files to be created: components/<ComponentName>.tsx, components/<ComponentName>.css (plain CSS — never .module.css), and index.tsx. Wait for approval.

Step 3 — Generate <ComponentName>.tsx, <ComponentName>.css (plain CSS, never .module.css), and index.tsx.

Required references for this step (load before generating):

If API documentation was ingested in Step 1, generate TypeScript interfaces from the extracted response shapes and use them in the component instead of the ${DATA_INTERFACE} placeholder. If the extension calls 2+ endpoints, extract fetch logic into custom hook(s). Validate against the 12-point checklist in code-templates-and-patterns.md. Fix all violations before presenting code; surface warnings to the user.

Step 4 — Generate docs/<ExtensionName>.md inside the Sales App package (create the docs/ folder if needed). Load the documentation template reference (references/documentation-template.md) for the required 9-section structure and the markdown skeleton to fill in.

Step 5 — Provide local dev commands and test URLs. Load the dev/build/deploy reference (references/local-dev-build-and-deploy.md).

Step 6 — Build command and deployment guide. Load the dev/build/deploy reference (references/local-dev-build-and-deploy.md).

Reference Files

Load these on demand based on what the task requires. Do not load all of them upfront.

FileLoad when…
references/extension-points-hooks-and-types.mdChoosing an extension point, selecting hooks, looking up TypeScript types (CartItem, ProductSku, Totalizers, Attachment), or checking hook return values and availability per extension point
references/code-templates-and-patterns.mdGenerating extension code — simple, hook, API, IO Proxy, or Direct Auth templates; CSS template with the full Sales App design system inlined (tokens, typography, spacing, responsive); index.tsx with defineExtensions; hook initialization; validation checklist
references/discovery-and-use-cases.mdRunning Step 1 (Discovery) — use case detection keywords, follow-up questions, API auth decision tree, IO Proxy vs Direct Auth flow
references/local-dev-build-and-deploy.mdRunning Steps 5–6 — dev server commands, test URLs, build command, common build errors, FastStore WebOps deployment, monitoring, rollback
references/static-analysis-rules.mdValidating generated code (Step 3) — sandbox security, CSS containment, and React performance rules from @vtex/fsp-analyzer; full rule catalog with violation IDs, detection patterns, and correct/wrong examples
references/design-guidelines.mdWriting UI text (sentence case rule) or using icons (Phosphor Icons). CSS-related design rules — tokens, typography, spacing, responsive — are inlined directly in the CSS template inside code-templates-and-patterns.md, so this file is not needed for CSS generation.
references/documentation-template.mdWriting the docs/<ExtensionName>.md file in Step 4 — the 9-section structure and markdown skeleton

Common failure modes

  • Skipping prerequisite checks — Generating code without FastStore/Sales App installed. Always confirm both before Step 1.
  • Not presenting plan — User may want a different approach. Always confirm at Step 2 before generating code.
  • Skipping documentation — Extension generated without docs/<ExtensionName>.md. Load documentation-template.md for the required structure.
  • Inventing API response types — Generated interface doesn't match actual API. If documentation was provided, derive types from it; if not, ask the user for a sample JSON response.
  • Ignoring provided API documentation — User provided a URL or file but agent asked manual questions anyway. Always check for documentation first and use the API Documentation Ingestion flow.
  • Inline fetch with 2+ endpoints — Multiple fetch calls inside the component body. Extract into custom hook(s) in hooks/use{Purpose}.ts.
  • Placing code outside packages/sales-app/src/ — Files outside this path are not included in the build.

Code-level violations (DOM APIs, Node imports, eval, CSS containment, React performance, design tokens) are enforced by @vtex/fsp-analyzer and caught during Step 3 validation. See static-analysis-rules.md (references/static-analysis-rules.md) and design-guidelines.md (references/design-guidelines.md).

Review checklist

Workflow gates

  • FastStore installed (biome.json, faststore.json, package.json, tsconfig.json, turbo.json)?
  • Sales App module installed (src/, package.json, tsconfig.json in sales-app directory)?
  • Discovery completed and use case identified?
  • Execution plan approved by user?
  • Extension point is valid (from the 8-point reference)?
  • Hooks compatible with chosen extension point?
  • Documentation generated at docs/<ExtensionName>.md (9 sections, using documentation-template.md)?
  • If API documentation was provided, TypeScript interfaces match the documented response shape?
  • If 2+ API endpoints used, fetch logic extracted into custom hook(s) in hooks/?
  • Build passes: yarn fsp build {account} sales-app?
  • Tested locally: yarn fsp dev {account}?

TypeScript / runtime guards (not caught by fsp-analyzer)

  • Component returns JSX.Element, never null?
  • Optional properties guarded (manualPrice, productRefId, attachments)?
  • useCartItem().item checked for undefined?
  • defineExtensions configured in index.tsx?

All other code-level rules (sandbox APIs, CSS containment, React performance, design tokens) are enforced by @vtex/fsp-analyzer and checked during Step 3. See static-analysis-rules.md (references/static-analysis-rules.md) and design-guidelines.md (references/design-guidelines.md).

Related skills

  • faststore-storefront — storefront customization outside Sales App
  • vtex-io-app-contract — building VTEX IO proxy apps for secure API integration

Reference


Supporting file: references/code-templates-and-patterns.md

Code Templates and Patterns

Template Selection

ConditionTemplate
No API + no hooksSimple template
No API + hooks neededHook template
API + no authenticationAPI template
API + VTEX IO proxy appIO Proxy template (recommended)
API + direct authDirect Auth template (insecure — warn user)

Simple Extension (no hooks, no API)

import React from 'react';
import './${COMPONENT_NAME}.css';

/**
 * ${COMPONENT_NAME}
 * ${DESCRIPTION}
 *
 * Extension Point: ${EXTENSION_POINT}
 *
 * IMPORTANT: This component MUST always return a JSX.Element, never null.
 * The defineExtensions type (ExtensionPointComponent) does not accept null.
 * If you need to conditionally hide content, return an empty fragment: <></>
 */
export function ${COMPONENT_NAME}(): JSX.Element {
  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}

Extension with Hooks

import React from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

/**
 * ${COMPONENT_NAME}
 * ${DESCRIPTION}
 *
 * Extension Point: ${EXTENSION_POINT}
 * Hooks: ${HOOKS_LIST}
 */
export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}

Hook initialization patterns

// useCart — access cart data and mutations
const cart = useCart();
// cart.items, cart.value, cart.totalizers, cart.clientProfileData, cart.giftCards
// cart.addItem(), cart.removeItem(), cart.addCoupon(), cart.addGiftCard(), cart.sync()

// useCartItem — access individual cart item (cart.cart-item.after ONLY)
const { item, itemIndex, changeItem, changePrice } = useCartItem();
// ALWAYS check: if (!item) return (<></>);

// useCurrentUser — access logged-in user (menu.drawer-content ONLY)
const { name, email } = useCurrentUser();

// useExtension — access account and extension point context (ALL extension points)
const { account, extensionPoint } = useExtension();

// usePDP — access product data (PDP extensions ONLY)
const { productSku } = usePDP();
// productSku.id, productSku.name, productSku.price, productSku.listPrice

Extension with API (no auth)

import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

interface ${COMPONENT_NAME}Data {
  // Generated from API documentation — see docs/<ExtensionName>.md for source
  // Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
  ${DATA_INTERFACE}
}

export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}
  const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null)
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('${API_ENDPOINT}', {
          method: 'GET',
          headers: { 'Content-Type': 'application/json' },
        });

        if (!response.ok) throw new Error('Failed to fetch data');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  if (loading) {
    return (
      <div className="container">
        <div className="loading">
          <span className="spinner"></span>
          <span>Loading...</span>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="container">
        <div className="error"><span>Error: {error}</span></div>
      </div>
    );
  }

  if (!data) return (<></>);

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}

Extension with IO Proxy (recommended for authenticated APIs)

The IO app handles the external API keys securely on the server side. The extension uses credentials: 'include' to forward session cookies.

Critical: The API endpoint must be a relative path (e.g., /_v/my-api/data). NEVER use a full URL like https://{account}.myvtex.com/.... The Sales App internal proxy resolves the domain automatically.

import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

interface ${COMPONENT_NAME}Data {
  // Generated from API documentation — see docs/<ExtensionName>.md for source
  // Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
  ${DATA_INTERFACE}
}

export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}
  const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('${API_ENDPOINT}', {
          method: '${API_METHOD}',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          ${FETCH_BODY}
        });

        if (!response.ok) throw new Error('Failed to fetch data');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  if (loading) {
    return (
      <div className="container">
        <div className="loading">
          <span className="spinner"></span>
          <span>Loading...</span>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="container">
        <div className="error"><span>Error: {error}</span></div>
      </div>
    );
  }

  if (!data) return (<></>);

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}

Extension with Direct Auth (insecure — testing only)

⚠️ SECURITY WARNING: This template passes authentication keys directly in request headers. The keys are visible to anyone inspecting the browser's network requests. Use a VTEX IO proxy app in production.

import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

/**
 * ⚠️ SECURITY WARNING: Authentication keys exposed in frontend code.
 * Move to a VTEX IO proxy app for production use.
 */

interface ${COMPONENT_NAME}Data {
  // Generated from API documentation — see docs/<ExtensionName>.md for source
  // Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
  ${DATA_INTERFACE}
}

export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}
  const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('${API_ENDPOINT}', {
          method: '${API_METHOD}',
          headers: {
            'Content-Type': 'application/json',
            // ⚠️ INSECURE: Authentication key exposed in frontend code
            '${AUTH_HEADER_NAME}': '${AUTH_HEADER_VALUE}',
          },
          ${FETCH_BODY}
        });

        if (!response.ok) throw new Error('Failed to fetch data');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  if (loading) {
    return (
      <div className="container">
        <div className="loading">
          <span className="spinner"></span>
          <span>Loading...</span>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="container">
        <div className="error"><span>Error: {error}</span></div>
      </div>
    );
  }

  if (!data) return (<></>);

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}

CSS Stylesheet

All extensions use plain CSS (not CSS modules). The full Sales App design system for CSS — tokens, typography, spacing grid, responsive breakpoints, and structure — is inlined below. Use this template as the single source of truth for any CSS generation.

CRITICAL — File naming: The CSS file MUST be named ${COMPONENT_NAME}.cssnever ${COMPONENT_NAME}.module.css. The Sales App bundler does not support CSS modules. Import as a side-effect: import './${COMPONENT_NAME}.css'; — never as import styles from './${COMPONENT_NAME}.module.css';. Use className="container" string literals, not className={styles.container}.

Design system rules baked into this template

These rules apply to every generated extension CSS. The template below already complies — keep it that way.

TopicRule
TokensAll colors must reference --sa-color-* tokens declared on .container. Never use hardcoded hex values anywhere else.
Font family'VTEX Trust', -apple-system, BlinkMacSystemFont, sans-serif on .container, inherited by descendants.
Font weightsOnly Regular (400), Medium (500), Semibold (600), Bold (700).
Font sizes (px)Allowed scale only: 10, 12, 14, 16, 18, 20, 22, 24, 28, 32, 36, 40, 44, 48. Never 13, 15, 17, etc.
Typography rolesSection title = 16px/600 · Subtitle/label = 14px/500 · Body = 14px/400 · Caption = 12px/400 · Price = 18px/600.
SpacingMultiples of 4px only (4, 8, 12, 16, 20, 24, 32, 40…). Never 10, 14, 18, 22. Applies to padding, margin, gap, height.
ContainmentNo position: fixed, no z-index: 9999, no :host, no :global, no @import. position: relative and sticky are safe.
SelectorsUse the unprefixed utility class names from this template (.container, .content, .row, .title, .button, etc.). The Sales App fsp-analyzer runs with transformNonCompliant: true, so unprefixed selectors emit a CSS_TRANSFORMED warning rather than a CSS_NAMESPACE_REQUIRED violation. Never use *, body, html, :root, head, main, #root, #__next — those are caught by CSS_GLOBAL_SELECTOR and remain blocking.
KeyframesPrefix with ${COMPONENT_NAME}- (e.g., ${COMPONENT_NAME}-spin). This raises a CSS_TRANSFORMED warning (build still passes) — use sales-app-extension-${COMPONENT_NAME}-spin if you want a clean run.
!importantNever.
ResponsiveUse the breakpoint at (max-width: 743px) for mobile overrides. Sales App shell handles the rest.
/**
 * ${COMPONENT_NAME} Styles
 * Sales App design system (tokens + typography + spacing + responsive) is fully inlined.
 * UX writing: Use sentence case for all UI text. Never use ALL-CAPS.
 */

.container {
  /* === Sales App design tokens — single source of truth ===
     Always declare these on .container. Reference via var(--sa-color-*) in all other rules.
     Never use hardcoded hex values in CSS rules. */

  /* Primary action */
  --sa-color-primary:       #157BF4;   /* Light Blue 800 */
  --sa-color-primary-hover: #0366DD;   /* Light Blue 900 */

  /* Text */
  --sa-color-text-primary:   #1F1F1F;  /* Neutral 1200 — high emphasis */
  --sa-color-text-secondary: #5C5C5C;  /* Neutral 600  — medium emphasis */
  --sa-color-text-tertiary:  #3D3D3D;  /* Neutral 500  — low emphasis */
  --sa-color-text-muted:     #999999;  /* Neutral 700  — disabled / hint */

  /* Backgrounds */
  --sa-color-bg:        #FFFFFF;       /* Neutral White */
  --sa-color-bg-subtle: #F5F5F5;       /* Neutral 100 */
  --sa-color-bg-muted:  #EBEBEB;       /* Neutral 200 */

  /* Borders */
  --sa-color-border:       #E0E0E0;    /* Neutral 300 */
  --sa-color-border-input: #D6D6D6;    /* Neutral 400 */

  /* Error (Red scale) */
  --sa-color-error-bg:     #FDF6F5;    /* Red 50 */
  --sa-color-error-border: #FFDFD9;    /* Red 200 */
  --sa-color-error-text:   #EC3727;    /* Red 800 */

  /* Success (Green scale) */
  --sa-color-success-bg:   #EDFDF5;    /* Green 50 */
  --sa-color-success-text: #01905F;    /* Green 800 */

  /* Warning (Yellow / Orange scale) */
  --sa-color-warning-bg:   #FBF7D4;    /* Yellow 50 */
  --sa-color-warning-text: #E57001;    /* Orange 700 */

  /* Info (Light Blue scale) */
  --sa-color-info-bg:   #F1F8FD;       /* Light Blue 50 */
  --sa-color-info-text: #157BF4;       /* Light Blue 800 */

  padding: 16px;
  background-color: var(--sa-color-bg);
  font-family: 'VTEX Trust', -apple-system, BlinkMacSystemFont, sans-serif;
}

/* Layout */
.content { display: flex; flex-direction: column; gap: 12px; }
.row { display: flex; align-items: center; gap: 12px; }
.spaceBetween { justify-content: space-between; }

/* Typography — uses approved size scale (10, 12, 14, 16, 18, 20, 22, 24...) */
.title    { font-size: 16px; font-weight: 600; color: var(--sa-color-text-primary);   margin: 0; }
.subtitle { font-size: 14px; font-weight: 500; color: var(--sa-color-text-secondary); margin: 0; }
.text     { font-size: 14px; color: var(--sa-color-text-tertiary); line-height: 1.5; }

/* Loading state */
.loading {
  display: flex; align-items: center; justify-content: center;
  gap: 8px; padding: 24px; color: var(--sa-color-text-secondary);
}
.spinner {
  width: 20px; height: 20px;
  border: 2px solid var(--sa-color-border); border-top-color: var(--sa-color-primary);
  border-radius: 50%; animation: ${COMPONENT_NAME}-spin 1s linear infinite;
}
@keyframes ${COMPONENT_NAME}-spin { to { transform: rotate(360deg); } }

/* Error state */
.error {
  padding: 16px; background-color: var(--sa-color-error-bg);
  border: 1px solid var(--sa-color-error-border); border-radius: 8px;
  color: var(--sa-color-error-text); font-size: 14px;
}

/* Button styles */
.button {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 12px 16px; font-size: 14px; font-weight: 500;
  border: none; border-radius: 6px; cursor: pointer;
  transition: background-color 0.2s ease;
}
.buttonPrimary { background-color: var(--sa-color-primary); color: #FFFFFF; }
.buttonPrimary:hover { background-color: var(--sa-color-primary-hover); }
.buttonSecondary { background-color: var(--sa-color-bg-muted); color: var(--sa-color-text-tertiary); }
.buttonSecondary:hover { background-color: var(--sa-color-border); }

/* Card */
.card {
  padding: 16px; background-color: var(--sa-color-bg-subtle);
  border-radius: 8px; border: 1px solid var(--sa-color-border);
}

/* Badges */
.badge {
  display: inline-block; padding: 4px 8px;
  font-size: 12px; font-weight: 500; border-radius: 4px;
}
.badgeSuccess { background-color: var(--sa-color-success-bg); color: var(--sa-color-success-text); }
.badgeWarning { background-color: var(--sa-color-warning-bg); color: var(--sa-color-warning-text); }
.badgeInfo    { background-color: var(--sa-color-info-bg);    color: var(--sa-color-info-text); }

/* Input */
.input {
  width: 100%; padding: 12px; font-size: 14px;
  border: 1px solid var(--sa-color-border-input); border-radius: 6px;
  outline: none; transition: border-color 0.2s ease;
}
.input:focus { border-color: var(--sa-color-primary); }

/* Price display */
.price          { font-size: 18px; font-weight: 600; color: var(--sa-color-text-primary); }
.priceOld       { font-size: 14px; color: var(--sa-color-text-muted); text-decoration: line-through; }
.priceDiscount  { font-size: 14px; color: var(--sa-color-success-text); font-weight: 500; }

/* Responsive — Sales App breakpoints
   Small (mobile)  320–743px  → margin: 24px, no column grid
   Medium (tablet) 744–1279px → host gutters: 16px (no override needed)
   Large (desktop) 1280–1919px → host gutters: 20px (no override needed)
   Extra large     1920px+    → host gutters: 20px (no override needed) */
@media (max-width: 743px) {
  .container { padding: 12px; margin: 0 24px; }
}

CSS file structure

Every extension CSS must follow this order:

  1. Token declarations--sa-color-* on .container
  2. Layout helpers.container, .content, .row, .spaceBetween
  3. Typography.title, .subtitle, .text
  4. State.loading, .spinner, @keyframes, .error
  5. Component classes.button, .card, .badge, .input, .price
  6. Responsive@media queries at the bottom

For UX writing rules (sentence case) and iconography (Phosphor Icons) used in .tsx, see design-guidelines.md.

index.tsx with defineExtensions

import { defineExtensions } from '@vtex/sales-app';
${COMPONENT_IMPORTS}

/**
 * Sales App Extensions Entry Point
 *
 * Connects extension components to their target extension points.
 * Each extension point can only have one component assigned to it.
 *
 * Available extension points:
 * - cart.cart-list.after
 * - cart.cart-item.after
 * - cart.order-summary.after
 * - pdp.sidebar.before
 * - pdp.sidebar.after
 * - pdp.content.after
 * - menu.item
 * - menu.drawer-content
 */
export default defineExtensions({
  ${EXTENSION_MAPPINGS}
});

Validation Rules

After generating code, validate for these issues:

  1. React import presentimport React from 'react' is required for JSX compilation
  2. Component exported — must have export function, export const, or export default
  3. No null returnsreturn null is not allowed; use return (<></>) instead
  4. Hook imports — all hooks must be imported from @vtex/sales-app
  5. Optional property guardsmanualPrice, productRefId, attachments must have null checks
  6. useCartItem item checkif (!item) return (<></>) before accessing item properties
  7. Hook/extension-point compatibility — each hook is only valid in specific extension points
  8. API call handling — loading state and error handling must be present for any fetch() call
  9. CSS class usage — CSS classes defined in the stylesheet should be used in the component
  10. defineExtensions in index.tsx — entry point must import and call defineExtensions
  11. Static analysis compliance — generated code must pass all fsp-analyzer sandbox security, CSS containment, and React performance rules. Load the static analysis reference (static-analysis-rules.md) to run the full check. Fix all violations before presenting code to the user; flag warnings to the user for review.
  12. Design system compliance — the CSS file must follow the rules table at the top of the "CSS Stylesheet" section above: --sa-color-* custom properties declared on .container and used in all rules, 'VTEX Trust' font family, allowed font sizes (10, 12, 14, 16, 18, 20, 22, 24, 28, 32…), 4px-multiple spacing, scoped selectors, and the responsive override at (max-width: 743px). For UI text (sentence case) and icons (Phosphor), see design-guidelines.md.

API Type Generation from Documentation

When API documentation was ingested during Discovery (Step 1), generate TypeScript interfaces from the extracted response shapes. Apply these rules:

JSON to TypeScript conversion rules

JSON valueTypeScript type
"string"string
123number
true / falseboolean
null or sometimes absenttype | null or property?: type
{ } (nested object)Separate named interface
[ ] (array of objects)InterfaceType[]
[ ] (array of primitives)string[], number[], etc.

Naming conventions

  • Top-level response type: {ComponentName}Data (replaces the ${DATA_INTERFACE} placeholder)
  • Nested object type: {ComponentName}{FieldName} (e.g., LoyaltyHistoryEntry)
  • Request body type (POST/PUT): {ComponentName}Request
  • Keep names PascalCase and domain-specific

Example

Given API response:

{ "points": 100, "tier": "gold", "expiresAt": null, "history": [{ "date": "2026-01-01", "amount": 10 }] }

Generate:

interface LoyaltyHistoryEntry {
  date: string;
  amount: number;
}

interface LoyaltyData {
  points: number;
  tier: string;
  expiresAt: string | null;
  history: LoyaltyHistoryEntry[];
}

If a field is documented as optional (not always present), use ?:

interface ProductRecommendationData {
  id: string;
  name: string;
  price: number;
  imageUrl?: string;  // optional per API docs
  badge?: string;     // optional per API docs
}

Multiple endpoints

If the extension calls more than one endpoint, generate a separate interface per endpoint response. Prefix with the endpoint purpose: {ComponentName}{Purpose}Data (e.g., LoyaltyBalanceData, LoyaltyRedemptionResponse).

Custom Fetch Hook Pattern

Use this pattern when the extension calls 2 or more endpoints or when the fetch logic is complex (chained requests, pagination, polling).

Decision rule

  • 1 endpoint → inline useEffect fetch inside the component (existing pattern).
  • 2+ endpoints → extract into one or more custom hooks in hooks/use{Purpose}.ts.

Custom hook template

import { useState, useEffect } from 'react';

export function use${Purpose}(account: string) {
  const [data, setData] = useState<${ResponseType} | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        const response = await fetch('${API_ENDPOINT}', {
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
        });
        if (!response.ok) throw new Error('Failed to fetch ${Purpose}');
        const result: ${ResponseType} = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  return { data, loading, error };
}

Usage in component

import { use${Purpose} } from './hooks/use${Purpose}';

export function ${COMPONENT_NAME}(): JSX.Element {
  const { account } = useExtension();
  const { data: balance, loading: loadingBalance, error: errorBalance } = use${Purpose}(account);
  const { data: history, loading: loadingHistory, error: errorHistory } = use${OtherPurpose}(account);

  if (loadingBalance || loadingHistory) return <div className="loading">Loading...</div>;
  if (errorBalance || errorHistory) return <div className="error">Error loading data</div>;
  if (!balance || !history) return (<></>);

  return (
    <div className="container">
      {/* render using balance and history */}
    </div>
  );
}

File placement

Custom hooks go in packages/sales-app/src/hooks/use{Purpose}.ts. They must be TypeScript-only files (no JSX, no .tsx extension needed unless they return JSX).


Supporting file: references/design-guidelines.md

Sales App Design Guidelines (non-CSS)

This reference covers two topics that apply to TSX content, not CSS:

  1. UX writing rules — for any text rendered in JSX
  2. Iconography — when the extension uses icons

For CSS: all design tokens, typography scale, spacing grid, and responsive breakpoints are inlined directly in the CSS template at code-templates-and-patterns.md §"CSS Stylesheet". That template is the single source of truth for any CSS generation. Do not invent or duplicate token values here.


1. UX Writing Rules

Apply to every string that appears in the UI (labels, buttons, messages, placeholders, alt text).

RuleCorrectWrong
Sentence case"Add to cart""ADD TO CART"
Proper nouns capitalized"VTEX account""vtex account"
No all-caps"Loyalty points""LOYALTY POINTS"
No system-style placeholders"Your email""EMAIL ADDRESS"
No CSS uppercase transforms(omit text-transform: uppercase)text-transform: uppercase

These rules derive from Google Material Design and Apple Human Interface Guidelines, adopted by VTEX.


2. Iconography

Library

All icons must come from Phosphor Icons (https://phosphoricons.com/). Do not use Material Icons, Font Awesome, Heroicons, or any other library.

Installation

yarn add @phosphor-icons/react

Sizes

UsageSize
Compact / inline16px
Standard / default24px
Large / featured32–48px

Always use sizes within 16px to 48px.

Weights

Use Regular or Bold weight variants. Keep the weight consistent within a single component.

Usage example

import { ShoppingCart, Star, Warning } from '@phosphor-icons/react';

export function MyExtension(): JSX.Element {
  return (
    <div className="container">
      <ShoppingCart size={24} />
      <span>Cart items</span>
    </div>
  );
}

3. Quick checklist

Before presenting generated .tsx to the user, verify:

  • All UI text in sentence case (no ALL-CAPS, no system-style placeholders)?
  • All icons imported from @phosphor-icons/react?
  • Icon sizes between 16px and 48px (24px default)?
  • Icon weights consistent within the component?

For CSS verification (tokens, fonts, spacing, responsive), see code-templates-and-patterns.md §"Design system rules baked into this template".


Supporting file: references/discovery-and-use-cases.md

Discovery Flow and Use Cases

Use Case Detection

Detect the use case from keywords in the user's description:

Use CaseKeywordsComplexityRequires APIRequires Hooks
Static Informationmessage, warning, banner, badge, text, static, fixedSimpleNoNo
Loyalty Programloyalty, points, benefit, redeem, cashback, programMediumYesYes
Additional Serviceswarranty, insurance, installation, assembly, service, extraMediumYesYes
Product Recommendationsrecommendation, suggestion, related, complementary, cross-sell, upsellHighYesYes
Custom Discountsdiscount, coupon, promotion, price, voucherHighYesYes
User Profile Displayprofile, user, seller, menu, drawerSimpleNoYes

Discovery Questions by Use Case

Static Information

  1. Content type: Text message, Banner/Card, Badge/Tag, List of information, or Alert/Warning?
  2. Location: Which extension point?
    • cart.cart-list.after — After the cart items list
    • cart.order-summary.after — After the order summary
    • pdp.sidebar.before — Before the PDP sidebar
    • pdp.sidebar.after — After the PDP sidebar
    • pdp.content.after — After the PDP content
  3. Content: What text/information to display?

Recommended extension points: cart.cart-list.after, cart.order-summary.after, pdp.sidebar.before Required hooks: none Template: Simple

Loyalty Program

  1. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 2–5 — the agent will extract endpoint, parameters, and response types automatically.
  2. Data source: Own external API, VTEX IO app, VTEX Master Data, Third-party API, or Not sure yet?
  3. API endpoint: Path (e.g., /_v/my-loyalty/points) — only if external data source and no docs provided
  4. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided
  5. Request params: Parameters, query strings, or request body to send — skip if API docs provided
  6. Response format: JSON structure of the API response — skip if API docs provided
  7. Features: Display points balance, Show earnings with purchase, Allow points redemption, Display customer level
  8. Location: cart.order-summary.after or menu.drawer-content
  9. Interaction: View only or Interactive (redeem points, apply discounts)?

Recommended extension points: cart.order-summary.after, menu.drawer-content Required hooks: useCart, useExtension, useCurrentUser Template: API (IO Proxy or Direct Auth)

Additional Services

  1. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically.
  2. Service types: Extended warranty, Insurance, Installation, Assembly, Customization, Other
  3. Data source: External API, VTEX IO app, Fixed list in code, or VTEX Catalog?
  4. API endpoint: Path — only if external data source and no docs provided
  5. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided
  6. Request params: Parameters or request body — skip if API docs provided
  7. Response format: JSON structure of the API response — skip if API docs provided
  8. Location: cart.cart-item.after (per item) or pdp.sidebar.after (product page)
  9. Pricing model: Fixed price, Percentage of product, or Calculated by API?

Recommended extension points: cart.cart-item.after, pdp.sidebar.after Required hooks: useCart, useCartItem, usePDP Template: Hook or API (depending on data source)

Product Recommendations

  1. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically.
  2. Recommendation type: Frequently bought together, Accessories, Similar products, Upgrades
  3. Data source: Own API, VTEX Intelligent Search, Fixed rules, or Third-party API?
  4. API endpoint: Path — only if external data source and no docs provided
  5. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided
  6. Request params: Parameters or request body — skip if API docs provided
  7. Response format: JSON structure of the API response — skip if API docs provided
  8. Location: cart.cart-list.after or pdp.content.after
  9. Quick add: Can the seller quickly add recommended products to the cart?
  10. Display count: 3, 4, 6 products, or customizable?

Recommended extension points: cart.cart-list.after, pdp.content.after Required hooks: useCart, usePDP, useExtension Template: API (IO Proxy or Direct Auth)

Custom Discounts

  1. API documentation (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically.
  2. Discount types: Discount coupon, Manual seller discount, Volume discount, Special promotion
  3. Validation source: Own promotions API, VTEX Promotions, or Fixed coupon list?
  4. API endpoint: Path for validation — only if external API and no docs provided
  5. HTTP method: GET, POST, PUT, or DELETE? — skip if API docs provided
  6. Request params: Parameters or request body — skip if API docs provided
  7. Response format: JSON structure of the API response — skip if API docs provided
  8. Discount limits: No limit, Maximum percentage, Maximum value, or Depends on seller level?
  9. Approval needed: No, Yes above a certain value, or Always?

Recommended extension points: cart.order-summary.after Required hooks: useCart, useCurrentUser, useExtension Template: API (IO Proxy or Direct Auth)

User Profile Display

  1. Information to display: Name, Email, Sales metrics, Goals, Quick settings
  2. Metrics source (if metrics selected): No metrics, Internal API, VTEX OMS, or Static data?

Recommended extension points: menu.drawer-content Required hooks: useCurrentUser, useExtension Template: Hook (no API if only using useCurrentUser data)

API Authentication Decision Tree

When the use case requires an external API:

Step 1: Check for API documentation and confirm API details

First — Ask: does the user have API documentation they can share?

  • Yes → Go to the API Documentation Ingestion (#api-documentation-ingestion) section. Extract HTTP method, request parameters/body, and response JSON structure from the docs. Present the extracted details to the user for confirmation, then proceed to Step 2.
  • No → Collect manually: HTTP method, request parameters/body, and API response JSON structure. Proceed to Step 2.

Step 2: Does the API need authentication?

  • No → Use the basic API template. Done.
  • Yes → Continue to Step 3.

Step 3: Does the user have a VTEX IO proxy app?

A VTEX IO proxy app acts as a middleware: the Sales App extension calls the IO app using credentials: 'include' to forward session cookies, and the IO app calls the external API with the secret keys on the server side. The keys never leave the server.

  • Yes → Collect the IO app endpoint path (relative, starting with /). Use IO Proxy template. Done.
  • No → Continue to Step 4.

Step 4: Implement IO proxy first, or continue with insecure direct auth?

  • Implement IO proxy first → Stop the Sales App extension workflow. The user should build the IO proxy app first and return later.
  • Continue with insecure direct auth → Continue to Step 5.

Step 5: Security warning and direct auth details

⚠️ WARNING: Passing authentication keys directly in frontend code is NOT secure. The keys will be visible to anyone inspecting the browser. Confirm the user understands and accepts the risk.

If confirmed, collect:

  • Auth header name (e.g., x-api-key, Authorization)
  • Auth header value (e.g., Bearer your-token, your-api-key)
  • Full API endpoint URL

Use the Direct Auth template.

IO Proxy Critical Rule

The fetch call in the extension must use ONLY the relative path (e.g., /_v/my-api/endpoint). NEVER prefix with https:// or a domain like {account}.myvtex.com. The Sales App has an internal proxy that automatically resolves the domain.

API Documentation Ingestion

When the user provides API documentation, follow these steps to extract structured information before generating code.

Supported input formats

FormatHow to loadNotes
URLUse fetch_webpage to retrieve the page contentWorks for REST API reference pages, Swagger UI, Redoc
OpenAPI/Swagger JSONParse the JSON directly from user input or file contentLook for paths, components/schemas keys
OpenAPI/Swagger YAMLConvert YAML mentally to JSON structure, then parse same as above
Markdown docRead sections for endpoint paths, method, headers, request/response examples
Inline textRead the pasted content, identify patterns: GET /path, POST /path, JSON examples

Extraction checklist

From any documentation format, extract and record:

  1. Base URL — the host/prefix (e.g., https://api.example.com/v2). For IO Proxy, this becomes irrelevant — only the path matters.
  2. Endpoints — for each endpoint: HTTP method, path, and purpose description.
  3. Required headersContent-Type, Authorization, x-api-key, or others.
  4. Query parameters — name, type, required/optional.
  5. Request body shape — JSON structure with field names, types, and required/optional status.
  6. Response shape — JSON structure. If multiple status codes, capture the success response (200/201).
  7. Error shapes — if documented, capture 4xx/5xx response structures.

Extraction output (mental model)

After extracting, summarize to the user in this format before proceeding to code generation:

Extracted API summary:
- Endpoint: [METHOD] [path]
- Auth: [None / IO Proxy at /_v/... / Direct with header X]
- Request: { field: type, ... }
- Response: { field: type, ... }
- Optional fields: [list]

Does this match what you expected?

Wait for user confirmation before proceeding to Step 2 (template selection) or code generation.

Multiple endpoints

If the documentation describes multiple endpoints needed by the extension:

  • List all of them in the summary above
  • Note whether they can be called in parallel or must be sequential
  • This will determine whether a custom fetch hook is needed (see code templates reference)

Supporting file: references/documentation-template.md

Extension Documentation Template

Generate docs/<ExtensionName>.md inside the Sales App package. Create the docs/ folder if it does not exist.

The document must contain these 9 sections:

  1. Extension name — title matching the component name.
  2. Overview — one-paragraph summary of what the extension does and why it was built.
  3. Extension point — which extension point it registers on (e.g., cart.cart-list.after) and why that point was chosen.
  4. Hooks used — list each hook (useCart, usePDP, etc.) with a brief explanation of what data it provides to this extension.
  5. Component structure — description of the component tree, props, and state management.
  6. Styling — CSS file name and summary of key classes.
  7. API integration (if applicable) — endpoint, auth strategy (IO Proxy or direct), request/response shape.
    • Source documentation: URL or file path of the original API documentation, if provided.
    • Generated types: list of TypeScript interfaces generated from the API documentation.
    • Note which fields are optional (?) vs required per the documentation.
  8. How to test — dev server command and URL to reach the extension.
  9. Known constraints — any guards, edge cases, or limitations (e.g., useCartItem().item may be undefined).
# <ExtensionName>

## Overview
<One-paragraph description of the extension purpose and value.>

## Extension point
- **Point:** `<extension.point.name>`
- **Rationale:** <Why this extension point was selected.>

## Hooks used
| Hook | Purpose |
|------|---------|
| `useCart` | <What data it provides here> |

## Component structure
<Describe the component tree, key props, and internal state.>

## Styling
- **File:** `<ComponentName>.css`
- <Summary of key CSS classes and design decisions.>

## API integration
- **Endpoint:** `/_v/...`
- **Auth strategy:** IO Proxy / Direct / None
- **Request/Response:** <Brief shape description.>

## How to test
Run `yarn fsp dev {account}` and navigate to `https://{account}.myvtex.com/sales-app/...` to verify the extension renders.

## Known constraints
- <Guard or limitation 1>
- <Guard or limitation 2>

Supporting file: references/extension-points-hooks-and-types.md

Extension Points, Hooks, and Types

Extension Points

Sales App has 8 extension points across 3 categories.

Cart Extension Points

cart.cart-list.after

  • Location: Below the cart items list
  • Layout Shift: No
  • Available Hooks: useCart, useExtension
  • Use Cases: Loyalty points summary, promotional banners, gift options, shipping estimates
import { defineExtensions } from '@vtex/sales-app';
import { MyExtension } from './components/MyExtension';

export default defineExtensions({
  'cart.cart-list.after': MyExtension,
});

cart.cart-item.after

  • Location: Below each individual cart item
  • Layout Shift: Yes — use loading states and skeletons
  • Available Hooks: useCart, useCartItem, useExtension
  • Use Cases: Warranty options, item-specific promotions, personalization, service attachments
export default defineExtensions({
  'cart.cart-item.after': ItemWarranty,
});

cart.order-summary.after

  • Location: Below the order summary/totals
  • Layout Shift: Yes — use loading states and skeletons
  • Available Hooks: useCart, useExtension
  • Use Cases: Loyalty points redemption, additional fees/discounts, coupon input, installment options
export default defineExtensions({
  'cart.order-summary.after': LoyaltyPoints,
});

PDP Extension Points

pdp.sidebar.before

  • Location: Above the sidebar content on the Product Detail Page
  • Layout Shift: Yes
  • Available Hooks: usePDP, useCart, useExtension
  • Use Cases: Product badges, stock alerts, promotional banners, seller information

pdp.sidebar.after

  • Location: Below the sidebar content on the Product Detail Page
  • Layout Shift: Yes
  • Available Hooks: usePDP, useCart, useExtension
  • Use Cases: Warranty selection, related services, financing options, gift wrapping

pdp.content.after

  • Location: Below the main content area on the Product Detail Page
  • Layout Shift: Yes
  • Available Hooks: usePDP, useCart, useExtension
  • Use Cases: Product recommendations, customer reviews, specifications, complementary products

Menu Extension Points

menu.item

  • Location: Sales App main menu
  • Layout Shift: No
  • Available Hooks: useExtension
  • Use Cases: Custom navigation links, quick action buttons, menu notifications

menu.drawer-content

  • Location: Sales App menu drawer
  • Layout Shift: No
  • Available Hooks: useCurrentUser, useExtension
  • Use Cases: User profile information, sales performance metrics, settings shortcuts, notifications

Hooks Reference

All hooks are imported from @vtex/sales-app.

useCart

Access cart data and perform mutations.

import { useCart } from '@vtex/sales-app';

const cart = useCart();

Available in: cart.cart-list.after, cart.cart-item.after, cart.order-summary.after, pdp.sidebar.before, pdp.sidebar.after, pdp.content.after

Returns:

PropertyTypeDescription
orderFormIdstring | undefinedUnique identifier of the current order form
valuenumberTotal cart value in cents
itemsCartItem[]Array of items in the cart
totalizersTotalizers[]Array of totalizers (Items, Shipping, Discounts, Tax)
clientProfileDataClientProfileDataClient profile (email, document, phone)
giftCardsGiftCard[]Gift cards currently attached to the cart
addItem(data: UseCartAddItemData) => Promise<void>Add an item to the cart
removeItem(id: string, index: number) => Promise<void>Remove an item from the cart
addCoupon(coupon: string) => Promise<void>Add a coupon to the cart
addGiftCard(redemptionCodeOrGiftCard: string | GiftCard, provider?: string) => Promise<void>Add a gift card to the cart payment data
sync() => Promise<void>Sync the cart with the latest Order Form data

Example — display total items:

const TotalItems = () => {
  const cart = useCart();
  return <div>Items: {cart.items.length}</div>;
};

Example — add item to cart:

const AddToCart = () => {
  const cart = useCart();
  const addItem = () => cart.addItem({
    quantity: 1,
    seller: '1',
    id: '8392'
  });
  return <button onClick={addItem}>Add to cart</button>;
};

Example — add gift card:

const AddGiftCard = () => {
  const cart = useCart();
  const add = async () => {
    await cart.addGiftCard('ABC-123', 'my-giftcard-provider');
    await cart.sync();
  };
  return <button onClick={add}>Add gift card</button>;
};

useCartItem

Access individual cart item data. Only available in cart.cart-item.after.

import { useCartItem } from '@vtex/sales-app';

const { item, itemIndex, changeItem, changePrice } = useCartItem();
// IMPORTANT: item may be undefined — always check first
if (!item) return (<></>);

Returns:

PropertyTypeDescription
itemCartItem | undefinedThe cart item data — check for undefined
itemIndexnumber | undefinedIndex of the item in the cart
changeItem(data: UseCartItemChangeItemData) => Promise<void>Modify quantity or attachments
changePrice(price: number) => Promise<void>Change item price (requires allowManualPrice in orderForm)

useCurrentUser

Access current authenticated user data. Only available in menu.drawer-content.

import { useCurrentUser } from '@vtex/sales-app';

const { name, email } = useCurrentUser();

Returns:

PropertyTypeDescription
namestringCurrent user's name
emailstringCurrent user's email

useExtension

Access account and extension point context. Available in all extension points.

import { useExtension } from '@vtex/sales-app';

const { account, extensionPoint } = useExtension();

Returns:

PropertyTypeDescription
accountstringCurrent VTEX account name
extensionPointExtensionPointsTypeName of the extension point where the component is mounted

usePDP

Access Product Detail Page data. Only available in PDP extensions.

import { usePDP } from '@vtex/sales-app';

const { productSku } = usePDP();

Returns:

PropertyTypeDescription
productSkuProductSkuCurrent product SKU data

TypeScript Types

type CartItem = {
  id: string;
  name: string;
  quantity: number;
  seller: string;
  sellingPrice: number;
  listPrice: number;
  manualPrice?: number;       // optional — guard before use
  price: number;
  imageUrl: string;
  productRefId?: string;      // optional — guard before use
  attachments?: Attachment[];  // optional — guard before use
};

type ClientProfileData = {
  email: string | null;
  document: string | null;
  phone: string | null;
};

type ProductSku = {
  id: string;
  name: string;
  quantity: number;
  price: number;
  listPrice: number;
  sellingPrice?: number;
};

type Totalizers = {
  id: string;    // Common IDs: "Items", "Shipping", "Discounts", "Tax"
  name: string;
  value: number; // in cents
};

type Attachment = {
  name: string;
  content: Record<string, string>;
};

type GiftCard = {
  id: string;
  redemptionCode?: string | null;
  name: string;
  caption: string;
  value: number;
  balance: number;
  provider: string;
  groupName?: string | null;
  inUse: boolean;
  isSpecialCard: boolean;
};

type UseCartAddItemData = {
  id: string;
  quantity: number;
  seller: string;
  attachments?: Attachment[];
};

type UseCartItemChangeItemData = {
  quantity?: number;
  attachments?: Attachment[];
};

type ExtensionPointsType =
  | 'cart.cart-list.after'
  | 'cart.cart-item.after'
  | 'cart.order-summary.after'
  | 'pdp.sidebar.before'
  | 'pdp.sidebar.after'
  | 'pdp.content.after'
  | 'menu.item'
  | 'menu.drawer-content';

Optional Properties Warning

Three CartItem properties are optional and must be guarded:

PropertyTypeSafe Access
manualPricenumber | undefineditem.manualPrice ?? item.sellingPrice
productRefIdstring | undefineditem.productRefId ?? 'N/A'
attachmentsAttachment[] | undefineditem.attachments?.length ?? 0

Supporting file: references/local-dev-build-and-deploy.md

Local Development, Build, and Deployment

Local Development (Step 4)

Start the dev server

# Verify dependencies are installed
yarn install

# Primary command — run at monorepo root
yarn fsp dev {account_name}

# Alternative with more control
yarn sales-app dev {account_name} ./{account_name}/sales-app 3002

# Show extension point placeholders (useful for finding where to render)
yarn sales-app dev {account_name} ./{account_name}/sales-app 3002 --show-placeholders

Test URLs

PageURL
Carthttp://localhost:3002/sales-app/checkout/cart
PDPhttp://localhost:3002/sales-app/product/<product-id>
Basehttp://localhost:3002/sales-app

Development tips

  • Hot reload is active — code changes reflect automatically
  • Use --show-placeholders to see where extension points are located
  • Check the port configuration in faststore.json if there are conflicts

Troubleshooting

IssueSolution
Port in useTry a different port: 3003, 3004
Extension not visibleCheck index.tsx and the configured extension point name
Build error during devRun yarn fsp build {account_name} sales-app for detailed errors
Module not foundRun yarn install to ensure dependencies are linked

Build (Step 5a)

Build command

yarn fsp build {account_name} sales-app

What the build checks

  1. TypeScript — types and syntax errors
  2. Imports — referenced modules and files exist
  3. JSX — correct React component syntax
  4. CSS — style files imported correctly
  5. defineExtensions — valid extension points configuration

Common build errors

ErrorCauseSolution
TypeScript errorsType mismatches, missing typesCheck variable types and @vtex/sales-app imports
Module not foundWrong import path or missing fileVerify file exists at the imported path
Hook errorsHooks inside conditionals or loopsMove hook calls to the top level of the component
CSS errorsMissing or misnamed CSS fileCheck .css file exists and import path is correct
defineExtensions errorsInvalid extension point or missing exportVerify extension point name and component export

Build tip

During development, yarn fsp dev already runs the build automatically. Use yarn fsp build when you want to validate before deploying.

Deployment (Step 5b)

Deployment is automatic via FastStore WebOps when you push to the main branch.

Deployment steps

# 1. Verify local build passes
yarn fsp build {account_name} sales-app

# 2. Review changes
git status

# 3. Stage files
git add .

# 4. Create commit
git commit -m "feat: add Sales App extension"

# 5. Push to main
git push origin main

After pushing:

  1. Monitor deploy — Access FastStore WebOps in VTEX Admin (https://{account_name}.myvtex.com/admin/faststore-webops)
  2. Wait for propagation — Deployment takes 5-10 minutes (includes CDN cache propagation)
  3. Validate in production — Access Sales App in production and verify the extension

Monitoring

FastStore WebOps:

  • URL: VTEX Admin > FastStore > WebOps
  • Shows build and deployment status

GitHub Checks:

  • Go to the GitHub repository
  • See the status icon (✔️, ❌, 🟡) next to each commit

Rollback

If something goes wrong after deployment:

git revert HEAD
git push origin main

Troubleshooting deployment

Build failed in WebOps

  1. Go to the GitHub repository
  2. Click the status icon next to the commit
  3. Select "Details" under "FastStore WebOps"
  4. Read the error logs
  5. Fix the issue locally
  6. Make a new commit and push

Extension doesn't appear after deploy

  1. Check the extension point is correct in index.tsx
  2. Clear browser cache (Ctrl+Shift+R)
  3. Wait longer for propagation (up to 10 minutes)
  4. Verify you're accessing the correct account

Runtime error in production

  1. Open the browser console (F12)
  2. Check for JavaScript errors
  3. If API error, verify URLs and authentication
  4. Fix locally, test, and redeploy

Supporting file: references/static-analysis-rules.md

Static Analysis Rules (fsp-analyzer)

Sales App extensions run inside a sandboxed environment enforced by @vtex/fsp-analyzer. These rules are checked at preBuild and preDev hook time. The AI must validate all generated code against these rules before presenting it to the user.

Rule IDs match the ViolationKind, ReactViolationKind, WarningKind, and ReactWarningKind types from @vtex/fsp-analyzer so developers can cross-reference with real build output.


How to apply these rules

After generating a component, CSS file, and index.tsx, check each generated file against the violation rules (block generation — fix before presenting) and the warning rules (flag to the user with a suggestion). Do not present code that has unresolved violations.


1. Sandbox Security Rules

Applied to .ts and .tsx files by FastStoreSandboxAnalyzer.

RESTRICTED_API — DOM API access banned

Severity: Violation (block)
Files: .ts, .tsx

Restricted identifiers: document, window, localStorage, sessionStorage, navigator.

Detection: Any use of these identifiers as a value (not as a type import).

Correct

// Use React state and props for data; use useExtension() for account context
const { account } = useExtension();

Wrong

const userId = localStorage.getItem('userId');
const width = window.innerWidth;
document.title = 'My Extension';

VARIABLE_ALIASING — Aliasing restricted APIs

Severity: Violation (block)
Files: .ts, .tsx

Assigning a restricted API to a variable to circumvent detection.

Detection: const x = document, const [w, d] = [window, document], or any destructuring that aliases a restricted API.

Correct

// Pass data via props or React context, not via aliased globals

Wrong

const doc = document;
const [w, d] = [window, document];
doc.querySelector('.item');

GLOBAL_MANIPULATION — Global object manipulation

Severity: Violation (block)
Files: .ts, .tsx

Restricted member expressions: window.location, window.history.

Detection: Member expressions on window to access location or history.

Correct

// Navigation is managed by Sales App shell; do not manipulate location

Wrong

window.location.href = '/new-path';
window.location.reload();

HISTORY_MANIPULATION — Browser history manipulation

Severity: Violation (block)
Files: .ts, .tsx

Restricted calls: window.history.pushState, window.history.replaceState, history.pushState, history.replaceState.

Detection: Any call to these methods.

Correct

// Let the Sales App shell handle routing

Wrong

history.pushState({}, '', '/cart');
window.history.replaceState(null, '', '/pdp');

DYNAMIC_SCRIPT_CREATION — Dynamic script creation

Severity: Violation (block)
Files: .ts, .tsx

Restricted expressions: document.createElement, document.write, document.writeln, document.body.appendChild, document.head.appendChild.

Detection: Any call to these methods.

Correct

// Render UI via JSX; never inject scripts dynamically

Wrong

const script = document.createElement('script');
script.src = 'https://cdn.example.com/lib.js';
document.body.appendChild(script);

CODE_EXECUTION — Arbitrary code execution

Severity: Violation (block)
Files: .ts, .tsx

Restricted calls: eval, Function (as constructor), setTimeout (string form), setInterval (string form), new Function.

Detection: Any call or instantiation of these identifiers.

Note on setTimeout/setInterval: These are restricted in the sandbox to prevent arbitrary code injection. Use useEffect with AbortController or cleanup functions for timing-based logic:

Correct

useEffect(() => {
  const id = window.setTimeout(() => { /* logic */ }, 500);
  return () => window.clearTimeout(id);
}, []);
// Note: even this should be reviewed; prefer event-driven patterns

Wrong

eval('doSomething()');
const fn = new Function('return 42');
setTimeout('doSomething()', 1000);

RESTRICTED_IMPORT — Restricted Node.js module imports

Severity: Violation (block)
Files: .ts, .tsx

Restricted modules: fs, path, child_process, crypto.

Detection: Any import statement importing from these module names.

Correct

// Extensions are browser-only React components; do not import Node.js modules
import { useCart } from '@vtex/sales-app';

Wrong

import fs from 'fs';
import { resolve } from 'path';
import crypto from 'crypto';

RESTRICTED_IMPORT_SOURCE — Direct node_modules path imports

Severity: Violation (block)
Files: .ts, .tsx

Detection: Import paths containing node_modules/.

Wrong

import something from 'node_modules/lodash/get';

NON_APPROVED_PACKAGE — Import from non-approved package

Severity: Warning (flag)
Files: .ts, .tsx

Any import whose source starts with @ or contains /, unless it is react or a node_modules/ direct path. Fires for every @vtex/* scoped package and every relative import (./foo, ../bar).

Note: This warning is noisy — it fires for all legitimate Sales App imports (@vtex/sales-app, @phosphor-icons/react, relative component paths). Do not surface to the user when the import is from @vtex/* or a relative path. Investigate only when the import is from an unexpected third-party package.

Detection: import { … } from 'source' where source.startsWith('@') or source.includes('/'), and source is not 'react' or a node_modules/ path.


EXTERNAL_RESOURCE_LOADING — Unrestricted external resource loading

Severity: Violation (block)
Files: .ts, .tsx

Two detection paths:

  1. Direct API use: fetch, XMLHttpRequest, script.src, link.href used as member expressions.
  2. Known CDN domains in string literal: fetch('https://cdn.jsdelivr.net/...') or any URL containing cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, ajax.googleapis.com, code.jquery.com, api.example.com, or ending with .js.

Exception: fetch via the IO Proxy relative path (/_v/...) is allowed and required for secure API integration.

Correct

const response = await fetch('/_v/my-loyalty-api/points', {
  credentials: 'include',
});

Wrong

const response = await fetch('https://api.example.com/data');
const response = await fetch('https://cdn.jsdelivr.net/npm/lodash.js'); // CDN — also Violation
const xhr = new XMLHttpRequest();

EXTERNAL_RESOURCE_FETCH — Fetch to known CDN domains

Note: This rule ID is declared in the analyzer type system but is never raised by any handler. CDN domain fetches are actually reported as EXTERNAL_RESOURCE_LOADING (Violation, block) — not as this Warning. Treat any fetch to a CDN domain as a build-blocking Violation, not a soft warning.

Severity: Warning (flag) — see note above; actual enforcement is via EXTERNAL_RESOURCE_LOADING
Files: .ts, .tsx

Known third-party domains: cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, ajax.googleapis.com, code.jquery.com.

Detection: fetch() first argument literal containing one of these domains.


QUERY_SELECTOR_USAGE — DOM query selectors

Severity: Violation (block)
Files: .ts, .tsx

Restricted calls: document.getElementById, document.getElementsByClassName, document.getElementsByTagName, document.querySelector, document.querySelectorAll, and element-scoped variants.

Detection: Any call where the callee matches document.querySelector, document.getElementById, etc. (the variable must be literally named document). Calls via a ref object — e.g. containerRef.current.querySelector(...) — are not caught by the analyzer and must be avoided as a Hard Constraint.

Correct

// Use React refs for direct DOM access when absolutely needed
const ref = useRef<HTMLDivElement>(null);

Wrong

const el = document.querySelector('.my-class');
document.getElementById('cart-container');

STYLE_MANIPULATION — Direct style manipulation

Severity: Violation (block)
Files: .ts, .tsx

Restricted expressions: element.style, element.className, element.classList, element.setAttribute.style, element.setAttribute.class, element.hidden, element.display, element.visibility.

Detection: Matches only when the object is literally named element (e.g. element.style). Equivalent access through other variable names — e.g. divRef.current.style, el.className — is not caught by the analyzer and must be avoided as a Hard Constraint.

Correct

// Use plain CSS classes from the component stylesheet
import './MyExtension.css';

return <div className={isActive ? 'active' : 'inactive'} />;

Wrong

element.style.color = 'red';
element.classList.add('active');
element.setAttribute('style', 'display: none');

INFINITE_LOOP — Infinite loops

Severity: Violation (block)
Files: .ts, .tsx

Detection: while(true) or do...while(true) with literal true condition.

Wrong

while (true) {
  processItem();
}

POTENTIAL_INFINITE_LOOP — Incomplete for-loop

Severity: Warning (flag)
Files: .ts, .tsx

Detection: for statement with no update expression AND no test expression (e.g. for (let i = 0;;) { … }).

Action: Warn the user and ensure the loop has a proper termination condition.


MEMORY_LEAK — Memory leak patterns

Note: This rule ID is declared in the analyzer type system but is never raised by any handler. The active rule is POTENTIAL_MEMORY_LEAK (Warning) below. Event listeners without cleanup will not block the build, but remain a Hard Constraint — always add a cleanup return in useEffect.

Severity: Violation (block) — see note above; not currently enforced at build time
Files: .ts, .tsx

Detection: Event listeners, subscriptions, or timers added without cleanup in useEffect.

Correct

useEffect(() => {
  const handler = () => { /* ... */ };
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

Wrong

useEffect(() => {
  window.addEventListener('resize', handler);
  // Missing cleanup — memory leak
}, []);

POTENTIAL_MEMORY_LEAK — Likely memory leak patterns

Severity: Warning (flag)
Files: .ts, .tsx

Three triggers:

  1. Any addEventListener call — the analyzer warns regardless of whether a corresponding removeEventListener exists in cleanup.
  2. Recursive function call where the callee name matches the parent FunctionDeclaration identifier.
  3. Array.push inside a for or while loop.

Action: Verify the component has a proper cleanup return in useEffect for any event listener, and that recursive functions have a clear base case.

Correct

useEffect(() => {
  const handler = () => { /* ... */ };
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

EXCESSIVE_API_CALLS — Excessive API call patterns

Severity: Violation (block)
Files: .ts, .tsx

Detection: fetch or XMLHttpRequest calls whose parent is a for or while loop.

Wrong

// fetch inside loop — excessive calls
for (const item of items) {
  await fetch(`/_v/api/item/${item.id}`);
}

FREQUENT_API_CALLS — API call outside loop / rapid setInterval

Severity: Warning (flag)
Files: .ts, .tsx

Two triggers:

  1. fetch or XMLHttpRequest call outside a loop (i.e. anywhere else in the component). Fires for every fetch — ensure calls are wrapped in useEffect with proper deps and are rate-limited or cached.
  2. setInterval with a numeric delay argument below 1000 ms.

Note: Trigger 1 is intentionally broad — it flags every fetch() call as a reminder to verify it is not running unintentionally on every render. It does not mean every fetch is wrong. Only surface this to the user when the fetch is genuinely outside a useEffect or appears to run on each render.

Action: Warn the user and suggest wrapping fetches in useEffect, using caching, or increasing the polling interval.


LARGE_BUNDLE_SIZE_IMPACT — Bundle size impacts

Severity: Warning (flag)
Files: .ts, .tsx

Detection:

  • String literals longer than 1,000 characters
  • Object literals with more than 100 properties
  • Imports from heavyweight libraries: lodash, moment

Action: Warn the user and suggest alternatives (date-fns instead of moment, native array methods instead of lodash).

Correct

import { format } from 'date-fns';
const formattedDate = format(new Date(), 'yyyy-MM-dd');

Wrong

import moment from 'moment';
import _ from 'lodash';

2. CSS Containment Rules

Applied to .css, .scss, .less files by CSSAnalyzer.

Sales App analyzer configuration (from @vtex/sales-app build/dev hook):

cssOptions: {
  allowedNamespaces: ['sales-app-extension-'],
  defaultNamespace: 'sales-app-extension-',
  transformNonCompliant: true,
  overwriteTransformed: true,
}

Two consequences for the rules below:

  1. The configured namespace is sales-app-extension-, not the analyzer's bare default of extension-. Any rule that talks about "the allowed namespace" means sales-app-extension- in this project.
  2. transformNonCompliant: true softens the namespace rules: when a selector or @keyframes name is not prefixed, the analyzer auto-rewrites it into a sibling *.transformed.css file and emits a CSS_TRANSFORMED warning instead of a CSS_NAMESPACE_REQUIRED / CSS_GLOBAL_KEYFRAMES violation. The original .css (and the className strings in JSX) are left untouched, so the build does not break. This is why the CSS template in code-templates-and-patterns.md ships unprefixed selectors like .container, .title, .row — they are valid in this project.

The other CSS rules (CSS_GLOBAL_SELECTOR, CSS_CONTAINMENT_BREAKOUT, CSS_GLOBAL_IMPORT, CSS_RESTRICTED_PROPERTY, CSS_RESTRICTED_VALUE, and the inline-style rules) are not softened by transformNonCompliant — they remain build-blocking violations.

CSS_GLOBAL_SELECTOR — Global element selectors

Severity: Violation (block)
Files: .css

Restricted selectors: *, body, html, :root, head, main, #root, #__next.

Detection: Any CSS rule whose selector matches or contains these elements at the top level.

Correct

/* Scope rules inside the extension container — never target global elements */
.container {
  font-size: 14px;
}

.container .title {
  font-weight: bold;
}

Wrong

body {
  font-size: 14px;
}

* {
  box-sizing: border-box;
}

:root {
  --my-color: red;
}

CSS_CONTAINMENT_BREAKOUT — CSS containment breakout

Severity: Violation (block)
Files: .css

Restricted patterns: :host, :host-context, ::slotted, :global, position: fixed, position: absolute, z-index: 9999.

Detection: Any rule or declaration matching these patterns.

Note on positioning: position: relative and position: sticky within the extension container are generally safe. Avoid fixed and unbounded absolute that escape the extension boundary.

Correct

.container {
  position: relative; /* safe — contained */
}

Wrong

.overlay {
  position: fixed;
  z-index: 9999;
  top: 0;
  left: 0;
}

:host {
  display: block;
}

CSS_NAMESPACE_REQUIRED — Missing CSS namespace

Severity: Warning in this project (auto-transformed) — would be Violation if transformNonCompliant were false
Files: .css

All CSS selectors should be scoped under an allowed namespace prefix. In the Sales App, that prefix is sales-app-extension-. Because the project sets transformNonCompliant: true, an unprefixed selector does not block the build: the analyzer rewrites it inside a sibling Component.transformed.css and emits a CSS_TRANSFORMED warning instead of CSS_NAMESPACE_REQUIRED. The original Component.css (which is what gets bundled) keeps the unprefixed class, so the JSX className="container" continues to match.

Detection: A selector that does not include any value from allowedNamespaces (substring check on .namespace, #namespace, or [class*="namespace"]).

Correct (and matches the CSS template)

/* Unprefixed top-level classes are accepted because transformNonCompliant=true. */
.container { padding: 8px; }
.title     { font-size: 16px; }

Also correct (explicit prefix, never warns)

.sales-app-extension-container { padding: 8px; }
.sales-app-extension-title     { font-size: 16px; }

Prefer the unprefixed form to match the CSS template in code-templates-and-patterns.md and the JSX in the component templates. The CSS_TRANSFORMED warning that fires for these is informational only.


CSS_GLOBAL_IMPORT — CSS @import rules

Severity: Violation (block)
Files: .css

@import pulls external stylesheets that affect global styles and bypass containment.

Detection: Any @import at-rule anywhere in the CSS file.

Correct

/* Import a local CSS variable file via bundler config, not @import */

Wrong

@import url('https://fonts.googleapis.com/css2?family=Roboto');
@import './reset.css';

CSS_GLOBAL_KEYFRAMES — Keyframes without namespace

Severity: Warning in this project (auto-transformed) — would be Violation if transformNonCompliant were false
Files: .css

@keyframes names are global. Without a namespace they may conflict with other extensions or the Sales App shell. As with CSS_NAMESPACE_REQUIRED, transformNonCompliant: true rewrites the name in *.transformed.css and emits CSS_TRANSFORMED instead of blocking.

Detection: @keyframes whose name does not start with an allowed namespace prefix (sales-app-extension- in this project).

The CSS template prefixes keyframes with ${COMPONENT_NAME}- (e.g. LoyaltyPoints-spin). That prefix does not start with sales-app-extension-, so it triggers a CSS_TRANSFORMED warning — the build still succeeds, but the transformed file will rename it to sales-app-extension-LoyaltyPoints-spin. Either form is acceptable.

Correct (no warning)

@keyframes sales-app-extension-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

Also correct (CSS_TRANSFORMED warning, build passes)

@keyframes LoyaltyPoints-spin {
  to { transform: rotate(360deg); }
}

CSS_RESTRICTED_PROPERTY — Restricted CSS properties

Severity: Violation (block)
Files: .css

Certain properties affect layout or rendering globally and are restricted to prevent layout shift or containment breakout.

Detection: Declarations using restricted property names (e.g., properties that set global layout hints).

Action: If detected, remove or scope the declaration appropriately.


CSS_RESTRICTED_VALUE — Restricted CSS values

Severity: Violation (block)
Files: .css

Certain CSS values (e.g., !important) override the cascade and break containment guarantees.

Detection: !important in any declaration, or values targeting global layout tokens.

Correct

.button {
  color: var(--sa-color-primary);
}

Wrong

.button {
  color: red !important;
}

CSS_RESTRICTED_INLINE_PROPERTY — Restricted property in inline style

Severity: Violation (block)
Files: .tsx

Inline style={{ … }} props in JSX may not use properties that affect layout or containment.

Restricted properties: position, z-index, top, left, right, bottom, all, contain, content, isolation.

Detection: A JSX style prop object containing one of the restricted property names.

Correct

// Use CSS classes for layout properties
return <div className="overlay" />;

Wrong

return <div style={{ position: 'fixed', top: 0 }} />;

CSS_RESTRICTED_INLINE_VALUE — Restricted value in inline style

Severity: Violation (block)
Files: .tsx

Inline style={{ … }} props may not use values that break containment or cascade.

Restricted values: fixed, absolute, !important, inherit, initial, unset.

Detection: A JSX style prop object containing one of the restricted values.

Wrong

return <div style={{ position: 'fixed' }} />;
return <div style={{ display: 'inherit' }} />;

3. React Performance Rules

Applied to .tsx files by ReactPerformanceAnalyzer. Violations block presentation; warnings are flagged to the user.

REACT_UNNECESSARY_RERENDER — State update inside loop

Severity: Violation (block)
Files: .tsx

State setter calls inside for or while loops trigger a re-render on every iteration.

Detection: A setState / set* call whose parent node is a for or while statement.

Correct

// Compute derived value, then set once
const updatedItems = items.map(transform);
setItems(updatedItems);

Wrong

for (const item of items) {
  setCount((c) => c + 1); // re-renders on every iteration
}

REACT_ANONYMOUS_COMPONENT — Anonymous (lowercase) component

Severity: Violation (block)
Files: .tsx

Components assigned to variables starting with lowercase are treated as HTML tags by React and cannot be used in JSX.

Detection: Arrow function or function expression assigned to a variable that does not start with an uppercase letter.

Correct

export function LoyaltyPoints(): JSX.Element { ... }
export const WarrantyBadge = (): JSX.Element => { ... };

Wrong

const loyaltyPoints = (): JSX.Element => { ... };
const warrantyBadge = function() { ... };

REACT_DIRECT_DOM_MANIPULATION — Direct DOM manipulation in React

Severity: Violation (block)
Files: .tsx

Bypasses the React virtual DOM and causes unpredictable behavior.

Detection: Calls to document.querySelector or document.getElementById inside a React component.

Correct

const containerRef = useRef<HTMLDivElement>(null);
// Access DOM via containerRef.current

Wrong

const el = document.querySelector('.cart-list');
el.style.display = 'none'; // bypasses React

REACT_LAYOUT_EFFECT_MISUSE — useLayoutEffect usage

Severity: Violation (block)
Files: .tsx

useLayoutEffect blocks the browser paint and can cause performance issues in a sandboxed extension.

Detection: Any call to useLayoutEffect.

Correct

useEffect(() => {
  // DOM reading/writing after paint
}, []);

Wrong

useLayoutEffect(() => {
  // blocks paint
}, []);

REACT_MISSING_DEPS — Missing dependency array

Severity: Warning (flag)
Files: .tsx

useEffect, useCallback, or useMemo without a second argument re-runs on every render.

Detection: A call to one of these hooks with fewer than 2 arguments, or with a non-array second argument.

Correct

useEffect(() => {
  fetchData();
}, [productId]); // explicit deps

Wrong

useEffect(() => {
  fetchData(); // runs on every render
});

REACT_INLINE_FUNCTION — Inline functions in JSX props

Severity: Warning (flag)
Files: .tsx

Arrow functions defined directly in JSX attributes create a new function reference on every render, preventing child memoization.

Detection: Function expression or arrow function expression whose parent is a JSX expression container or prop.

Correct

const handleClick = useCallback(() => {
  doSomething();
}, []);

return <button onClick={handleClick}>Click</button>;

Wrong

return <button onClick={() => doSomething()}>Click</button>;

REACT_LARGE_COMPONENT — Component over 200 lines

Severity: Warning (flag)
Files: .tsx

Large components are harder to maintain and test. Threshold: 200 lines.

Detection: A function/arrow function with start/end line difference > 200.

Action: Suggest splitting into smaller sub-components or extracting logic into custom hooks.


REACT_MANY_USESTATE — More than 5 useState calls

Severity: Warning (flag)
Files: .tsx

Many useState calls in a single component indicate complex local state. Threshold: 5 calls.

Detection: More than 5 calls to useState (or React.useState) within a single function component.

Action: Suggest consolidating related state into a single useState with an object, or extracting to a custom hook.

Correct

const [formState, setFormState] = useState({
  name: '',
  email: '',
  quantity: 1,
});

Wrong

const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [submitted, setSubmitted] = useState(false); // 6th useState — flagged

REACT_CONDITIONAL_HOOK — Hook called conditionally

Severity: Warning (flag)
Files: .tsx

Calling a hook inside an if, switch, or ternary expression violates the Rules of Hooks.

Detection: A hook call (useEffect, useState, useMemo, useCallback, useLayoutEffect) whose ancestor is an IfStatement, SwitchStatement, or ConditionalExpression.

Correct

const cart = useCart(); // always called unconditionally

if (!cart) return <></>;

Wrong

if (isEnabled) {
  const cart = useCart(); // conditional hook call — violates Rules of Hooks
}

REACT_COMPLEX_JSX — Deep JSX nesting

Severity: Warning (flag)
Files: .tsx

Deep JSX trees are harder to read and may indicate a need to extract sub-components. Threshold: 5 levels deep.

Detection: A JSX element whose JSX child tree exceeds 5 levels of nesting.

Action: Extract inner JSX into a named sub-component.


REACT_UNOPTIMIZED_LIST — List rendering without key or large list

Severity: Warning (flag)
Files: .tsx

Missing key props in list rendering causes React reconciliation issues. Lists over 50 items may need virtualization.

Detection:

  • map() producing JSX without a key attribute on the root element
  • List size exceeding 50 items

Correct

return (
  <ul>
    {items.map((item) => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
);

Wrong

return (
  <ul>
    {items.map((item) => (
      <li>{item.name}</li> // missing key
    ))}
  </ul>
);

REACT_COMPLEX_STATE — useState with complex object

Severity: Warning (flag)
Files: .tsx

useState initialized with an object having more than 5 properties suggests a need for useReducer. Threshold: 5 properties.

Detection: useState(obj) where obj is an object literal with > 5 properties.

Action: Suggest converting to useReducer for complex state.


REACT_NESTED_UPDATE — Nested state updates

Severity: Warning (flag)
Files: .tsx

Calling a state setter from within another state setter's callback can cause unexpected re-renders.

Detection: A set* call whose ancestor is another set* call expression.


Validation Output Format

When reporting issues after code review, group by file and severity:

### Static Analysis Results

**Violations (must fix before completing Step 3):**
- [RULE_ID] File: ComponentName.tsx, Line ~N — <message>
  Fix: <suggested fix>

**Warnings (review with user):**
- [RULE_ID] File: ComponentName.tsx, Line ~N — <message>
  Suggestion: <improvement>

✅ No violations found. N warnings flagged.

Do not proceed with Step 4 (Documentation) until all violations are resolved.

How do I install Sales app extensibility in Cursor, Claude Code, or Codex?

Run npx skills add vtex/skills --skill sales-app-extensibility in the project where you want it, then ask your agent for the skill by name. The --skill flag installs only Sales app extensibility, not every skill in the repository.

Where does Sales app extensibility come from and what license is it under?

Sales app extensibility comes from the vtex/skills repository on GitHub. That repository has 40 GitHub stars. No license was detected on the source repository, so check with the author before redistributing it.

Prefer plain text? Read the Sales app extensibility guide as markdown.