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.51.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() | ✅ | | Loyalty & rewards (points balance + tiers + redeem) | client.getLoyaltyStatus(), client.getAvailableRewards(), client.getRecommendedReward(), client.redeemLoyaltyReward(id), client.reportSocialShare() | conditional | | Loyalty paid membership (premium subscription) | client.getMembershipPlans(), client.getMySavedPaymentMethods(), client.subscribeToMembership(params), client.cancelMembership() | conditional | | Embeddable loyalty widget (points + rewards on ANY site) | client.getLoyaltyWidgetSession() | conditional | | 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.
  • Call setLocale() before createCheckout() — the order captures the active locale (order.locale), which drives the confirmation-email language AND the product names shown in order history. Without it, orders and their emails fall back to the store default language. Search also needs the locale active to match translated names.
  • Built-in order emails are localized for English and Hebrew; for other languages the merchant must add a custom email template per language (otherwise the email falls back to English).

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. Include an optional "Order notes" textarea by default — its value lands on the order for the merchant.
  2. Submit address to get shipping rates:
    const { checkout, rates } = await client.setShippingAddress(checkoutId, {
      email,
      firstName,
      lastName,
      line1,
      city,
      region,
      postalCode,
      country,
      notes: orderNotes, // optional shopper note (max 2000 chars)
    });
    // 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 result = await client.waitForOrder(checkoutId) — polls until the webhook writes the order. result.status.orderNumber / result.status.orderId are available on success.
  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 the order number, or — if your design wants more than that — fetch full details:
const result = await client.waitForOrder(checkoutId);
if (result.success) {
  const order = await client.getOrderByCheckout(checkoutId);
  // order.items, order.shippingAddress, order.notes (the shopper's own
  // order note, read-only), order.subtotal, order.shippingAmount,
  // order.taxAmount / order.taxBreakdown, order.totalAmount
}

getOrderByCheckout works for guests too — possession of the checkout id is the credential, no customer token needed.

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) | | buildProductJsonLd(product, opts) | schema.org Product JSON-LD (PDPs only) | See SEO section | | buildArticleJsonLd(post, opts) | schema.org Article JSON-LD for blog posts | See SEO section | | buildOrganizationJsonLd(store, opts) | schema.org Organization for the homepage | See SEO section | | buildBreadcrumbJsonLd(items) | schema.org BreadcrumbList | See SEO section | | jsonLdScriptProps(data) | XSS-safe <script type="application/ld+json"> props | <script {...jsonLdScriptProps(data)} /> | | getBlogSitemapEntries(client, opts) | Paginate published posts into sitemap entries | See SEO section |

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 (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, PAGE.html, and Product.description are merchant-authored HTML. The server does NOT pre-sanitize FAQ/RICH_TEXT/PAGE (merchants may embed iframes); Product.description is server-sanitized on write but you still sanitize on render. Product.description may contain <video> and host-locked YouTube/Vimeo <iframe> embeds — allow those tags (iframe restricted to www.youtube.com / www.youtube-nocookie.com / player.vimeo.com) and add those hosts to your CSP frame-src. 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".

Blog

Merchants publish blog posts in Content → Blog. Storefronts choose their own URL scheme — render posts at /blog/[slug], /articles/[slug], or whatever fits the brand.

// List published posts (any SDK mode)
const { data: posts, meta } = await client.blog.getPosts({ page: 1, limit: 10 });

// Filter by category or tag
const { data: news } = await client.blog.getPosts({ category: 'news' });
const { data: tips } = await client.blog.getPosts({ tag: 'tutorial' });

// Fetch one by slug — returns null on 404
const post = await client.blog.getPost(params.slug);
if (!post) notFound();

Security: post.content is merchant-authored HTML. Always sanitize before rendering:

import DOMPurify from 'isomorphic-dompurify';
const safeHtml = DOMPurify.sanitize(post.content);
return <div dangerouslySetInnerHTML={{ __html: safeHtml }} className="prose" />;

Scheduling: A post is visible once status === 'PUBLISHED' and publishedAt <= now(). Set a future publishedAt when publishing to schedule.

SEO Autopilot writes here too: the platform's SEO Autopilot publishes AI-written articles into this same blog automatically — render whatever getPosts() returns, and see the SEO section below for the required discoverability pieces.

SEO — JSON-LD builders, sitemap helper, IndexNow key, llms.txt

The SDK ships schema.org builders that encode Google's structured-data rules (aggregateRating gated on reviewCount > 0, AggregateOffer for VARIABLE products, XSS-safe serialization). buildProductJsonLd's Offer also always includes itemCondition (hardcoded NewCondition — first-party new-goods catalog), priceValidUntil when the product has an active sale-price window (salePriceEndsAt), and shippingDetails when you pass shipping (real flat-rate/free zones from storeInfo.shipping — omitted entirely, never fabricated, if you don't pass it). Prefer these builders over hand-rolled JSON-LD:

import {
  buildProductJsonLd,        // single-product pages ONLY (never listing pages)
  buildArticleJsonLd,        // blog post pages
  buildOrganizationJsonLd,   // homepage
  buildCollectionPageJsonLd, // category landing pages
  buildBreadcrumbJsonLd,
  jsonLdScriptProps,         // XSS-safe <script> props
} from 'brainerce';

// Blog article page
<script {...jsonLdScriptProps(buildArticleJsonLd(post, {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
  path: `/blog/${post.slug}`,
  organizationName: storeInfo.name,
}))} />

// Product page
<script {...jsonLdScriptProps(buildProductJsonLd(product, {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
  path: `/products/${product.slug}`,
  currency: storeInfo.currency,
  shipping: storeInfo.shipping, // optional — adds shippingDetails when present
}))} />

Blog entries in sitemap.xml (required — autopilot articles missing from the sitemap never get crawled):

// app/sitemap.ts
import { getBlogSitemapEntries } from 'brainerce';

const blogPages = await getBlogSitemapEntries(client, {
  siteUrl: baseUrl,
  locales: supportedLocales, // optional (multi-locale stores)
  defaultLocale,
}).catch(() => []);
return [...staticPages, ...productPages, ...blogPages];

IndexNow key file (required): the platform pings IndexNow when posts publish; search engines verify by fetching GET /indexnow-key.txt. Serve getStoreInfo().seo.indexNowKey as text/plain, 404 while null. The key is not a secret (public by protocol design).

llms.txt (recommended): a plain-text site summary (store name, key pages, recent article links) for AI answer engines at GET /llms.txt.


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 }

Detecting a Silent Session Cart Reset

If a guest's stored session cart can no longer be resolved as-is — the fetch fails, or the cart was found but is no longer ACTIVE (e.g. a prior checkout on it already completed) — smartAddToCart() / smartGetCart() / smartUpdateCartItem() transparently start a fresh empty cart so the call still succeeds. Pass onCartReset to the constructor to find out when this happens, so you can tell the shopper their cart expired instead of them just seeing it empty out:

const client = new BrainerceClient({
  salesChannelId: 'vc_abc123...',
  onCartReset: ({ previousCartId, reason }) => {
    // reason: 'not_found' (fetch failed) | 'not_active' (cart no longer ACTIVE)
    toast.warn('Your cart session expired — please re-add your items.');
  },
});

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.

Address Autocomplete (optional, for the line1 field)

Turn the shipping address's line1 input into a typeahead instead of free text. Suggestions come from Google Places; each resolved address is flagged inZone against the store's configured shipping zones — a soft signal for a warning banner, never a hard block.

const sessionToken = crypto.randomUUID(); // one per address-entry attempt

// Debounce (~300ms) as the shopper types
const suggestions = await client.getAddressSuggestions('Rothschild 1', sessionToken);
// [{ placeId: 'ChIJ...', description: 'Rothschild Blvd 1, Tel Aviv-Yafo' }, ...]

// On picking a suggestion — reuse the SAME sessionToken to end the session
const { address, inZone } = await client.getAddressDetails(suggestions[0].placeId, sessionToken);
// address: { line1, city, region, postalCode, country, lat, lng, formattedAddress }

// address.region is Google's own administrative-area code — usually (not
// guaranteed) the same ISO 3166-2 subdivision code this store's own region
// lists use. Validate against destinations.regions before trusting it;
// never assign a code the region <select> wouldn't recognize.
const destinations = await client.getShippingDestinations();
const validRegions = destinations.regions[address.country] ?? [];
const region = validRegions.some((r) => r.code === address.region) ? address.region : '';

await client.setShippingAddress(checkout.id, {
  firstName: 'John',
  lastName: 'Doe',
  ...address,
  region,
});

if (!inZone) {
  // Non-blocking: "This address is outside our regular delivery zones.
  // You can still continue — we'll confirm delivery by phone."
}

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;
  slug?: string | null; // link a category page as `/category/${slug}`
  parentId?: string | null;
  image?: string | null;
  children: CategoryNode[]; // Recursive — can nest to any depth
}

Get Category by Slug (Category Page)

Category (collection) pages are the highest-leverage organic-SEO surface — they rank for broad "research intent" queries that individual product pages never do. getCategoryBySlug returns the landing-page payload; fetch the products themselves with getProducts({ categories: [category.id] }).

// app/category/[slug]/page.tsx
const category = await client.getCategoryBySlug(params.slug).catch(() => null);
if (!category) notFound();

const { data: products } = await client.getProducts({ categories: [category.id] });

// SEO: CollectionPage + BreadcrumbList JSON-LD (never Product markup on a listing)
const jsonLd = buildCollectionPageJsonLd(category, { siteUrl, path: `/category/${category.slug}` });

Type:

interface CategoryDetail {
  id: string;
  name: string;
  slug: string | null;
  description: string | null; // long-form HTML — render below the product grid
  metaDescription: string | null; // <meta name="description">
  image: string | null;
  breadcrumb: Array<{ name: string; slug: string | null }>; // root → parent
  productCount: number;
}

Vibe-coded mode only (like getCategories). Category slugs come from the dashboard SEO hub, which also writes the description + meta.

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 — pass the IDs from getTags()/getBrands(),
// NOT the display names:
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 brand IDs (from getBrands())
  tags: ['tag_id'], // Optional: filter by tag IDs (from getTags()) — NOT tag names
  minPrice: 10, // Optional: minimum price
  maxPrice: 100, // Optional: maximum price
  metafields: { color: ['red', 'blue'] }, // Optional: filter by custom-field values
  sortBy: 'createdAt', // Optional: 'name' | 'price' | 'createdAt'
  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, basePr