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.
Maintainers
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 brainerceWhat 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
fetchdirectly. - 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()orcreateOrder()— 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
requiresVerificationflag inregisterCustomerandloginCustomerresponses. 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 →handlePaymentSuccess→waitForOrder. 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.lineItemson the summary, notcart.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.tokenfromloginCustomer/registerCustomer) should be passed toclient.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_codeURL param. Callclient.exchangeOAuthCode(authCode)to swap it for the JWT and apply viasetCustomerToken. (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)— useformatPrice()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 addflex-row-reverseon top.
Type safety
- NEVER use
as anyoras 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.Carthas no.totalfield — callgetCartTotals(cart).
Business Flows
These sequences are non-negotiable. The order of SDK calls matters.
Checkout flow
- Collect customer email, billing address, shipping address (
line1,line2,city,region,postalCode,country).emailis required. - 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 - Let the customer pick a rate, then persist it:
await client.selectShippingMethod(checkoutId, rateId); - Fetch available payment providers:
Each provider has aconst providers = await client.getPaymentProviders();renderType—'sdk-widget'(Stripe, PayPal, Grow),'iframe'(Cardcom),'redirect','sandbox'. Branch onrenderType, never on provider name. - Confirm payment using the provider's flow (Stripe Elements
stripe.confirmCardPayment, PayPal button, redirect, etc.). - On the confirmation page, always call both:
await client.handlePaymentSuccess(checkoutId); // clears cart const order = await client.waitForOrder(checkoutId); // polls until order exists - Display
checkout.lineItems(notcart.items) on the order summary.
Registration flow
- Collect email, password, first name, last name.
- Call
registerCustomer:const result = await client.registerCustomer({ email, password, firstName, lastName }); - Branch on
result.requiresVerification:true→ store token temporarily, route to verify-email UI (do NOT set token yet)false→client.setCustomerToken(result.token), route to account
- On verify-email: collect 6-digit code →
client.verifyEmail(code). Offer resend viaclient.resendVerificationEmail(). - After
verifyEmailresolves:client.setCustomerToken(result.token), route to account.
Build the verify-email step even if verification is currently disabled — it auto-hides.
Login flow
- Collect email + password.
- Call
loginCustomer:const result = await client.loginCustomer(email, password); - Branch on
result.requiresVerification:true→ route to verify-emailfalse→client.setCustomerToken(result.token), route to previous page or account
- Always offer OAuth buttons from
client.getAvailableOAuthProviders()— render the region even when empty, it auto-populates when a provider is enabled. - Render specific errors (bad credentials, rate limited, disabled) — never swallow them.
Order confirmation flow
- Read
checkoutIdfrom URL or session. await client.handlePaymentSuccess(checkoutId)— mandatory, clears cart so purchased items don't show on next visit.const order = await client.waitForOrder(checkoutId)— polls until the webhook writes the order.- 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.
- On success: render order number and line items from the returned
orderobject.
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
- Get the list of available provider names:
const { providers } = await client.getAvailableOAuthProviders(); // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects) - 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 - On callback, the URL contains
auth_code+oauth_success(oroauth_error) query params. Exchange the single-use code for the JWT:
The legacyconst 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 }?token=URL param is still emitted for backward compatibility but will be removed in the next major release — migrate toauth_codenow. - On
oauth_errorquery 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 (reservationis 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 createutils/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()orsubmitGuestOrder() - 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 fields7. 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") insidevariant.attributes. ThegetVariantOptions()andgetProductSwatches()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 name11. 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 array13. 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, NOTdiscount - Cart has NO
totalfield - usegetCartTotals()or calculate - Checkout DOES have a
totalfield, 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 cartGuest 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 cartAfter Checkout — Clear Cart
client.onCheckoutComplete();
// Clears session cart reference so next visit starts freshAfter Logout — Preserve Guest Cart
client.clearCustomerToken();
client.onLogout();
// Session cart is preserved — guest can continue browsingComplete 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! Usechildrento build nested UI.- Selecting a parent category automatically includes all descendants (backend handles this).
- Use
position: relativeon the chip wrapper andposition: absoluteon the dropdown for proper overlay positioning. - Use
paddingInlineStart(notpaddingLeft) 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 pagesGet 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:
metafieldsmay 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:
customizationFieldsis only present when the product has customer input fields assigned. After checkout, customization values are preserved in the order asitem.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
typeis'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
typeis'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