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";
// Returns { data: T, setCookies: string[] }
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 bybunx tsx node_modules/@decocms/blocks-cli/scripts/generate-invoke.ts. Audit:- File exists?
- Contains
function forwardResponseCookies()? - Every action handler calls
forwardResponseCookies()after theawait?
If any answer is "no", regenerate with the script above. Then make sure
useCart,useUser,useWishlistimportinvokefrom~/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 useHeaders.getSetCookie()(notentries()!) when copyingRequestContext.responseHeadersonto the response, viaforwardCtxHeadersTo(). None of the@decocms/*packages are published yet — everything sits at0.0.0and consuming sites link against a local checkout viabun link(see the repo's README "Local development" section). There's no version to pin; just confirm the linked checkout'sadmin/invoke.tshas thegetSetCookie()-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);
// Pass as: { cookie: cookieHeader } or { Cookie: cookieHeader }
Files that use this pattern:
actions/address.ts— gql helperactions/misc.ts— gql helperactions/newsletter.ts— gql helperactions/profile.ts— gql helperactions/wishlist.ts— buildCookieHeaderactions/session.ts— deleteSessionutils/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.tsintelligentSearch()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:
// Generate if missing
if (!cookieHeader.includes("vtex_is_session")) {
const sessionId = crypto.randomUUID();
// Set on response
}
Or, generating from existing request cookies:
// middleware.ts
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.
// useUser.ts — correct pattern
async function fetchUser(): Promise<VtexUser> {
const res = await fetch(
"/api/sessions?items=profile.email,profile.firstName,profile.lastName,profile.id",
{ credentials: "include" },
);
// Parse session response for user data
}
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
// Before
headers: { VtexidClientAutCookie: authCookie }
// After
import { VTEX_AUTH_COOKIE } from "../utils/vtexId";
headers: { [VTEX_AUTH_COOKIE]: authCookie }
Fix: Missing expectedOrderFormSections
// Before
await vtexFetch<OrderForm>(`/api/checkout/pub/orderForm`, { method: "POST", headers });
// After
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
// Before
return vtexFetch<T[]>(`/api/catalog_system/pub/products/search/?${params}`);
// After
const { salesChannel } = getVtexConfig();
if (salesChannel) params.set("sc", salesChannel);
return vtexFetch<T[]>(`/api/catalog_system/pub/products/search/?${params}`);
Validation
After all fixes, run:
# TypeScript
npx -p typescript tsc --noEmit
# No hardcoded cookie strings
rg "VtexIdclientAutCookie" vtex/ --glob '!vtex/utils/vtexId.ts' --glob '!*.md'
# No debug logs
rg "console\.log" vtex/ --glob '*.ts' --glob '!client.ts'
# No trailing whitespace
rg "\s+$" vtex/ --glob '*.ts'
All must return 0 results (except TypeScript which exits 0).