npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

brainerce

v1.33.0

Published

Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.

Readme

brainerce

Official SDK for building e-commerce storefronts with Brainerce Platform.

This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts to connect to Brainerce's unified commerce API.

AI Agents / Vibe Coders (Cursor, Lovable, Claude Code, VS Code): Use the MCP server for AI-powered store building: npx @brainerce/mcp-server. It provides docs, code templates, and live store capabilities directly inside your IDE. Note: the MCP server runs inside your IDE — it is not available in chat-only tools like Google AI Studio or ChatGPT.

Two SDK modes — choose the right one

| Mode | Config key | Use for | Where to run | | -------------- | ------------------------ | ---------------------------------- | ----------------------------------- | | Storefront | salesChannelId: 'vc_*' | Building the customer-facing store | Browser / client-side | | Admin | apiKey: 'brainerce_*' | Managing products, team, settings | Server only — never in browser code |

Building a storefront? You only need your Sales Channel ID (vc_*) from the Brainerce dashboard under Sales Channels. No API key needed. API keys are a server-side admin secret.

Installation

npm install brainerce
# or
pnpm add brainerce
# or
yarn add brainerce

What You Must Build

Every Brainerce storefront must include all mandatory features below. Features auto-hide when the underlying capability is disabled, so build them all anyway — they'll appear the moment the store owner enables them.

| Feature | SDK entry point | Mandatory | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------- | | Product list with search, filter, pagination | client.getProducts(), client.getSearchSuggestions(query) | ✅ | | Product detail with variant picker, stock, price | client.getProductBySlug() + helpers | ✅ | | Buyer customization fields (engraving, uploads, select) | product.customizationFields, client.uploadCustomizationFile() | ✅ | | Cart (add, update, remove, coupon, totals) | client.addToCart(), getCartTotals(cart) | ✅ | | Inventory reservation countdown | Cart expiry timestamp from client.getCart() | ✅ | | Full checkout end-to-end with payment | setShippingAddress → selectShippingMethod → getPaymentProviders → pay → handlePaymentSuccess → waitForOrder | ✅ | | Order confirmation (clear cart + wait for real order) | client.handlePaymentSuccess(), client.waitForOrder() | ✅ | | Register + email verification flow | client.registerCustomer(), client.verifyEmail() | ✅ | | Login + verification branch | client.loginCustomer() | ✅ | | Forgot / reset password | client.forgotPassword(), client.resetPassword() | ✅ | | OAuth sign-in buttons + callback handler | client.getAvailableOAuthProviders() | ✅ | | Account area (profile + order history) | client.getMyProfile(), client.getMyOrders() | ✅ | | Global header: cart count + search autocomplete | client.getCart(), client.getSearchSuggestions(query) | ✅ | | Discount banners + product badges | client.getDiscountBanners(), client.getProductDiscountBadge(productId) | ✅ | | Product reviews on PDP + JSON-LD aggregateRating | client.listProductReviews(id), client.submitProductReview(id, …) | ✅ | | Site chrome (header + footer + announcement bar) | client.content.header.get(), client.content.footer.get(), client.content.announcement.list() | ✅ | | FAQ page | client.content.faq.get('main', locale) | conditional | | Static pages catch-all (/pages/[slug]) | client.content.page.getBySlug(slug, locale) | conditional | | Multi-language + RTL (when i18n enabled) | client.setLocale(), client.getStoreDirection(locale) | conditional |


Critical Rules

Violating any of these causes production incidents or broken orders. Read them before writing SDK code.

SDK usage

  • ALWAYS call SDK client methods. Never reconstruct REST URLs or call fetch directly.
  • NEVER invent SDK method names. If it's not in this README or in get-sdk-docs, it doesn't exist.
  • NEVER hardcode product data, categories, or store copy — Brainerce is the database.
  • NEVER use submitGuestOrder() or createOrder() — they bypass payment and produce unpaid orders.
  • ALWAYS use SDK helpers (getCartTotals, formatPrice, getProductPriceInfo, getCartItemImage, getCartItemName, getVariantPrice, getStockStatus, getDescriptionContent) instead of reading raw fields.

State management

  • The SDK manages cart, checkout, and session state. Do NOT duplicate it in your own Redux/context.
  • Product lists, categories, and inventory counts are NOT client state — fetch on demand.
  • Discount rules and coupon validity are evaluated server-side. Never re-implement them client-side.

Authentication

  • ALWAYS handle the requiresVerification flag in registerCustomer and loginCustomer responses. If true, route to the verify-email step BEFORE treating the user as logged in.
  • ALWAYS build the verify-email, forgot-password, and reset-password flows even when the store currently has email verification disabled. They auto-hide when unused.
  • ALWAYS build OAuth button placeholders and a callback handler even when no OAuth provider is configured.
  • NEVER silently swallow auth errors. Render the specific error (invalid credentials, expired token, rate limited).

Checkout & orders

  • The checkout sequence is strict: setShippingAddress → pick a shipping rate → getPaymentProviders → provider payment → handlePaymentSuccesswaitForOrder. Never skip or reorder.
  • ALWAYS call handlePaymentSuccess(checkoutId) on the confirmation page — clears the cart so users don't see stale items.
  • ALWAYS call waitForOrder(checkoutId) to poll for the real order before showing an order number. The payment callback may return before the order record exists.
  • NEVER use the checkout total as the cart total — they diverge (tax, shipping, discounts). Display checkout.lineItems on the summary, not cart.items.
  • The reservation timer is a hard guarantee — display the countdown from the cart and let the SDK handle expiry.

Token handling

  • Customer auth tokens (result.token from loginCustomer/registerCustomer) should be passed to client.setCustomerToken(token). The SDK stores session state internally.
  • NEVER put the admin API key (brainerce_*) in client code. It is a server-only secret.
  • OAuth callbacks arrive with a one-time auth_code URL param. Call client.exchangeOAuthCode(authCode) to swap it for the JWT and apply via setCustomerToken. (The legacy ?token= URL param is still emitted for backward compatibility but will be removed in the next major release.)

i18n

  • NEVER hardcode currency, locale, or language strings — read them from getStoreInfo().
  • NEVER format prices with toFixed(2) — use formatPrice() from the SDK.
  • When i18n is enabled, call client.setLocale(locale) at app init and include a language switcher. For RTL locales (he, ar), set <html dir="rtl"> — do NOT add flex-row-reverse on top.

Type safety

  • NEVER use as any or as unknown as. Fix the type, don't hide it.
  • NEVER write your own copies of SDK types (Cart, Product, Order). Import from 'brainerce'.
  • All prices are STRINGS — always parseFloat() before math or comparisons.
  • CartItem / CheckoutLineItem = NESTED (item.product.name, item.unitPrice). OrderItem = FLAT (item.name, item.price). Not interchangeable.
  • Cart has no .total field — call getCartTotals(cart).

Business Flows

These sequences are non-negotiable. The order of SDK calls matters.

Checkout flow

  1. Collect customer email, billing address, shipping address (line1, line2, city, region, postalCode, country). email is required.
  2. Submit address to get shipping rates:
    const { checkout, rates } = await client.setShippingAddress(checkoutId, {
      email,
      firstName,
      lastName,
      line1,
      city,
      region,
      postalCode,
      country,
    });
    // rates = available shipping rates; checkout = updated checkout object
  3. Let the customer pick a rate, then persist it:
    await client.selectShippingMethod(checkoutId, rateId);
  4. Fetch available payment providers:
    const providers = await client.getPaymentProviders();
    Each provider has a renderType'sdk-widget' (Stripe, PayPal, Grow), 'iframe' (Cardcom), 'redirect', 'sandbox'. Branch on renderType, never on provider name.
  5. Confirm payment using the provider's flow (Stripe Elements stripe.confirmCardPayment, PayPal button, redirect, etc.).
  6. On the confirmation page, always call both:
    await client.handlePaymentSuccess(checkoutId); // clears cart
    const order = await client.waitForOrder(checkoutId); // polls until order exists
  7. Display checkout.lineItems (not cart.items) on the order summary.

Registration flow

  1. Collect email, password, first name, last name.
  2. Call registerCustomer:
    const result = await client.registerCustomer({ email, password, firstName, lastName });
  3. Branch on result.requiresVerification:
    • true → store token temporarily, route to verify-email UI (do NOT set token yet)
    • falseclient.setCustomerToken(result.token), route to account
  4. On verify-email: collect 6-digit code → client.verifyEmail(code). Offer resend via client.resendVerificationEmail().
  5. After verifyEmail resolves: client.setCustomerToken(result.token), route to account.

Build the verify-email step even if verification is currently disabled — it auto-hides.

Login flow

  1. Collect email + password.
  2. Call loginCustomer:
    const result = await client.loginCustomer(email, password);
  3. Branch on result.requiresVerification:
    • true → route to verify-email
    • falseclient.setCustomerToken(result.token), route to previous page or account
  4. Always offer OAuth buttons from client.getAvailableOAuthProviders() — render the region even when empty, it auto-populates when a provider is enabled.
  5. Render specific errors (bad credentials, rate limited, disabled) — never swallow them.

Order confirmation flow

  1. Read checkoutId from URL or session.
  2. await client.handlePaymentSuccess(checkoutId) — mandatory, clears cart so purchased items don't show on next visit.
  3. const order = await client.waitForOrder(checkoutId) — polls until the webhook writes the order.
  4. Show a spinner during step 3 (webhook may lag). On timeout: show "we're still processing, check your email" with a link to order history — the order WILL appear there.
  5. On success: render order number and line items from the returned order object.

Password reset flow

Forgot password step: collect email → client.forgotPassword(email) → always show a generic success message (prevents account enumeration, regardless of whether the account exists).

Reset password step: read token from URL query param → if missing, show error with link back → collect new password → client.resetPassword(token, newPassword) → on success route to login → on expired/invalid token show the specific error with link back.

OAuth flow

  1. Get the list of available provider names:
    const { providers } = await client.getAvailableOAuthProviders();
    // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects)
  2. For each provider, get the authorization URL:
    const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
      redirectUrl: `${window.location.origin}/auth/callback`,
    });
    window.location.href = authorizationUrl; // full-page redirect, NOT a popup
  3. On callback, the URL contains auth_code + oauth_success (or oauth_error) query params. Exchange the single-use code for the JWT:
    const params = new URLSearchParams(location.search);
    const code = params.get('auth_code');
    if (code) {
      const result = await client.exchangeOAuthCode(code);
      client.setCustomerToken(result.token); // then redirect to account
    }
    The legacy ?token= URL param is still emitted for backward compatibility but will be removed in the next major release — migrate to auth_code now.
  4. On oauth_error query param: redirect to login with an error message.

Build the OAuth button region AND the callback handler even when no providers are configured.

Inventory reservation flow

  • Display the countdown from cart.reservation?.expiresAt — refresh once per second (reservation is optional; only present when a reservation strategy is active).
  • On expiry: call client.getCart() to refresh. Items whose reservation expired are flagged server-side.
  • On the checkout page: if reservation has expired, block payment and show "your cart has expired" with a link back to cart.
  • Do NOT implement your own timer logic — the SDK is the source of truth.

Quick Reference - Helper Functions

The SDK exports these utility functions for common UI tasks:

| Function | Purpose | Example | | ---------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | formatPrice(amount, { currency?, locale? }) | Format prices for display | formatPrice("99.99", { currency: 'USD' })$99.99 | | getPriceDisplay(amount, currency?, locale?) | Alias for formatPrice | Same as above | | getDescriptionContent(product) | Get product description (HTML or text) | getDescriptionContent(product) | | isHtmlDescription(product) | Check if description is HTML | isHtmlDescription(product)true/false | | getStockStatus(inventory) | Get human-readable stock status | getStockStatus(inventory)"In Stock" | | getProductPrice(product) | Get effective price (handles sales) | getProductPrice(product)29.99 | | getProductPriceInfo(product) | Get price + sale info + discount % (falls back to priceMin when basePrice=0 on VARIABLE) | { price, isOnSale, discountPercent } | | getVariantPrice(variant, basePrice) | Get variant price with fallback | getVariantPrice(variant, '29.99')34.99 | | getCartTotals(cart, shippingPrice?) | Calculate cart subtotal/discount/total | { subtotal, discount, shipping, total } | | getCartItemName(item) | Get name from nested cart item | getCartItemName(item)"Blue T-Shirt" | | getCartItemImage(item) | Get image URL from cart item | getCartItemImage(item)"https://..." | | getVariantOptions(variant) | Get variant attributes as array | [{ name: "Color", value: "Red" }] | | isCouponApplicableToProduct(coupon, product) | Check if coupon applies | isCouponApplicableToProduct(coupon, product) | | isAllowedPaymentUrl(url, options?) | Validate a payment URL host | isAllowedPaymentUrl(intent.clientSecret)true | | safePaymentRedirect(url, options?) | Validate then window.location.href | safePaymentRedirect(intent.clientSecret) |

import {
  formatPrice,
  getDescriptionContent,
  getStockStatus,
  getProductPrice,
  getProductPriceInfo,
  getCartTotals,
  getCartItemName,
  getCartItemImage,
} from 'brainerce';

// Format price for display
const priceText = formatPrice(product.basePrice, { currency: 'USD' }); // "$99.99"

// Get product description (handles HTML vs plain text)
const description = getDescriptionContent(product);

// Get stock status text
const stockText = getStockStatus(product.inventory); // "In Stock", "Low Stock", "Out of Stock"

// Get effective price (handles sale prices automatically)
const price = getProductPrice(product); // Returns number: 29.99

// Get full price info including sale status
const priceInfo = getProductPriceInfo(product);
// { price: 19.99, originalPrice: 29.99, isOnSale: true, discountPercent: 33 }

// Calculate cart totals
const totals = getCartTotals(cart, shippingRate?.price);
// { subtotal: 59.98, discount: 10, shipping: 5.99, total: 55.97 }

// Access cart item details (handles nested structure)
const itemName = getCartItemName(cartItem); // "Blue T-Shirt - Large"
const itemImage = getCartItemImage(cartItem); // "https://..."

⚠️ DO NOT CREATE YOUR OWN UTILITY FILES! All helper functions above are exported from brainerce. Never create utils/format.ts, lib/helpers.ts, or similar files - use the SDK exports directly.


⚠️ CRITICAL: Payment Integration Required!

Your store will NOT work without payment integration. The store owner has already configured payment providers (Stripe/PayPal) - you just need to implement the payment page.

// On your checkout/payment page, ALWAYS call this first:
const { hasPayments, providers } = await client.getPaymentProviders();

if (!hasPayments) {
  // Show error - payment is not configured
  return <div>Payment not configured for this store</div>;
}

// Show payment forms for available providers
const stripeProvider = providers.find(p => p.provider === 'stripe');
const paypalProvider = providers.find(p => p.provider === 'paypal');

See the Payment Integration section for complete implementation examples.


Quick Start

For Vibe-Coded Sites (Recommended)

import { BrainerceClient } from 'brainerce';

// ✅ salesChannelId (vc_*) is all you need — no API key for storefronts
const client = new BrainerceClient({
  salesChannelId: 'vc_YOUR_SALES_CHANNEL_ID', // found in Brainerce dashboard → Sales Channels
});

// Fetch products
const { data: products } = await client.getProducts();

Product customization fields (buyer input)

Products can expose customizationFields — merchant-defined inputs the buyer fills on the product page (engraving text, photo upload, select / multi-select options, date pickers, etc.). Render the form from the array, upload any images via uploadCustomizationFile(), then pass values as metadata on add-to-cart. The server validates and snapshots everything onto the order line. Definitions flagged appliesToAllProducts: true are folded into every product's customizationFields automatically — no client-side merging required.

if (product.customizationFields?.length) {
  // Render a form control per field using field.type (TEXT, SELECT,
  // MULTI_SELECT, IMAGE, GALLERY, DATE, ...) — see INTEGRATION.md §2.8
}

// For IMAGE / GALLERY fields: upload first
const { url: photoUrl } = await client.uploadCustomizationFile(file);

await client.addToCart(cart.id, {
  productId: product.id,
  quantity: 1,
  metadata: {
    engraving_text: 'Happy Birthday!',
    frame_color: 'Gold', // SELECT — must be in enumValues
    upload_photo: photoUrl, // IMAGE — URL from uploadCustomizationFile
    addons: ['Gift wrap'], // MULTI_SELECT — always an array
  },
});

Full rendering guide + per-type validation rules: Core Integration §2.8 and Rules & Reference.

Modifier groups (restaurant / build-your-own products)

Products can expose modifierGroups — merchant-defined option blocks like "Toppings" (max 8, first 3 free) or "Sauce" (pick exactly one). Render radios for selectionType: 'SINGLE' and checkboxes for 'MULTIPLE', honor defaultModifierIds and isDefault on first render, disable modifiers with available: false, and pass selections on add-to-cart. The server is the source of truth for free-allocation and final pricing.

// 5-line add-to-cart with modifiers
await client.addToCart(cart.id, {
  productId: 'prod_pizza',
  quantity: 1,
  selections: [
    { modifierGroupId: 'mg_bread', modifierIds: ['m_thick'] },
    { modifierGroupId: 'mg_toppings', modifierIds: ['m_olive', 'm_mushroom', 'm_bacon'] },
  ],
});

Money on the wire is always strings (priceDelta: "5.00"). Validation failures arrive as a structured 400 envelope on BrainerceError.details with code: 'MODIFIER_VALIDATION_FAILED' and errors[] — see INTEGRATION-RULES.md "Modifier validation errors" for the full code list.

Full rendering guide: Core Integration §2.9. Restaurant features (allergens, scheduled availability, nested combos to depth 3, downsell modifiers): Optional Features "Restaurant / build-your-own products".

Content (FAQ / Footer / Header / Announcements / Pages)

Merchants edit site chrome and static content in the Brainerce dashboard under Sell → Content; storefronts pick up changes within ~5 minutes. Six types: FAQ, FOOTER, HEADER, ANNOUNCEMENT, RICH_TEXT, PAGE. Every type has 'main' as its universal default key.

// Fetch chrome at the root layout (server component if Next.js)
const [header, footer, announcements] = await Promise.all([
  client.content.header.get('main', locale),
  client.content.footer.get('main', locale),
  client.content.announcement.list(locale),
]);

// FAQ page
const faq = await client.content.faq.get('main', locale);

// Static pages — catch-all route
const page = await client.content.page.getBySlug(params.slug, locale);
if (!page) notFound();

All get / getBySlug return null on 404 — render a hard-coded fallback so the page never crashes when the merchant hasn't seeded yet.

Security: FAQ.items[i].answer, RICH_TEXT.html, and PAGE.html are merchant-authored HTML. The server does NOT pre-sanitize (merchants may embed iframes). ALWAYS sanitize before injecting:

import DOMPurify from 'isomorphic-dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(rawHtml) }} />

The create-brainerce-store scaffold ships ready-made components (<AnnouncementBar>, <SiteHeader>, <SiteFooter>, <FaqSection>, <RichTextBlock>) + a /pages/[slug] catch-all route — use them rather than rolling your own renderers.

Full guide: Core Integration "Content". Advanced patterns (channel scoping, custom fields, translations, admin writes): Optional Features "Content". Validation + sanitize rules: Rules & Reference "Content".


Common Mistakes to Avoid

AI Agents / Vibe-Coders: Read this section carefully! These are common misunderstandings.

1. Guest Checkout - Use startGuestCheckout() for Guests

For guest users, use startGuestCheckout() which creates a checkout from the session cart:

// ✅ CORRECT - Use startGuestCheckout() for guest users
const result = await client.startGuestCheckout();
if (result.tracked) {
  const checkout = await client.getCheckout(result.checkoutId);
  // Continue with payment flow...
}

// ✅ ALTERNATIVE - Use submitGuestOrder() for simple checkout without payment UI
const order = await client.submitGuestOrder();

Rule of thumb:

  • Guest user + Session cart → startGuestCheckout() or submitGuestOrder()
  • Logged-in user + Server cart → createCheckout({ cartId })

2. ⛔ NEVER Create Local Interfaces - Use SDK Types!

This causes type errors and runtime bugs!

// ❌ WRONG - Don't create your own interfaces!
interface CartItem {
  id: string;
  name: string; // WRONG - it's item.product.name!
  price: number; // WRONG - prices are strings!
}

// ❌ WRONG - Don't use 'as unknown as' casting!
const item = result as unknown as MyLocalType;

// ✅ CORRECT - Import ALL types from SDK
import type {
  Product,
  ProductVariant,
  Cart,
  CartItem,
  Checkout,
  CheckoutLineItem,
  Order,
  OrderItem,
  CustomerProfile,
  CustomerAddress,
  ShippingRate,
  PaymentProvider,
  PaymentIntent,
  PaymentStatus,
  SearchSuggestions,
  ProductSuggestion,
  CategorySuggestion,
  OAuthAuthorizeResponse,
  CustomerOAuthProvider,
} from 'brainerce';

⚠️ SDK Type Facts - Trust These!

| What | Correct | Wrong | | ------------------------ | ----------------------------- | --------------------- | | Prices | string (use parseFloat()) | number | | Cart item name | item.product.name | item.name | | Order item name | item.name | item.product.name | | Cart item image | item.product.images[0] | item.image | | Order item image | item.image | item.product.images | | Address state/province | region | state or province | | OAuth redirect URL | authorizationUrl | url | | OAuth providers response | { providers: [...] } | [...] directly |

If you think a type is "wrong", YOU are wrong. Read the SDK types!

3. formatPrice Expects Options Object

// ❌ WRONG
formatPrice(amount, 'USD');

// ✅ CORRECT
formatPrice(amount, { currency: 'USD' });

4. Cart/Checkout vs Order - Different Item Structures!

IMPORTANT: Cart and Checkout items have NESTED product data. Order items are FLAT.

// CartItem and CheckoutLineItem - NESTED product
cart.items.forEach((item) => {
  console.log(item.product.name); // ✅ Correct for Cart/Checkout
  console.log(item.product.sku);
  console.log(item.product.images);
});

// OrderItem - FLAT structure
order.items.forEach((item) => {
  console.log(item.name); // ✅ Correct for Orders
  console.log(item.sku);
  console.log(item.image); // singular, not images
});

| Type | Access Name | Access Image | | ------------------ | ------------------- | --------------------- | | CartItem | item.product.name | item.product.images | | CheckoutLineItem | item.product.name | item.product.images | | OrderItem | item.name | item.image |

5. Payment Status is 'succeeded', not 'completed'

// ❌ WRONG
if (status.status === 'completed')

// ✅ CORRECT
if (status.status === 'succeeded')

6. ProductSuggestion vs Product - Different Types

getSearchSuggestions() returns ProductSuggestion[], NOT Product[]. This is intentional - suggestions are lightweight for autocomplete.

// ProductSuggestion has:
{
  (id, name, slug, image, basePrice, salePrice, type);
}

// Product has many more fields

7. All Prices Are Strings - Use parseFloat()

// ❌ WRONG - assuming number
const total = item.price * quantity;

// ✅ CORRECT - parse first
const total = parseFloat(item.price) * quantity;

// Or use SDK helper
import { formatPrice } from 'brainerce';
const display = formatPrice(item.price, { currency: 'USD' });

8. Variant Attributes Are Record<string, string> (Locale-Aware)

// Accessing variant attributes:
const color = variant.attributes?.['Color']; // string
const size = variant.attributes?.['Size']; // string

// Use getVariantOptions() for display — returns translated names when locale is active:
const options = getVariantOptions(variant);
// With Accept-Language: en → [{ name: "Color", value: "Red" }, { name: "Size", value: "M" }]
// With Accept-Language: he → [{ name: "צבע", value: "אדום" }, { name: "מידה", value: "M" }]

i18n note: When client.setLocale() is active, the backend translates both attribute keys (e.g. "צבע" → "Color") and option values (e.g. "אדום" → "Red") inside variant.attributes. The getVariantOptions() and getProductSwatches() helpers automatically return translated names.

9. Address Uses region, NOT state

// ❌ WRONG
const address = {
  state: 'NY', // This field doesn't exist!
};

// ✅ CORRECT
const address: SetShippingAddressDto = {
  firstName: 'John',
  lastName: 'Doe',
  line1: '123 Main St',
  city: 'New York',
  region: 'NY', // Use 'region' for state/province
  postalCode: '10001',
  country: 'US',
};

10. OAuth - Use authorizationUrl, NOT url

// ❌ WRONG
const response = await client.getOAuthAuthorizeUrl('GOOGLE', { redirectUrl });
window.location.href = response.url; // 'url' doesn't exist!

// ✅ CORRECT
const response = await client.getOAuthAuthorizeUrl('GOOGLE', { redirectUrl });
window.location.href = response.authorizationUrl; // Correct property name

11. OAuth Provider Type is Exported

// ❌ WRONG - creating your own type
type Provider = 'google' | 'facebook'; // lowercase won't work!

// ✅ CORRECT - import from SDK
import { CustomerOAuthProvider } from 'brainerce';
// CustomerOAuthProvider = 'GOOGLE' | 'FACEBOOK' | 'GITHUB'  (UPPERCASE)

const provider: CustomerOAuthProvider = 'GOOGLE';
await client.getOAuthAuthorizeUrl(provider, { redirectUrl });

12. getAvailableOAuthProviders Returns Object, Not Array

// ❌ WRONG - expecting array directly
const providers = await client.getAvailableOAuthProviders();
providers.forEach(p => ...);  // Error! providers is not an array

// ✅ CORRECT - access the providers property
const response = await client.getAvailableOAuthProviders();
response.providers.forEach(p => ...);  // response.providers is the array

13. SDK Uses null, Not undefined

Optional fields in SDK types use null, not undefined:

// SDK types use:
slug: string | null;
salePrice: string | null;

// So when checking:
if (product.slug !== null) {
  // ✅ Check for null
  // ...
}

14. Cart Has No total Field - Use getCartTotals() Helper

// ❌ WRONG - these fields don't exist on Cart
const total = cart.total; // ← 'total' doesn't exist!
const discount = cart.discount; // ← 'discount' doesn't exist! It's 'discountAmount'

// ✅ CORRECT - use the helper function (RECOMMENDED)
import { getCartTotals } from 'brainerce';
const totals = getCartTotals(cart, shippingPrice);
// Returns: { subtotal: 59.98, discount: 10, shipping: 5.99, total: 55.97 }

// ✅ CORRECT - or calculate manually
const subtotal = parseFloat(cart.subtotal);
const discount = parseFloat(cart.discountAmount); // ← Note: 'discountAmount', NOT 'discount'
const total = subtotal - discount;

Important Notes:

  • Cart field is discountAmount, NOT discount
  • Cart has NO total field - use getCartTotals() or calculate
  • Checkout DOES have a total field, but Cart does not
  • getCartTotals() works with all carts — guests now use server-side session carts with full pricing fields.

15. SearchSuggestions - Products Have price, Not basePrice

// In SearchSuggestions, ProductSuggestion has:
// - price: effective price (sale price if on sale, otherwise base price)
// - basePrice: original price
// - salePrice: sale price if on sale

// ✅ Use 'price' for display (it's already the correct price)
suggestions.products.map(p => (
  <div>{p.name} - {formatPrice(p.price, { currency })}</div>
));

16. Forgetting to Clear Cart After Payment

This causes "ghost items" in the cart after successful payment!

// ❌ WRONG - Cart items remain after payment!
// In your success page:
export default function SuccessPage() {
  return <div>Thank you for your order!</div>;
  // User goes back to shop → still sees purchased items in cart!
}

// ✅ CORRECT - Call completeGuestCheckout() on success page
export default function SuccessPage() {
  const checkoutId = new URLSearchParams(window.location.search).get('checkout_id');

  useEffect(() => {
    if (checkoutId) {
      // Send order to server AND clear cart
      client.completeGuestCheckout(checkoutId);
    }
  }, []);

  return <div>Thank you for your order!</div>;
}

Why is this needed?

  • completeGuestCheckout() sends the order to the server AND clears the session cart
  • Without it, the order is never created on the server (payment goes through but no order!)
  • For partial checkout (AliExpress-style), only the purchased items are removed
  • After successful checkout, also call client.onCheckoutComplete() to clear the session cart reference

Checkout: Guest vs Logged-In Customer

Both guests and logged-in customers now use the same smart* cart methods. The SDK handles server-side session carts for guests automatically.

| Customer Type | Cart Method | Checkout | | ------------- | --------------------------------- | ------------------------------------------- | | Guest | smartAddToCart() (session cart) | startGuestCheckout()createCheckout() | | Logged In | smartAddToCart() (server cart) | createCheckout()completeCheckout() |

Cart Usage (Same for Both)

// Add to cart — works for both guests and logged-in users
await client.smartAddToCart({ productId: 'prod_123', quantity: 2 });

// Get cart
const cart = await client.smartGetCart();

// Get cart with recommendations, upgrades, and bundles in a single request
const cartWithExtras = await client.smartGetCart({
  include: ['recommendations', 'upgrades', 'bundles'],
});
// cartWithExtras.recommendations, cartWithExtras.upgrades, cartWithExtras.bundles

// Update quantity
await client.smartUpdateCartItem('prod_123', 5);

// Remove item
await client.smartRemoveFromCart('prod_123');

// Get totals (works for all carts)
import { getCartTotals } from 'brainerce';
const totals = getCartTotals(cart); // { subtotal, discount, shipping, total }

On Login — Merge Guest Cart

// After setting customer token
client.setCustomerToken(token);
await client.syncCartOnLogin(); // Merges session cart into customer cart

Guest Checkout

// Guest checkout creates a checkout from the session cart
const result = await client.startGuestCheckout();

if (result.tracked) {
  const checkout = await client.getCheckout(result.checkoutId);
  await client.setShippingAddress(result.checkoutId, shippingAddress);
  // ... continue with shipping rates, payment, etc.
}

Logged-In Customer Checkout (orders linked to account)

// 1. Make sure customer token is set (after login)
client.setCustomerToken(authResponse.token);

// 2. Add items to cart (smart methods handle server cart automatically)
await client.smartAddToCart({
  productId: products[0].id,
  quantity: 1,
});

// 3. Get cart and create checkout
const cart = await client.smartGetCart();
const checkout = await client.createCheckout({ cartId: cart.id });

// 4. Set customer info (REQUIRED - email is needed for order!)
await client.setCheckoutCustomer(checkout.id, {
  email: '[email protected]',
  firstName: 'John',
  lastName: 'Doe',
});

// 5. Set shipping address
await client.setShippingAddress(checkout.id, {
  firstName: 'John',
  lastName: 'Doe',
  line1: '123 Main St',
  city: 'New York',
  postalCode: '10001',
  country: 'US',
});

// 6. Get shipping rates and select one
const rates = await client.getShippingRates(checkout.id);
await client.selectShippingMethod(checkout.id, rates[0].id);

// 7. Complete checkout - order is linked to customer!
const { orderId } = await client.completeCheckout(checkout.id);
console.log('Order created:', orderId);

// Customer can now see this order in client.getMyOrders()

WARNING: Do NOT use submitGuestOrder() for logged-in customers! Their orders won't be linked to their account and won't appear in their order history.


Cart (Unified for All Users)

The SDK uses server-side carts for all users. Guests get automatic session carts; logged-in customers get server carts linked to their account.

  • ✅ Cart persists across page refreshes (via session token in localStorage)
  • ✅ Server-side pricing, discounts, and totals
  • ✅ Automatic migration from guest → customer cart on login
  • ✅ Same API for both guests and logged-in users
// Add to cart (guest or logged-in — same code!)
await client.smartAddToCart({ productId: 'prod_123', quantity: 2 });

// Get cart
const cart = await client.smartGetCart();
console.log('Items:', cart.items.length);
console.log('Total:', getCartTotals(cart).total);

// Update quantity
await client.smartUpdateCartItem('prod_123', 5);

// Remove item
await client.smartRemoveFromCart('prod_123');

After Login — Sync Cart

client.setCustomerToken(token);
const mergedCart = await client.syncCartOnLogin();
// Guest session cart items are merged into the customer's server cart

After Checkout — Clear Cart

client.onCheckoutComplete();
// Clears session cart reference so next visit starts fresh

After Logout — Preserve Guest Cart

client.clearCustomerToken();
client.onLogout();
// Session cart is preserved — guest can continue browsing

Complete Store Setup

Step 1: Create the Brainerce Client

Create a file lib/brainerce.ts:

import { BrainerceClient } from 'brainerce';

export const client = new BrainerceClient({
  salesChannelId: 'vc_YOUR_SALES_CHANNEL_ID', // found in Brainerce dashboard → Sales Channels
});

// ----- Cart Helpers -----

export async function getCart() {
  return client.smartGetCart();
}

export function getCartItemCount(): number {
  return client.getSmartCartItemCount();
}

// ----- Customer Token Helpers -----

export function setCustomerToken(token: string | null): void {
  if (token) {
    localStorage.setItem('customerToken', token);
    client.setCustomerToken(token);
  } else {
    localStorage.removeItem('customerToken');
    client.clearCustomerToken();
  }
}

export function restoreCustomerToken(): string | null {
  const token = localStorage.getItem('customerToken');
  if (token) client.setCustomerToken(token);
  return token;
}

export function isLoggedIn(): boolean {
  return !!localStorage.getItem('customerToken');
}

Important: Cart & Checkout Data Structures

Nested Product/Variant Structure

Cart and Checkout items use a nested structure for product and variant data. This is a common pattern that prevents data duplication and ensures consistency.

Common Mistake:

// WRONG - product name is NOT at top level
const name = item.name; // undefined!
const sku = item.sku; // undefined!

Correct Access Pattern:

// CORRECT - access via nested objects
const name = item.product.name;
const sku = item.product.sku;
const variantName = item.variant?.name;
const variantSku = item.variant?.sku;

Field Mapping Reference

| What You Want | CartItem | CheckoutLineItem | | -------------- | ------------------------- | ------------------------- | | Product Name | item.product.name | item.product.name | | Product SKU | item.product.sku | item.product.sku | | Product ID | item.productId | item.productId | | Product Images | item.product.images | item.product.images | | Variant Name | item.variant?.name | item.variant?.name | | Variant SKU | item.variant?.sku | item.variant?.sku | | Variant ID | item.variantId | item.variantId | | Unit Price | item.unitPrice (string) | item.unitPrice (string) | | Quantity | item.quantity | item.quantity |

Price Fields Are Strings

All monetary values in Cart and Checkout are returned as strings (e.g., "29.99") to preserve decimal precision across different systems. Use parseFloat() or the formatPrice() helper:

// Monetary fields that are strings:
// - CartItem: unitPrice, discountAmount
// - Cart: subtotal, discountAmount
// - CheckoutLineItem: unitPrice, discountAmount
// - Checkout: subtotal, discountAmount, shippingAmount, taxAmount, total
// - ShippingRate: price

import { formatPrice } from 'brainerce';

// Option 1: Using formatPrice helper (recommended)
const cart = await client.getCart(cartId);
const total = formatPrice(cart.subtotal); // "$59.98"
const totalNum = formatPrice(cart.subtotal, { asNumber: true }); // 59.98

// Option 2: Manual parseFloat
const subtotal = parseFloat(cart.subtotal);
const discount = parseFloat(cart.discountAmount);
const total = subtotal - discount;

// Line item total
cart.items.forEach((item) => {
  const lineTotal = parseFloat(item.unitPrice) * item.quantity;
  console.log(`${item.product.name}: $${lineTotal.toFixed(2)}`);
});

Complete Cart Item Display Example

import type { CartItem } from 'brainerce';
import { formatPrice } from 'brainerce';

function CartItemRow({ item }: { item: CartItem }) {
  // Access nested product data
  const productName = item.product.name;
  const productSku = item.product.sku;
  const productImage = item.product.images?.[0]?.url;

  // Access nested variant data (if exists)
  const variantName = item.variant?.name;
  const displayName = variantName ? `${productName} - ${variantName}` : productName;

  // Format price using helper
  const unitPrice = formatPrice(item.unitPrice);
  const lineTotal = formatPrice(item.unitPrice, { asNumber: true }) * item.quantity;

  return (
    <div className="flex items-center gap-4">
      <img src={productImage} alt={displayName} className="w-16 h-16 object-cover" />
      <div className="flex-1">
        <h3 className="font-medium">{displayName}</h3>
        <p className="text-sm text-gray-500">SKU: {item.variant?.sku || productSku}</p>
      </div>
      <span className="text-gray-600">Qty: {item.quantity}</span>
      <span className="font-medium">${lineTotal.toFixed(2)}</span>
    </div>
  );
}

API Reference

Categories, Brands & Tags (Filtering)

Get Categories (Tree Structure)

Categories are returned as a nested tree. Each category can have children, which can also have children (unlimited depth). Use this to build category filter UI with dropdowns for subcategories.

const { categories } = await client.getCategories();

// categories is a tree:
// [
//   { id: "cat_1", name: "Jewelry", parentId: null, children: [
//     { id: "cat_2", name: "Necklaces", parentId: "cat_1", children: [] },
//     { id: "cat_3", name: "Rings", parentId: "cat_1", children: [
//       { id: "cat_4", name: "Gold Rings", parentId: "cat_3", children: [] }
//     ]}
//   ]},
//   { id: "cat_5", name: "Clothing", parentId: null, children: [] }
// ]

Type:

interface CategoryNode {
  id: string;
  name: string;
  parentId?: string | null;
  children: CategoryNode[]; // Recursive — can nest to any depth
}

Filter Products by Category

Pass a category ID to getProducts(). The backend automatically includes all subcategories. So filtering by a parent category returns products from that category AND all its descendants.

// Filter by parent "Jewelry" — returns products from Jewelry, Necklaces, Rings, Gold Rings
const { data } = await client.getProducts({ categories: 'cat_1' });

// Filter by specific subcategory "Rings" — returns products from Rings + Gold Rings
const { data } = await client.getProducts({ categories: 'cat_3' });

// Multiple categories
const { data } = await client.getProducts({ categories: ['cat_1', 'cat_5'] });

Building Category Filter UI (Nested Dropdowns)

Complete example for a products page with hierarchical category chips:

'use client';
import { useEffect, useState, useRef } from 'react';
import { client } from '@/lib/brainerce';

interface CategoryNode {
  id: string;
  name: string;
  parentId?: string | null;
  children: CategoryNode[];
}

// Check if a category or any descendant matches the selected ID
function isActiveInTree(node: CategoryNode, selectedId: string): boolean {
  if (node.id === selectedId) return true;
  return node.children.some((child) => isActiveInTree(child, selectedId));
}

// Category chip — simple button for leaf categories, dropdown for parents
function CategoryChip({ category, selectedId, onSelect }: {
  category: CategoryNode;
  selectedId: string;
  onSelect: (id: string) => void;
}) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);
  const hasChildren = category.children.length > 0;
  const isActive = isActiveInTree(category, selectedId);

  // Close on outside click
  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);

  // Simple chip for categories without children
  if (!hasChildren) {
    return (
      <button
        onClick={() => onSelect(category.id)}
        className={selectedId === category.id ? 'chip active' : 'chip'}
      >
        {category.name}
      </button>
    );
  }

  // Chip with dropdown for categories with children
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button onClick={() => setOpen(!open)} className={isActive ? 'chip active' : 'chip'}>
        {category.name} ▾
      </button>
      {open && (
        <div className="dropdown">
          <button onClick={() => { onSelect(category.id); setOpen(false); }}>
            All {category.name}
          </button>
          <SubcategoryList nodes={category.children} depth={0} selectedId={selectedId}
            onSelect={(id) => { onSelect(id); setOpen(false); }} />
        </div>
      )}
    </div>
  );
}

// Recursive list — renders children at any depth with indentation
function SubcategoryList({ nodes, depth, selectedId, onSelect }: {
  nodes: CategoryNode[]; depth: number; selectedId: string; onSelect: (id: string) => void;
}) {
  return (
    <>
      {nodes.map((node) => (
        <div key={node.id}>
          <button
            onClick={() => onSelect(node.id)}
            style={{ paddingInlineStart: `${(depth + 1) * 16}px` }}
            className={selectedId === node.id ? 'active' : ''}
          >
            {node.name}
          </button>
          {node.children.length > 0 && (
            <SubcategoryList nodes={node.children} depth={depth + 1}
              selectedId={selectedId} onSelect={onSelect} />
          )}
        </div>
      ))}
    </>
  );
}

// Usage in products page:
function ProductFilters() {
  const [categories, setCategories] = useState<CategoryNode[]>([]);
  const [selectedCategory, setSelectedCategory] = useState('');

  useEffect(() => {
    client.getCategories().then(({ categories }) => setCategories(categories));
  }, []);

  useEffect(() => {
    // Fetch products filtered by selected category
    client.getProducts({
      categories: selectedCategory || undefined,
    }).then(/* update products state */);
  }, [selectedCategory]);

  return (
    <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
      <button onClick={() => setSelectedCategory('')}
        className={!selectedCategory ? 'chip active' : 'chip'}>
        All
      </button>
      {categories.map((cat) => (
        <CategoryChip key={cat.id} category={cat}
          selectedId={selectedCategory} onSelect={setSelectedCategory} />
      ))}
    </div>
  );
}

Key points for AI builders:

  • getCategories() returns a tree — don't flatten it! Use children to build nested UI.
  • Selecting a parent category automatically includes all descendants (backend handles this).
  • Use position: relative on the chip wrapper and position: absolute on the dropdown for proper overlay positioning.
  • Use paddingInlineStart (not paddingLeft) for RTL support.
  • Close dropdown on outside click to prevent multiple dropdowns staying open.

Get Brands & Tags

const { brands } = await client.getBrands();
// [{ id: "brand_1", name: "Nike" }, ...]

const { tags } = await client.getTags();
// [{ id: "tag_1", name: "Sale" }, ...]

// Use in product filtering:
const { data } = await client.getProducts({
  brands: ['brand_1'],
  tags: ['tag_1'],
  categories: ['cat_1'], // Can combine all filters
});

Products

Get Products (with pagination)

import { client } from '@/lib/brainerce';
import type { Product, PaginatedResponse } from 'brainerce';

const response: PaginatedResponse<Product> = await client.getProducts({
  page: 1,
  limit: 12,
  search: 'shirt', // Optional: search by name
  categories: 'cat_id', // Optional: filter by category (includes subcategories)
  brands: ['brand_id'], // Optional: filter by brands
  tags: ['tag_id'], // Optional: filter by tags
  minPrice: 10, // Optional: minimum price
  maxPrice: 100, // Optional: maximum price
  metafields: { color: ['red', 'blue'] }, // Optional: filter by custom-field values
  sortBy: 'createdAt', // Optional: 'name' | 'createdAt' | 'updatedAt' | 'basePrice'
  sortOrder: 'desc', // Optional: 'asc' | 'desc'
});

console.log(response.data); // Product[]
console.log(response.meta.total); // Total number of products
console.log(response.meta.totalPages); // Total pages

Get Single Product

const product: Product = await client.getProduct('product_id');

console.log(product.name);
console.log(product.basePrice);
console.log(product.salePrice); // null if no sale
console.log(product.images); // ProductImage[]
console.log(product.variants); // ProductVariant[] (for VARIABLE products)
console.log(product.inventory); // { total, reserved, available }

Search Suggestions (Autocomplete)

Get search suggestions for building autocomplete/search-as-you-type UI:

import type { SearchSuggestions } from 'brainerce';

// Basic autocomplete
const suggestions: SearchSuggestions = await client.getSearchSuggestions('shirt');

console.log(suggestions.products);
// [{ id, name, image, basePrice, salePrice, type }]

console.log(suggestions.categories);
// [{ id, name, productCount }]

// With custom limit (default: 5, max: 10)
const suggestions = await client.getSearchSuggestions('dress', 3);

Search covers: name, sku, description, categories, tags, and brands.

Example: Search Input with Suggestions

function SearchInput() {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState<SearchSuggestions | null>(null);

  // Debounce search requests
  useEffect(() => {
    if (query.length < 2) {
      setSuggestions(null);
      return;
    }

    const timer = setTimeout(async () => {
      const results = await client.getSearchSuggestions(query, 5);
      setSuggestions(results);
    }, 300);

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products..."
      />
      {suggestions && (
        <div className="suggestions">
          {suggestions.products.map((product) => (
            <a key={product.id} href={`/products/${product.slug}`}>
              <img src={product.image || '/placeholder.png'} alt={product.name} />
              <span>{product.name}</span>
              <span>${product.basePrice}</span>
            </a>
          ))}
          {suggestions.categories.map((category) => (
            <a key={category.id} href={`/category/${category.id}`}>
              {category.name} ({category.productCount} products)
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

Product Type Definition

interface Product {
  id: string;
  name: string;
  description?: string | null;
  descriptionFormat?: 'text' | 'html' | 'markdown'; // Format of description content
  sku: string;
  basePrice: number;
  salePrice?: number | null;
  priceMin?: string | null; // Lowest variant price (VARIABLE products only)
  priceMax?: string | null; // Highest variant price (VARIABLE products only)
  priceVaries?: boolean; // true when range should be shown ("₪49 – ₪199")
  status: 'active' | 'draft';
  type: 'SIMPLE' | 'VARIABLE';
  images?: ProductImage[];
  inventory?: InventoryInfo | null;
  variants?: ProductVariant[];
  categories?: string[];
  tags?: string[];
  createdAt: string;
  updatedAt: string;
}

interface ProductImage {
  url: string;
  position?: number;
  isMain?: boolean;
}

interface ProductVariant {
  id: string;
  sku?: string | null;
  name?: string | null;
  price?: number | null;
  salePrice?: number | null;
  attributes?: Record<string, string>;
  inventory?: InventoryInfo | null;
}

interface InventoryInfo {
  total: number;
  reserved: number;
  available: number;
  trackingMode?: 'TRACKED' | 'UNLIMITED' | 'DISABLED';
  inStock: boolean; // Pre-calculated - use this for display!
  canPurchase: boolean; // Pre-calculated - use this for add-to-cart
}

Product Metafields (Custom Fields)

Products can have custom fields (metafields) defined by the store owner, such as "Material", "Care Instructions", or "Warranty".

Important: Each metafield has a type field. When rendering, you must check field.type and render accordingly — don't just display field.value as text for all types.

| Type | Rendering | | ----------------------------------------------------------- | ------------------------------------------------------- | | IMAGE | <img> thumbnail | | GALLERY | Row of <img> thumbnails (value is JSON array of URLs) | | URL | <a> clickable link | | COLOR | Color swatch circle + hex value | | BOOLEAN | "Yes" / "No" | | DATE | Formatted date (toLocaleDateString()) | | DATETIME | Formatted datetime (toLocaleString()) | | TEXT, TEXTAREA, NUMBER, DIMENSION, WEIGHT, JSON | Plain text |

import {
  getProductMetafield,
  getProductMetafieldValue,
  getProductMetafieldsByType,
} from 'brainerce';
import type { ProductMetafield } from 'brainerce';

const product = await client.getProductBySlug('blue-shirt');

// Render metafield value based on type
function renderMetafieldValue(field: ProductMetafield): string | JSX.Element {
  switch (field.type) {
    case 'IMAGE':
      return field.value ? <img src={field.value} alt={field.definitionName} className="h-16 w-16 rounded object-cover" /> : '-';
    case 'GALLERY': {
      let urls: string[] = [];
      try { urls = JSON.parse(field.value); } catch { urls = field.value ? [field.value] : []; }
      return <div className="flex gap-2">{urls.map((url, i) => <img key={i} src={url} className="h-16 w-16 rounded object-cover" />)}</div>;
    }
    case 'URL':
      return field.value ? <a href={field.value} target="_blank" rel="noopener noreferrer">{field.value}</a> : '-';
    case 'COLOR':
      return <span><span style={{ backgroundColor: field.value }} className="inline-block h-4 w-4 rounded-full" /> {field.value}</span>;
    case 'BOOLEAN':
      return field.value === 'true' ? 'Yes' : 'No';
    case 'DATE':
      return field.value ? new Date(field.value).toLocaleDateString() : '-';
    case 'DATETIME':
      return field.value ? new Date(field.value).toLocaleString() : '-';
    default:
      return field.value || '-';
  }
}

// Display in a spec table
product.metafields?.forEach((field) => {
  console.log(`${field.definitionName}: ${renderMetafieldValue(field)}`);
});

// Get a specific metafield by key
const material = getProductMetafieldValue(product, 'material'); // auto-parsed (string | number | boolean | null)
const careField = getProductMetafield(product, 'care_instructions'); // full ProductMetafield object

// Filter metafields by type
const textFields = getProductMetafieldsByType(product, 'TEXT');

// Fetch metafield definitions (schema) to build dynamic UI
const { definitions } = await client.getPublicMetafieldDefinitions();
definitions.forEach((def) => {
  console.log(`${def.name} (${def.key}): ${def.type}, required: ${def.required}`);
});

Note: metafields may be empty if the store hasn't defined custom fields. Always use optional chaining (product.metafields?.forEach).

Product Customization Fields (Customer Input)

Some products allow customers to provide custom input (e.g., "What text to write on the cake?", "Upload your logo"). These are returned in the customizationFields array on the product response.

import { getProductCustomizationFields } from 'brainerce';
import type { ProductCustomizationField } from 'brainerce';

const product = await client.getProductBySlug('custom-cake');

// Get customization fields for this product
const fields = getProductCustomizationFields(product);

fields.forEach((field) => {
  // field.key: unique identifier (e.g., "cake_text")
  // field.name: display label (e.g., "Text on Cake")
  // field.type: TEXT, TEXTAREA, NUMBER, BOOLEAN, DATE, COLOR, IMAGE, GALLERY, etc.
  // field.required: whether customer must fill this in
  // field.minLength, field.maxLength: validation for text fields
  // field.minValue, field.maxValue: validation for number fields
  // field.enumValues: allowed options for dropdown fields
  // field.defaultValue: default value to pre-fill
});

// Customer fills in values, then add to cart with metadata
await client.addToCart(cartId, {
  productId: product.id,
  quantity: 1,
  metadata: {
    cake_text: 'Happy Birthday!', // TEXT field
    font_color: '#FF0000', // COLOR field
  },
});

For IMAGE/GALLERY fields, upload the file first:

// Upload customer file (storefront/vibe-coded mode)
const { url } = await client.uploadCustomizationFile(fileInput.files[0]);

// Then include the URL in cart metadata
await client.addToCart(cartId, {
  productId: product.id,
  quantity: 1,
  metadata: {
    logo: url, // IMAGE field value is the uploaded URL
  },
});

Supported field types:

| Type | Stored in metadata as | Customer UI | | ---------------- | --------------------- | ------------------- | | TEXT | string | Text input | | TEXTAREA | string | Textarea | | NUMBER | number | Number input | | BOOLEAN | boolean | Checkbox | | DATE/DATETIME | string (ISO) | Date picker | | COLOR | string (hex) | Color picker | | URL | string | URL input | | IMAGE | string (URL) | File upload | | GALLERY | string[] (URLs) | Multi-file upload | | DIMENSION/WEIGHT | { value, unit } | Value + unit inputs |

Admin methods (require API key):

// Set which customer-input definitions apply to a product
await client.setProductCustomizationFields(productId, [definitionId1, definitionId2]);

// Get current assignments
const fields = await client.getProductCustomizationFields(productId);

Note: customizationFields is only present when the product has customer input fields assigned. After checkout, customization values are preserved in the order as item.customizations.

Displaying Price Range for Variable Products

For products with type: 'VARIABLE' and multiple variants with different prices, display a price range instead of a single price:

// Helper function to get price range from variants
function getPriceRange(product: Product): { min: number; max: number } | null {
  if (product.type !== 'VARIABLE' || !product.variants?.length) {
    return null;
  }

  const prices = product.variants
    .map(v => v.price ?? product.basePrice)
    .filter((p): p is number => p !== null);

  if (prices.length === 0) return null;

  const min = Math.min(...prices);
  const max = Math.max(...prices);

  // Return null if all variants have the same price
  return min !== max ? { min, max } : null;
}

// Usage in component
function ProductPrice({ product }: { product: Product }) {
  const priceRange = getPriceRange(product);

  if (priceRange) {
    // Variable product with different variant prices - show range
    return <span>${priceRange.min} - ${priceRange.max}</span>;
  }

  // Simple product or all variants same price - show single price
  return product.salePrice ? (
    <>
      <span className="text-red-600">${product.salePrice}</span>
      <span className="line-through text-gray-400 ml-2">${product.basePrice}</span>
    </>
  ) : (
    <span>${product.basePrice}</span>
  );
}

When to show price range:

  • Product type is 'VARIABLE'
  • Has 2+ variants with different prices
  • Example: T-shirt sizes S/M/L at $29, XL/XXL at $34 → Display "$29 - $34"

When to show single price:

  • Product type is 'SIMPLE'
  • Variable product where all variants have the same price

Rendering Product Descriptions

CRITICAL: Product descriptions from Shopify/WooCommerce contain HTML tags. If you render them as plain text, users will see raw <p>, <ul>, <li> tags instead of formatted content!

Use the SDK helper functions to handle this automatically:

import { isHtmlDescription, getDescriptionContent } from 'brainerce';

// Option 1: Using isHtmlDescription helper (recommended)
function ProductDescription({ product }: { product: Product }) {
  if (!product.description) return null;

  if (isHtmlDescription(product)) {
    // HTML from Shopify/WooCommerce - M