Supercommerce API Docs
Admin API

Reports Module — Admin

Pluggable, registry-driven operational reports (orders, discount redemptions, customers) with date-ranged paginated views and async CSV/XLSX file exports.

A pluggable, registry-driven reporting surface: date-ranged, row-level operational reports (orders, discount redemptions, customers) with an on-screen paginated view and async CSV/XLSX file exports.

Source: api-modules/reports/src/controllers/admin-report.controller.ts + admin-report-export.controller.ts.

Optional plugin. Removing ReportsModule.forRoot({ reports: [...] }) from app.module.ts disables every report + export route, stops the export worker and its retention cron, and leaves the report_export table inert.


How it works

Each report is a ReportDefinition provider (key, title, description, dateField, columns, filters, filtersSchema, run(), and optionally exportColumns + runExport() for a file wider than the screen) registered via ReportsModule.forRoot({ reports: [...] }). The HTTP surface is generic — a report is addressed by its key; columns and filters are discovered from the metadata endpoint. Adding a report = add a definition class to the forRoot array; removing one = delete it (an external plugin exposes its report class for the host app to include).

  • View is synchronous + paginated (buildPaginatedResponse), filtered to a [from, to) window. Reports may also expose summary KPI cards (kpis + summary()) and a trend chart (trendSeries + trend()), served by GET .../summary and GET .../trend — the metadata advertises which a report supports.
  • Export is asynchronous: POST .../export enqueues a BullMQ job; a worker runs the report in batches, writes a CSV/XLSX file to object storage, and flips the report_export row to ready. The admin polls the exports list and downloads the file. Files auto-expire after 7 days (daily retention sweep); the row remains as expired.

A date range is required for exports and capped at 366 days. Export files are capped at 200,000 rows — beyond that the file is marked truncated (never silently cut).

Built-in reports

KeyRow granularityDate fieldFilters
ordersone orderorder placed datestatus, paymentStatus, vendorId (filter type vendor — UI renders a vendor-name picker; value is the vendor id)
discountsone discount redemptionredemption datecode
customersone customer (ordered in range)order placed dateemail

Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching permission.

EndpointPermission
GET /admin/reports, GET /admin/reports/:key, GET /admin/reports/:key/{meta,summary,trend}reports: view
GET /admin/report-exports, GET /admin/report-exports/:idreports: view
POST /admin/reports/:key/exportreports: export
GET /admin/report-exports/:id/downloadreports: export

Response envelope

Successful responses are wrapped by ResponseInterceptor into { data, message, statusCode, metadata? }. List/run endpoints use the canonical metadata: { total, limit, offset, hasMore }. The download endpoint streams the raw file (Content-Disposition: attachment) and bypasses the envelope.


Endpoints

GET /admin/reports

List registered reports with their metadata.

Response data is an array of:

{
  "key": "orders",
  "title": "Orders",
  "description": "Every order placed in the range with totals, status, and customer.",
  "dateField": "Order placed date",
  "columns": [{ "key": "orderNumber", "label": "Order #", "type": "string" }, /* … */],
  "filters": [{ "key": "status", "label": "Order status", "type": "enum", "options": [/* … */] }],
  "exportNote": "The file carries every order column … one row per item …"  // only when the file differs from the table
}

Column type is one of string | number | money | date | datetime | boolean (money is integer subunits).

Column type is one of string | number | money | date | datetime | boolean. When present, kpis ({key,label,type}) and trendSeries ({key,label,type:number|money}) tell the client to render summary cards and a trend chart. exportNote (string?) is present when the exported file differs from the on-screen table — render it beside the export buttons.

GET /admin/reports/:key/meta

Metadata for a single report (same shape as one element above).

GET /admin/reports/:key/summary

Summary KPI values for the range. Same query params as the run endpoint (from, to, filters). Returns:

{ "kpis": [{ "key": "orders", "label": "Orders", "type": "number", "value": 1240 }, /* … */] }

value is null when not computable; empty kpis when the report has no summary.

GET /admin/reports/:key/trend

Time-bucketed series for the range (bucketed by day, or by month for ranges over ~62 days). Returns:

{
  "granularity": "day",
  "series": [{ "key": "orders", "label": "Orders", "type": "number" }, /* … */],
  "points": [{ "bucket": "<ISO>", "orders": 42, "revenue": 1839900 }, /* … */]
}

GET /admin/reports/:key

Run a report (paginated, on-screen).

Query params:

ParamTypeNotes
fromISO datetimeinclusive; omitted → last 30 days
toISO datetimeexclusive; omitted → now
filtersJSON stringreport-specific, validated against filtersSchema
limitint (1–500)default 100
offsetintdefault 0

Response: paginated array of column-keyed row objects (metadata carries totals).

POST /admin/reports/:key/export

Queue an async export. Body:

{ "format": "csv" | "xlsx", "from": "<ISO>", "to": "<ISO>", "filters": { /* optional */ } }

from/to are required. Returns the created report_export row (status: "pending").

A report may export more than it shows. A definition can declare exportColumns + runExport() alongside columns + run(); the worker then writes that wider column set to the file and leaves the on-screen table alone. runExport() may also expand one source record into several file rows, reporting the records it consumed as consumed so the worker keeps paging on records rather than on rows.

The orders report does exactly this: its file is the admin orders-table export — every order column plus one row per item — rather than the twelve summary columns the report shows. See Orders → export for the column list and the caveat that order-level money columns repeat per item row and must not be summed. It honours the report's own scope (date range on the order's placed date, order status, payment status, vendor), not the order table's filters.

GET /admin/report-exports

List export history (newest first). Query: reportKey?, status?, limit (default 50), offset. Each row:

{
  "id": "…",
  "reportKey": "orders",
  "format": "csv",
  "status": "pending | processing | ready | failed | expired",
  "params": { "from": "<ISO>", "to": "<ISO>", "filters": {} },
  "rowCount": 1240,
  "truncated": false,
  "fileName": "orders-20260101-20260201.csv",
  "fileSize": 84213,
  "error": null,
  "createdAt": "<ISO>",
  "completedAt": "<ISO>",
  "expiresAt": "<ISO>",
  "downloadUrl": "/admin/report-exports/…/download"  // only when ready
}

GET /admin/report-exports/:id

Status of a single export — poll this (or the list) until status: "ready".

GET /admin/report-exports/:id/download

Stream a ready export file as an attachment. 404 if not ready / expired / unknown.

On this page