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()) 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": [/* … */] }]
}

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.

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").

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