| name | deco-apps-vtex-review |
| description | Audit and fix the VTEX integration in @decocms/apps-start (TanStack Start). Covers cookie propagation (vtexFetchWithCookies, buildAuthCookieHeader), expectedOrderFormSections, salesChannel injection, HttpOnly cookie handling, Intelligent Search cookie generation, useCart/useUser/useWishlist hooks, and TypeScript validation. Use when reviewing vtex/ code quality, fixing authentication issues, debugging missing cart sections, or ensuring full parity with deco-cx/apps. |
VTEX apps-start Review & Fix
Comprehensive audit checklist for the VTEX integration in @decocms/apps-start. Use after porting or when debugging issues.
File Structure
apps-start/vtex/
├── client.ts # vtexFetch, vtexFetchWithCookies, intelligentSearch, vtexIOGraphQL
├── middleware.ts # extractVtexContext, propagateISCookies
├── actions/
│ ├── checkout.ts # Cart mutations (addItems, updateItems, etc.)
│ ├── auth.ts # classicSignIn, logout, sendEmailVerification
│ ├── session.ts # createSession, editSession, deleteSession
│ ├── address.ts # GraphQL address mutations
│ ├── misc.ts # notifyMe, sendEvent, submitReview, deletePaymentToken
│ ├── newsletter.ts # subscribe, updateNewsletterOptIn
│ ├── orders.ts # cancelOrder
│ ├── profile.ts # updateProfile, updateAddress
│ ├── wishlist.ts # addItem, removeItem
│ └── trigger.ts # Analytics trigger
├── loaders/
│ ├── cart.ts # getCart (OrderForm)
│ ├── catalog.ts # searchProducts, getCrossSelling, getCategoryTree
│ ├── legacy.ts # legacyProductDetailsPage, legacyProductList, legacyPLP, legacySuggestions
│ ├── workflow.ts # workflowProduct, workflowProducts
│ ├── search.ts # getTopSearches, getProductIdByTerm
│ └── (14 more)
├── inline-loaders/ # TanStack-compatible loaders for sections
├── hooks/ # Client-side React hooks (useCart, useUser, useWishlist)
└── utils/
├── transform.ts # Canonical VTEX→schema.org mapping
├── types.ts # VTEX API types
├── vtexId.ts # VTEX_AUTH_COOKIE, buildAuthCookieHeader
├── segment.ts # buildSegmentFromCookies, isAnonymous
├── intelligentSearch.ts # withDefaultParams, withDefaultFacets
├── similars.ts # withIsSimilarTo
└── enrichment.ts # withSimulation
Audit Checklist
1. Cookie Propagation
VTEX APIs return Set-Cookie headers that must reach the browser. Standard vtexFetch discards them.
Pattern: Use vtexFetchWithCookies for any action that creates/modifies server state:
export interface VtexFetchResult<T> {
data: T;
setCookies: string[];
}
export async function vtexFetchWithCookies<T>(
path: string,
init?: RequestInit,
): Promise<VtexFetchResult<T>> {
const response = await vtexFetchResponse(path, init);
const data = await response.json() as T;
const setCookies: string[] = [];
response.headers.forEach((value, key) => {
if (key.toLowerCase() === "set-cookie") setCookies.push(value);
});
if (setCookies.length === 0 && typeof response.headers.getSetCookie === "function") {
setCookies.push(...response.headers.getSetCookie());
}
return { data, setCookies };
}
import { vtexFetchWithCookies } from "../client";
import type { VtexFetchResult } from "../client";
const result = await vtexFetchWithCookies<OrderForm>(url, opts);
Where required: checkout.ts (all cart mutations), session.ts (create/edit), auth.ts (signIn, logout).
Where NOT needed: Read-only loaders, GraphQL queries.
Server→browser bridge — the cookies that vtexFetchWithCookies captures must be forwarded onto the outgoing HTTP response, or the browser never sees them and the cart appears empty on the next request. There are two bridge points in a TanStack Start site, and both must be wired:
-
src/server/invoke.gen.ts (TanStack RPC path). Generated by bunx tsx node_modules/@decocms/blocks-cli/scripts/generate-invoke.ts. Audit:
- File exists?
- Contains
function forwardResponseCookies()?
- Every action handler calls
forwardResponseCookies() after the await?
If any answer is "no", regenerate with the script above. Then make sure useCart, useUser, useWishlist import invoke from ~/server/invoke.gen (or a barrel that re-exports it), not from the proxy ~/runtime.ts.
-
packages/blocks-admin/src/admin/invoke.ts (/deco/invoke/... HTTP path, mounted into the site via @decocms/blocks-admin). The single + batch invoke handlers must use Headers.getSetCookie() (not entries()!) when copying RequestContext.responseHeaders onto the response, via forwardCtxHeadersTo(). None of the @decocms/* packages are published yet — everything sits at 0.0.0 and consuming sites link against a local checkout via bun link (see the repo's README "Local development" section). There's no version to pin; just confirm the linked checkout's admin/invoke.ts has the getSetCookie()-based fix.
The historical failure mode: a for…of headers.entries() loop collapsed N Set-Cookie values into one comma-joined string, which browsers silently discard. Every VTEX cart action returns 3–5 cookies (checkout.vtex.com__orderFormId, segment, sc, vtex_session…), so even one collapse breaks the entire cart flow.
Quick diagnosis: Add an item, watch DevTools → Network → the cart-action response should have multiple Set-Cookie: rows, not one comma-joined line.
Pitfall: never copy with Headers.entries() or forEach
When a caller pushes the captured cookies onto the request scope's response headers (e.g. RequestContext.responseHeaders.append("set-cookie", c)), the eventual HTTP-response bridge must read them back with Headers.getSetCookie(). Do not iterate headers.entries() (or forEach) and re-append — both collapse multiple Set-Cookie values into a single comma-joined string, which browsers silently discard. The result: the cart appears empty after addItemToCart even though the action returned an OrderForm with items.
Client-side setOrderFormIdCookie is defense-in-depth, not the fix
Some migrated useCart hooks manually call document.cookie = "checkout.vtex.com__orderFormId=..." after each cart action. That only patches one cookie — segment, sc, vtex_session etc. are still dropped. Keep the workaround if you like (cheap, idempotent), but the real fix is the two server-side bridges above. Once both are in place, the manual cookie write can be removed without regressing the cart.
2. Auth Cookie Headers
All authenticated VTEX IO GraphQL calls need both cookie variants:
VtexIdclientAutCookie={token}; VtexIdclientAutCookie_{account}={token}
Implementation (vtexId.ts):
export const VTEX_AUTH_COOKIE = "VtexIdclientAutCookie";
export function buildAuthCookieHeader(authCookie: string, account: string): string {
if (authCookie.includes("=")) return authCookie;
return `${VTEX_AUTH_COOKIE}=${authCookie}; ${VTEX_AUTH_COOKIE}_${account}=${authCookie}`;
}
Use the centralized helper:
import { buildAuthCookieHeader, VTEX_AUTH_COOKIE } from "../utils/vtexId";
import { getVtexConfig } from "../client";
const { account } = getVtexConfig();
const cookieHeader = buildAuthCookieHeader(authCookie, account);
Files that use this pattern:
actions/address.ts — gql helper
actions/misc.ts — gql helper
actions/newsletter.ts — gql helper
actions/profile.ts — gql helper
actions/wishlist.ts — buildCookieHeader
actions/session.ts — deleteSession
utils/enrichment.ts — simulation auth
Files that use VTEX_AUTH_COOKIE directly (as header name, not cookie):
actions/misc.ts — submitReview sends { [VTEX_AUTH_COOKIE]: authCookie } as HTTP header (Reviews API quirk)
Audit: grep for hardcoded VtexIdclientAutCookie strings. Only vtexId.ts should define it.
rg "VtexIdclientAutCookie" vtex/ --glob '!vtex/utils/vtexId.ts'
Any match outside vtexId.ts (except JSDoc comments) is a bug.
3. expectedOrderFormSections
VTEX Checkout API returns incomplete OrderForm without explicit sections. Every POST to /api/checkout/pub/orderForm must include:
import { DEFAULT_EXPECTED_SECTIONS } from "../actions/checkout";
body: JSON.stringify({ expectedOrderFormSections: DEFAULT_EXPECTED_SECTIONS })
Full list (actions/checkout.ts):
export const DEFAULT_EXPECTED_SECTIONS = [
"items",
"totalizers",
"clientProfileData",
"shippingData",
"paymentData",
"sellers",
"messages",
"marketingData",
"clientPreferencesData",
"storePreferencesData",
"giftRegistryData",
"ratesAndBenefitsData",
"openTextField",
"commercialConditionData",
"customData",
];
Audit: Check loaders/cart.ts and hooks/useCart.ts — both must send this body. Also used in actions/checkout.ts (all cart mutations).
4. salesChannel (sc) Parameter
Missing sc causes wrong prices, ORD027, or invisible products.
Where required:
- All
/api/checkout/pub/orderForm/* endpoints → ?sc={sc}
/api/catalog_system/pub/products/search/* → ?sc={sc}
/buscaautocomplete → &sc={sc}
- Intelligent Search: handled by
client.ts intelligentSearch() automatically
Injection points:
| Component | How sc is injected |
|---|
client.ts intelligentSearch | Auto from getVtexConfig().salesChannel |
hooks/useCart.ts | Reads VTEXSC cookie via document.cookie |
loaders/cart.ts | From getVtexConfig().salesChannel |
loaders/catalog.ts | From getVtexConfig().salesChannel |
loaders/legacy.ts | buildSearchParams() includes sc |
actions/checkout.ts | Helper scParam() / appendSc() |
middleware.ts | Reads VTEXSC cookie from request |
Audit:
rg "catalog_system/pub/products/search|buscaautocomplete|orderForm" vtex/ | rg -v "sc="
5. Intelligent Search Cookies
VTEX IS requires vtex_is_session and vtex_is_anonymous cookies (UUIDs).
Pattern in middleware.ts:
if (!cookieHeader.includes("vtex_is_session")) {
const sessionId = crypto.randomUUID();
}
Or, generating from existing request cookies:
const vtexIsSession = cookies.get("vtex_is_session") ?? crypto.randomUUID();
const vtexIsAnonymous = cookies.get("vtex_is_anonymous") ?? crypto.randomUUID();
Pass to intelligentSearch() via opts.cookieHeader:
const data = await intelligentSearch<T>(path, params, {
cookieHeader: `vtex_is_session=${session}; vtex_is_anonymous=${anonymous}`,
locale: "pt-BR",
});
6. HttpOnly Cookies
VtexIdclientAutCookie is HttpOnly — cannot be read via document.cookie.
Wrong: Client-side hooks checking document.cookie for auth status.
Correct: useUser calls /api/sessions?items=profile.email server-side.
async function fetchUser(): Promise<VtexUser> {
const res = await fetch(
"/api/sessions?items=profile.email,profile.firstName,profile.lastName,profile.id",
{ credentials: "include" },
);
}
7. Hooks Completeness
Compare with original deco-cx/apps hooks:
| Hook | Must Have |
|---|
useCart | addItems, updateQuantity, removeItem, addCoupons, fetchCart |
useUser | Server-side session check via /api/sessions |
useWishlist | add, remove, toggle, isInWishlist |
8. transform.ts Parity
All exported functions must match the original:
toProduct, toProductPage, pickSku, aggregateOffers, forceHttpsOnAssets,
sortProducts, filtersFromURL, mergeFacets, legacyFacetToFilter,
toFilter, categoryTreeToNavbar, toBrand, toReview, toInventories,
toPlace, toPostalAddress, parsePageType, normalizeFacet
Critical: seller: sellerId (not sellerName) in buildOffer.
9. Page Structure (schema.org)
| Page | Required Structure |
|---|
| PDP | ProductDetailsPage with breadcrumbList + product (via toProductPage) + seo |
| PLP | ProductListingPage with BreadcrumbList + filters + products + pageInfo + sortOptions + seo |
10. No Debug Logs in Production
rg "console\.log" vtex/ --glob '*.ts'
Only acceptable: 1x startup log in client.ts. All others should be console.error or console.warn in catch blocks.
Common Fixes
Fix: Header uses string instead of constant
headers: { VtexidClientAutCookie: authCookie }
import { VTEX_AUTH_COOKIE } from "../utils/vtexId";
headers: { [VTEX_AUTH_COOKIE]: authCookie }
Fix: Missing expectedOrderFormSections
await vtexFetch<OrderForm>(`/api/checkout/pub/orderForm`, { method: "POST", headers });
import { DEFAULT_EXPECTED_SECTIONS } from "../actions/checkout";
await vtexFetch<OrderForm>(`/api/checkout/pub/orderForm`, {
method: "POST", headers,
body: JSON.stringify({ expectedOrderFormSections: DEFAULT_EXPECTED_SECTIONS }),
});
Fix: Missing salesChannel in catalog
return vtexFetch<T[]>(`/api/catalog_system/pub/products/search/?${params}`);
const { salesChannel } = getVtexConfig();
if (salesChannel) params.set("sc", salesChannel);
return vtexFetch<T[]>(`/api/catalog_system/pub/products/search/?${params}`);
Validation
After all fixes, run:
npx -p typescript tsc --noEmit
rg "VtexIdclientAutCookie" vtex/ --glob '!vtex/utils/vtexId.ts' --glob '!*.md'
rg "console\.log" vtex/ --glob '*.ts' --glob '!client.ts'
rg "\s+$" vtex/ --glob '*.ts'
All must return 0 results (except TypeScript which exits 0).