Frequently Bought Together Module — Storefront
Public, anonymous storefront surface for Frequently-Bought-Together (FBT) recommendations — the "Customers also bought" carousel on the product detail page (PDP) and the cart.…
Public, anonymous storefront surface for Frequently-Bought-Together (FBT) recommendations — the "Customers also bought" carousel on the product detail page (PDP) and the cart. Both endpoints read a precomputed pair set built offline by the admin/cron rebuild pipeline (see ../admin/fbt.md).
Source:
api-modules/fbt/src/controllers/storefront-fbt.controller.ts.No auth guards: the recommendation set is identical for every visitor and the kill switch (
fbt.enabledsetting) sits inside the service. When FBT is disabled, or when an anchor has no mined signal (long-tail / brand-new product), the endpoint returns an emptyproductsarray — there is no fallback to similar-products or top-sellers, by design. The frontend hides the section when the array is empty.
Conventions
Authentication
| Endpoint group | Auth |
|---|---|
GET /store/products/:productId/fbt, POST /store/cart/fbt | none (public) |
Response envelope
{
"data": { "products": [ /* FbtProduct[] */ ] },
"message": "Success",
"statusCode": 200
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | VALIDATION_ERROR (bad limit, empty/oversized productIds) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Currency
priceStart / priceEnd on FbtProduct are integer subunits (paise / cents). The frontend formats.
Domain types
FbtProduct
A slim product summary tuned for recommendation tiles — intentionally not the full product-search shape (no variant matrix, attributes, or facets; just enough to render a card and link to the PDP).
type FbtProduct = {
id: string;
slug: string;
title: string;
thumbnail: string | null;
priceStart: number | null; // integer subunits
priceEnd: number | null; // integer subunits (range for multi-variant)
inStock: boolean;
hasActiveSpecial: boolean;
};FbtResponse
type FbtResponse = { products: FbtProduct[] }; // empty ⇒ hide the sectionEndpoints
Base path: /store.
GET /store/products/:productId/fbt — PDP recommendations (single anchor)
Returns up to limit related products for one anchor, ranked by stored confidence. Empty array when there's no FBT signal for this anchor.
Path params
| Name | Notes |
|---|---|
productId | The anchor product id (the PDP being viewed) |
Query
| Name | Type | Default | Constraints |
|---|---|---|---|
limit | int | 4 | 1..10 |
Response 200 — FbtResponse.
POST /store/cart/fbt — Cart recommendations (multi-anchor)
Multi-anchor lookup over the customer's cart contents. Returns products co-purchased with the cart items, ranked by summed confidence across anchors and deduped against the input ids.
Body
{ "productIds": ["01J9...", "01J9..."], "limit": 4 }| Field | Type | Default | Constraints |
|---|---|---|---|
productIds | string[] | — | 1..50 ids (bounded server-side to cap query fan-out) |
limit | int? | 4 | 1..10 |
Response 200 — FbtResponse (input product ids excluded from results).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty productIds, more than 50 ids, or limit out of range |
Related modules
fbt(admin) — builds and refreshes the pair set this surface reads. See../admin/fbt.md.catalog—FbtProductfields (title, slug, thumbnail, price, special) are hydrated from catalog products/variants at retrieval time.inventory— drives theinStockflag.settings— thefbt.enabledkill switch makes both endpoints return an empty list when off.
Dynamic Link Module — Storefront
HTTP surface for reading dynamic link groups by slug. Dynamic link groups are CMS-style ordered collections of {image, text, url} cards — used for the storefront home grid, promo…
Global Scripts Module — Storefront
Public read surface that returns the enabled global scripts for one document slot. The storefront layout calls this once per slot (HEAD, BODY_START, BODY_END) at render time and…