Skip to content

Analytics

Analytics works out of the box with zero configuration. The kernel instantiates a DrizzleAnalyticsAdapter that compiles analytics queries into SQL and runs them directly against your PostgreSQL database.

On startup, the kernel registers four built-in models:

ModelWhat it covers
OrdersOrder count, revenue, status breakdown
OrderLineItemsPer-item revenue, quantity, product breakdown
CustomersCustomer count, new vs returning
InventoryStock levels, reservation status

If the marketplace plugin is installed, three additional models are registered automatically: VendorOrders, VendorBalance, VendorReviews.

For the complete list of measures, dimensions, segments, and filter operators, see the Analytics Reference.

Use the analytics.query service method from a hook, custom route, or script:

import { buildAnalyticsScope } from "@porulle/core";
const scope = buildAnalyticsScope(actor);
const result = await kernel.analytics.query(
{
model: "Orders",
measures: ["count", "revenue"],
dimensions: ["status"],
filters: [{ dimension: "createdAt", operator: "gte", value: "2026-01-01" }],
limit: 100,
},
scope,
);

All queries run within an AnalyticsScope that restricts data visibility based on the caller’s role. Always pass a scope — never query without one. Scopes are created exclusively through buildAnalyticsScope.

Plugins contribute analytics models via the analyticsModels manifest slot in defineCommercePlugin:

import type { AnalyticsModel } from "@porulle/core";
import { defineCommercePlugin } from "@porulle/core";
const subscriptionsModel: AnalyticsModel = {
name: "Subscriptions",
title: "Subscriptions",
sql: "SELECT * FROM subscriptions",
measures: {
count: { type: "count", title: "Subscription Count" },
mrr: { type: "sum", sql: "monthly_amount", title: "Monthly Recurring Revenue" },
},
dimensions: {
status: { type: "string", sql: "status", title: "Status" },
startedAt: { type: "time", sql: "started_at", title: "Started At" },
customerId: { type: "string", sql: "customer_id", title: "Customer ID" },
},
};
export const subscriptionsPlugin = defineCommercePlugin({
id: "subscriptions",
version: "1.0.0",
analyticsModels: () => [subscriptionsModel],
});

Models contributed this way are registered at kernel startup. They appear in GET /api/analytics/meta and can be queried through GET /api/analytics/query like any built-in model.

Analytics queries enforce role-based scoping automatically. A customer actor can only see their own data. A vendor actor sees only their vendor’s orders. An admin actor sees all data for the organization.

The buildAnalyticsScope function reads the actor’s role and organization ID and builds the appropriate filter set. Passing the wrong scope (or no scope) is a security defect — the query will return either wrong data or fail validation.

Query analytics via the REST API with the analytics:read permission:

Terminal window
# List available models, measures, and dimensions
curl "http://localhost:4000/api/analytics/meta" \
-H "x-api-key: dev-staff-key"
# Query orders by status
curl -X POST "http://localhost:4000/api/analytics/query" \
-H "content-type: application/json" \
-H "x-api-key: dev-staff-key" \
-d '{
"model": "Orders",
"measures": ["count", "revenue"],
"dimensions": ["status"],
"limit": 50
}'