Analytics Module — Admin surface
Admin-facing HTTP surface for precomputed platform-wide dashboard snapshots (GMV, orders, commission & payouts, vendor leaderboard, customers, inventory KPIs for today / 7d / 30d / all-time) and a live SSE platform paid-order feed.
Admin-facing HTTP surface for precomputed platform-wide dashboard snapshots (GMV, orders, commission & payouts, vendor leaderboard, customers, inventory KPIs) and a live SSE paid-order feed for the whole platform. The dashboard is computed every 10 minutes by a background worker and served directly from a snapshot table — it is never computed on request. The SSE stream is a long-lived connection that emits a baseline count on connect and increments as new paid orders arrive anywhere on the platform.
This is the platform-wide counterpart to the per-vendor Analytics — Vendor surface. Where the vendor surface scopes every metric to one vendor, the admin surface aggregates across all vendors and adds admin-only metrics (vendor leaderboard, platform commission, payouts, customers).
Source:
api-modules/admin-analytics/src/controllers/admin-analytics.controller.ts,api-modules/admin-analytics/src/controllers/admin-live-orders.controller.ts.
Conventions
Authentication & authorization
Both endpoints require a Better-Auth admin bearer session and the analytics:view permission.
Authorization: Bearer <session-token>Guarded by BetterAuthGuard + PermissionsGuard with @RequirePermissions({ analytics: ["view"] }). The analytics resource is granted to the built-in superAdmin and admin roles; narrower dynamic roles must be granted analytics:view explicitly. A session without the permission is rejected with 403 Forbidden (FORBIDDEN).
There is no tenant scoping — every metric spans the entire platform.
Response envelope (dashboard)
{
"data": { /* dashboard payload */ },
"message": "Success",
"statusCode": 200
}SSE stream (live/orders)
The SSE endpoint is not wrapped by the standard response envelope. It is a raw text/event-stream connection; each frame is a data: line containing a JSON object.
Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN (missing analytics:view) |
| 500 | INTERNAL_SERVER_ERROR |
Money + dates
All monetary fields are integer subunits (paise / cents / eurocents — single currency per tenant). Date/time fields are ISO 8601 strings (computedAt, at, bucket).
Endpoints
GET /admin/analytics/dashboard
Returns the precomputed platform analytics snapshot for the selected time window.
The snapshot is computed every 10 minutes by a background BullMQ worker (admin-analytics-refresh job). This read never recalculates on request — it always returns the latest stored snapshot. To force a fresh recompute without waiting for the next 10-minute tick, use POST /admin/analytics/refresh (below), then re-read this endpoint. Before the worker has run for the first time, every metric is zeroed and computedAt is null.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
window | "today" | "7d" | "30d" | "all_time" | "30d" | The analytics time window to serve |
Response fields (data object)
| Field | Type | Description |
|---|---|---|
window | string | The window requested |
computedAt | string | null | ISO datetime of the last snapshot computation, or null if never computed |
grossSales | int (subunits) | Platform GMV — total order grand-total in the window (includes shipping + tax) |
ordersCount | int | Total orders placed in the window |
unitsSold | int | Total line-item units sold |
aov | int (subunits) | Average order value (grossSales / ordersCount, 0 when ordersCount = 0) |
refundsAmount | int (subunits) | Total value of refunds processed |
deliveredOrders | int | Sub-orders in delivered status |
cancelledOrders | int | Sub-orders in cancelled status |
avgFulfillSeconds | int | Average seconds from order placed to fulfilled (across vendors) |
avgDeliverSeconds | int | Average seconds from fulfilled to delivered (across vendors) |
commissionEarned | int (subunits) | Platform commission earned (ledger sale entries) |
vendorNetEarnings | int (subunits) | Vendor net earnings accrued (ledger sale + refund) |
payoutsPaid | int (subunits) | Total paid out to vendors (payouts marked paid in the window) |
vendorsTotal | int | Total registered vendors |
vendorsActive | int | Vendors with at least one sub-order in the window |
customersTotal | int | Total registered (non-admin, non-anonymous) customers |
newCustomers | int | Customers created within the window |
returningCustomers | int | Distinct in-window buyers who also have an order before the window start |
productsTotal | int | Total products platform-wide (excludes soft-deleted) |
productsActive | int | Active products platform-wide |
productsDraft | int | Draft products platform-wide |
productsArchived | int | Archived products platform-wide |
skusLowStock | int | SKUs below their low-stock threshold |
skusOutOfStock | int | SKUs with zero available inventory |
inventoryUnitsOnHand | int | Total on-hand units across all SKUs |
pendingActionCount | int | Sub-orders awaiting vendor action (pending status) |
salesTrend | TrendBucket[] | Revenue + order count over time (see below) |
statusBreakdown | StatusBreakdown | Sub-order counts by status (see below) |
topVendorsByRevenue | TopVendor[] (max 5) | Top 5 vendors ranked by revenue |
topVendorsByOrders | TopVendor[] (max 5) | Top 5 vendors ranked by order count |
topProductsByRevenue | TopProduct[] (max 5) | Top 5 SKUs ranked by revenue |
topProductsByUnits | TopProduct[] (max 5) | Top 5 SKUs ranked by units sold |
lowStockList | LowStockItem[] (max 20) | Up to 20 SKUs closest to or past their low-stock threshold |
deltas | Deltas | null | Period-over-period changes; null for all_time (no previous period) |
TrendBucket
Bucket granularity depends on the window:
today→ hourly buckets7d,30d→ daily bucketsall_time→ monthly buckets
{
bucket: string; // ISO datetime (start of the bucket)
revenue: number; // integer subunits
orders: number;
}StatusBreakdown
{
pending: number;
fulfilled: number;
delivered: number;
cancelled: number;
returned: number;
}TopVendor
{
vendorId: string;
name: string;
revenue: number; // integer subunits
orders: number;
}TopProduct
{
variantId: string;
productId: string | null;
name: string;
sku: string;
units: number;
revenue: number; // integer subunits
}LowStockItem
{
variantId: string;
sku: string;
name: string;
onHand: number;
threshold: number | null;
}Deltas
Holds period-over-period comparison for four headline KPIs. Each delta has an absolute value (in the same unit as the metric) and a pct (percentage change, or null if the previous period had a zero baseline so percentage is undefined). The deltas field itself is null for the all_time window because there is no defined previous period.
{
grossSales: { absolute: number; pct: number | null };
ordersCount: { absolute: number; pct: number | null };
unitsSold: { absolute: number; pct: number | null };
commissionEarned: { absolute: number; pct: number | null };
}Example response
// GET /admin/analytics/dashboard?window=7d
{
"data": {
"window": "7d",
"computedAt": "2026-06-08T09:40:00.000Z",
"grossSales": 84200000,
"ordersCount": 612,
"unitsSold": 1043,
"aov": 137581,
"refundsAmount": 1250000,
"deliveredOrders": 470,
"cancelledOrders": 33,
"avgFulfillSeconds": 16200,
"avgDeliverSeconds": 169200,
"commissionEarned": 12630000,
"vendorNetEarnings": 71570000,
"payoutsPaid": 58000000,
"vendorsTotal": 38,
"vendorsActive": 24,
"customersTotal": 5821,
"newCustomers": 142,
"returningCustomers": 318,
"productsTotal": 1987,
"productsActive": 1840,
"productsDraft": 96,
"productsArchived": 51,
"skusLowStock": 73,
"skusOutOfStock": 22,
"inventoryUnitsOnHand": 41280,
"pendingActionCount": 109,
"salesTrend": [
{ "bucket": "2026-06-02T00:00:00.000Z", "revenue": 11200000, "orders": 82 },
{ "bucket": "2026-06-03T00:00:00.000Z", "revenue": 12450000, "orders": 90 },
{ "bucket": "2026-06-04T00:00:00.000Z", "revenue": 13900000, "orders": 101 },
{ "bucket": "2026-06-05T00:00:00.000Z", "revenue": 12800000, "orders": 95 },
{ "bucket": "2026-06-06T00:00:00.000Z", "revenue": 13100000, "orders": 96 },
{ "bucket": "2026-06-07T00:00:00.000Z", "revenue": 11550000, "orders": 86 },
{ "bucket": "2026-06-08T00:00:00.000Z", "revenue": 9200000, "orders": 62 }
],
"statusBreakdown": {
"pending": 109,
"fulfilled": 0,
"delivered": 470,
"cancelled": 33,
"returned": 0
},
"topVendorsByRevenue": [
{ "vendorId": "ven_01", "name": "GlowLabs", "revenue": 18400000, "orders": 132 },
{ "vendorId": "ven_02", "name": "DermaCo", "revenue": 14100000, "orders": 98 },
{ "vendorId": "ven_03", "name": "PureSkin", "revenue": 11900000, "orders": 87 },
{ "vendorId": "ven_04", "name": "AuraBeauty", "revenue": 9700000, "orders": 71 },
{ "vendorId": "ven_05", "name": "NudeEssentials", "revenue": 8200000, "orders": 64 }
],
"topVendorsByOrders": [
{ "vendorId": "ven_01", "name": "GlowLabs", "revenue": 18400000, "orders": 132 },
{ "vendorId": "ven_02", "name": "DermaCo", "revenue": 14100000, "orders": 98 },
{ "vendorId": "ven_03", "name": "PureSkin", "revenue": 11900000, "orders": 87 },
{ "vendorId": "ven_04", "name": "AuraBeauty", "revenue": 9700000, "orders": 71 },
{ "vendorId": "ven_05", "name": "NudeEssentials", "revenue": 8200000, "orders": 64 }
],
"topProductsByRevenue": [
{ "variantId": "var_01", "productId": "prod_01", "name": "Vitamin C Serum 30ml", "sku": "VCS-30", "units": 220, "revenue": 6600000 }
],
"topProductsByUnits": [
{ "variantId": "var_03", "productId": "prod_01", "name": "Vitamin C Serum 15ml", "sku": "VCS-15", "units": 410, "revenue": 2870000 }
],
"lowStockList": [
{ "variantId": "var_07", "sku": "MZ-CLN-100", "name": "Micellar Cleanser 100ml", "onHand": 3, "threshold": 10 },
{ "variantId": "var_08", "sku": "RET-EYE", "name": "Retinol Eye Serum", "onHand": 0, "threshold": 5 }
],
"deltas": {
"grossSales": { "absolute": 5400000, "pct": 6.85 },
"ordersCount": { "absolute": 41, "pct": 7.18 },
"unitsSold": { "absolute": 63, "pct": 6.43 },
"commissionEarned": { "absolute": 810000, "pct": 6.85 }
}
},
"message": "Success",
"statusCode": 200
}POST /admin/analytics/refresh
Forces an immediate recompute of every snapshot window instead of waiting for the scheduled 10-minute tick.
Requires @RequirePermissions({ analytics: ["refresh"] }) — granted to the built-in superAdmin and admin roles alongside analytics:view.
The recompute runs in the background on the worker, not in this request. The handler enqueues a one-off admin-analytics-refresh job (fixed jobId admin-analytics-refresh-manual, so rapid repeat calls dedup to a single in-flight run) onto the same admin-analytics BullMQ queue the cron uses, then returns immediately. Poll GET /admin/analytics/dashboard afterwards — once the worker finishes, every window's computedAt advances.
No request body.
Response 200 — { "data": { "enqueued": true }, "message": "Success", "statusCode": 200 }.
| Status | Code | When |
|---|---|---|
| 403 | FORBIDDEN | Session lacks analytics:refresh |
GET /admin/analytics/live/orders (Server-Sent Events)
Opens a long-lived SSE connection (text/event-stream) that streams real-time paid-order activity across the whole platform.
This endpoint is not wrapped in the standard response envelope — the response body is a raw event stream.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
since | ISO 8601 datetime | no | Lower bound on paid_at for the count. Clients send the viewer's local start of day (new Date().setHours(0,0,0,0)), so "today" follows the device's timezone. Defaults to the server's start of day. The bound is fixed for the life of the connection — the count does not reset at midnight. |
Behaviour
| Event | When emitted | Payload |
|---|---|---|
| Immediately on (re)connect | baseline snapshot of today's platform-wide paid-order count | snapshot frame |
| On each newly-paid order anywhere on the platform | incremented live count | order.paid frame |
| Every 30 seconds | keep-alive | heartbeat frame |
Self-healing counter: on every connect (including EventSource auto-reconnects after a dropped connection), the controller re-baselines the count from a fresh DB query (COUNT(order WHERE status='confirmed' AND paid_at >= since)). A missed event corrects itself within one reconnect cycle — the count never permanently drifts.
Frame shapes
All frames arrive as data: <JSON>\n\n lines.
snapshot — emitted once immediately on connect:
{
"type": "snapshot",
"paidOrdersToday": 184,
"at": "2026-06-08T09:41:05.123Z"
}order.paid — emitted for each newly-paid order on the platform:
{
"type": "order.paid",
"paidOrdersToday": 185,
"orderNumber": "ORD-2026-00892",
"grandTotal": 249900,
"at": "2026-06-08T09:53:28.447Z"
}grandTotal is the order's total in integer subunits.
heartbeat — emitted every 30 seconds:
{
"type": "heartbeat",
"at": "2026-06-08T10:00:05.001Z"
}Example: curl
curl -N \
-H "Authorization: Bearer <session-token>" \
-H "Accept: text/event-stream" \
'https://api.example.com/admin/analytics/live/orders?since=2026-09-18T18:30:00.000Z'Affiliate Module — Admin
HTTP surface for platform staff managing the affiliate plugin: review applications, manage affiliates (incl. suspend/resume + per-affiliate commission overrides), configure…
Assets Module — Admin
HTTP surface for the operator's media library — upload any file once, catalogue it, and hand out the public link that the storefront's asset host serves.