Frequently Bought Together Module — Admin
HTTP surface for operating the Frequently-Bought-Together (FBT) recommendation pipeline — an offline batch that mines product co-purchase pairs from confirmed orders and stores a…
HTTP surface for operating the Frequently-Bought-Together (FBT) recommendation pipeline — an offline batch that mines product co-purchase pairs from confirmed orders and stores a ranked related-product set per anchor. This admin surface triggers on-demand rebuilds and reads the rebuild audit log; the recommendations themselves are served by the storefront surface (see ../store/fbt.md).
Source:
api-modules/fbt/src/controllers/admin-fbt.controller.ts.The pipeline is normally driven by a monthly BullMQ cron (
0 3 1 * *by default, overridable via thefbtsettings group). This controller exposes the manual trigger and the audit log. A kill switch (fbt.enabledsetting) makes both the rebuild job and the retrieval endpoints no-op when off.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching fbt:* permission.
| Endpoint group | Permission |
|---|---|
POST /admin/fbt/rebuild | fbt: rebuild |
GET /admin/fbt/runs, GET /admin/fbt/runs/:id | fbt: view |
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
How the pipeline works
A rebuild run mines pairs from orders confirmed within a rolling window and writes the top-N related products per anchor. Each run is recorded as an fbt_rebuild_run row (status, counts, timings, error) that this surface exposes as an audit log.
Defaults (overridable per-run by the cron via the fbt settings group; the manual trigger always uses the module defaults):
| Knob | Default | Meaning |
|---|---|---|
windowDays | 180 | Orders confirmed within this many days are eligible |
minSupport | 10 | Minimum co-occurrence count for a pair to qualify (below = noise) |
topN | 20 | Max related-product rows stored per anchor (retrieval can request fewer) |
Concurrency is guarded by a Postgres advisory lock inside the worker — two rebuilds never race on the same data. Stale queued/running runs older than 2h are marked failed on worker boot.
Domain types
RebuildRunResponse
type FbtRebuildTrigger = "cron" | "admin";
type FbtRebuildStatus = "queued" | "running" | "succeeded" | "failed";
type RebuildRunResponse = {
id: string;
trigger: FbtRebuildTrigger;
triggeredByUserId: string | null; // null for cron runs
status: FbtRebuildStatus;
windowDays: number;
minSupport: number;
topN: number;
pairCount: number | null; // null until the run finishes
anchorCount: number | null;
eligibleLineCount: number | null;
startedAt: string | null; // ISO; null while queued
finishedAt: string | null; // ISO; null until done
/** Wall-clock ms (finishedAt - startedAt). Null while in flight. */
durationMs: number | null;
error: string | null; // populated on failed runs
createdAt: string; // ISO
};TriggerRebuildResponse
type TriggerRebuildResponse = {
runId: string;
status: FbtRebuildStatus; // typically "queued"
/** True when an in-flight run already existed (no new run enqueued). */
alreadyRunning: boolean;
};Endpoints
Base path: /admin/fbt.
POST /admin/fbt/rebuild — Enqueue an on-demand rebuild
Required permission: fbt: rebuild. Idempotent under concurrency: if a rebuild is already queued or running, the existing run is returned with alreadyRunning: true rather than spawning a duplicate. (The advisory lock inside the worker is the real concurrency guard; this short-circuit is a UX nicety.) The run is enqueued with trigger: "admin" and the module-default window/support/topN.
Response 202 Accepted — TriggerRebuildResponse.
{
"data": { "runId": "01J9...", "status": "queued", "alreadyRunning": false },
"message": "Success",
"statusCode": 202
}GET /admin/fbt/runs — Paginated rebuild audit log
Required permission: fbt: view. Newest first.
Query
| Name | Type | Default | Constraints |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | 20 | 1..100 |
status | FbtRebuildStatus? | — | Filter by lifecycle status (e.g. "show me the latest failure") |
trigger | FbtRebuildTrigger? | — | Filter by source (cron / admin) |
Response 200 — paginated envelope of RebuildRunResponse[] with metadata pagination.
GET /admin/fbt/runs/:id — Single rebuild run detail
Required permission: fbt: view.
Path params
| Name | Notes |
|---|---|
id | Rebuild run id |
Response 200 — RebuildRunResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown run id |
Related modules
admin-rbac— gates both surfaces viafbt:*permissions. Seeadmin-rbac.md.order— confirmed orders are the input signal; the rebuild minesorder_lineco-occurrence within the window.settings— thefbtsettings group holds the kill switch (enabled), cron override, and window/support/top-N overrides used by the scheduled rebuild.catalog— anchors and related products resolve to catalog products at retrieval time. See the storefront surface in../store/fbt.md.
Dynamic Link Module — Admin
HTTP surface for managing dynamic link groups (CMS-style tile collections — e.g. homepage banner slots) and the dynamic links inside each group. Groups are addressable by slug and…
Free Gift Module — Admin
HTTP surface for managing free-gift rules — one row per promotion. The cart engine evaluates active rules and produces cart.pendingGifts[], which the customer resolves into an…