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 Product Listing Module — Storefront
Public storefront surface for reading dynamic product listing groups by slug and resolving dynamic product listings (saved filter specs) into live product results from the search engine.
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…