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/rebuild/status, 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;
};RebuildStatusResponse
type RebuildStatusResponse = {
runId: string | null;
status: FbtRebuildStatus | null;
finishedAt: string | null; // ISO 8601; null while running / never run
pairCount: number | null;
anchorCount: number | null;
};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/rebuild/status — Latest rebuild status
Required permission: fbt: view. Returns the most recent run's status and completion timestamp, or all-null when no rebuild has ever run. Powers the Admin → Settings → Frequently Bought Together page's "Rebuild now" button, which triggers POST /admin/fbt/rebuild and then polls this endpoint until finishedAt advances past its value at trigger time.
Response 200 — RebuildStatusResponse.
{
"data": {
"runId": "01J9...",
"status": "succeeded",
"finishedAt": "2026-07-01T03:12:44.000Z",
"pairCount": 8421,
"anchorCount": 1533
},
"message": "Success",
"statusCode": 200
}All fields are null when no run exists yet.
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 (titled Frequently Bought Together) holds the kill switch (enabled), cron override, and window/support/top-N overrides used by the scheduled rebuild. It also declares a registry action button ("Rebuild now") that renders on the group's settings page, callingPOST /admin/fbt/rebuildand pollingGET /admin/fbt/rebuild/status.catalog— anchors and related products resolve to catalog products at retrieval time. See the storefront surface in../store/fbt.md.
Email Module — Admin
Operator-configurable email: outbound provider (Resend/SMTP), store brand tokens for transactional emails, per-template content overrides, and the block-based campaign builder.
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…