# Deco Apps Vtex Review

> 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.

- Skill: `decocms/deco-apps-vtex-review` (Agent Skill)
- Install (CLI): `npx skillmds@latest add decocms/deco-apps-vtex-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/decocms/deco-apps-vtex-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: decocms (https://skillmd.com/u/decocms)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/decocms/deco-apps-vtex-review

---


# 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:

```typescript
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 };
}
```

```typescript
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:

1. **`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`.

2. **`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`):

```typescript
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**:

```typescript
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 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.

```bash
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:

```typescript
import { DEFAULT_EXPECTED_SECTIONS } from "../actions/checkout";

body: JSON.stringify({ expectedOrderFormSections: DEFAULT_EXPECTED_SECTIONS })
```

**Full list** (`actions/checkout.ts`):

```typescript
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**:

```bash
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**:

```typescript
// Generate if missing
if (!cookieHeader.includes("vtex_is_session")) {
  const sessionId = crypto.randomUUID();
  // Set on response
}
```

Or, generating from existing request cookies:

```typescript
// 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`:

```typescript
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.

```typescript
// 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

```bash
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

```typescript
// Before
headers: { VtexidClientAutCookie: authCookie }
// After
import { VTEX_AUTH_COOKIE } from "../utils/vtexId";
headers: { [VTEX_AUTH_COOKIE]: authCookie }
```

### Fix: Missing expectedOrderFormSections

```typescript
// 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

```typescript
// 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:

```bash
# 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).

