| 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:
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/start/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.
-
@decocms/start/src/admin/invoke.ts (/deco/invoke/... HTTP path). The framework's single + batch invoke handlers must use Headers.getSetCookie() (not entries()!) when copying RequestContext.responseHeaders onto the response. Pin to a version ≥ 5.0.0 that ships forwardCtxHeadersTo.
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.
2. Auth Cookie Headers
All authenticated VTEX IO GraphQL calls need both cookie variants:
VtexIdclientAutCookie={token}; VtexIdclientAutCookie_{account}={token}
Use the centralized helper:
import { buildAuthCookieHeader, VTEX_AUTH_COOKIE } from "../utils/vtexId";
import { getVtexConfig } from "../client";
const { account } = getVtexConfig();
const cookieHeader = buildAuthCookieHeader(authCookie, account);
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 })
Audit: Check loaders/cart.ts and hooks/useCart.ts — both must send this body.
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
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();
}
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.
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).