Skip to main content

Ship an analytics view

@zuilib/analytics is a complete open-source path from a semantic schema to an interactive React workspace. Your application owns authentication, authorization, query execution, and its data source. ZUI Cloud is not required for interactive analytics.

AnalyticsWorkspace -> REST executor -> your Web API -> your query executor -> your data
JSON query trusted identity JSON result

The integration glue below is well under 100 lines. The schema and the query implementation are domain code and remain in your application.

1. Install the package

pnpm add @zuilib/analytics react

2. Define the semantic contract

A resource schema contains stable dimension and measure IDs, supported operations, presentation metadata, and an opaque revision. It contains no rows, SQL, credentials, or executable expressions.

analytics/orders-schema.ts
import type {AnalyticsResourceSchema} from '@zuilib/analytics'

export const ordersSchema = {
id: 'orders',
label: 'Orders',
description: 'Recognized order revenue.',
revision: 'orders-v1',
dimensions: [
{
id: 'createdAt',
label: 'Created',
valueType: 'datetime',
timeGrains: ['day', 'month'],
filterOps: ['gte', 'lt'],
},
{
id: 'region',
label: 'Region',
valueType: 'enum',
values: ['AMER', 'EMEA', 'APAC'],
filterOps: ['eq', 'in'],
},
],
measures: [
{
id: 'revenue',
label: 'Revenue',
valueType: 'number',
format: 'currency:USD',
allowedDimensions: ['createdAt', 'region'],
},
],
capabilities: {
filtering: true,
sorting: true,
grouping: true,
search: false,
totals: true,
pagination: 'offset',
maximumPageSize: 100,
export: ['csv'],
},
} satisfies AnalyticsResourceSchema

Keep this module shared between the server and UI when the catalog may be public. If even schema discovery is sensitive, return an already-authorized schema from your host instead.

Implement queryOrders as an AnalyticsExecutor in server-only code. It maps the stable IDs above to your query engine and returns JSON-safe rows plus column metadata:

server/query-orders.ts
import type {AnalyticsExecutor} from '@zuilib/analytics'
import {runOrdersQuery} from './orders-data-source'

export const queryOrders: AnalyticsExecutor = async ({
query,
schema,
subject,
environment,
signal,
}) => {
// Enforce subject-based field and row scope here, translate only known IDs,
// execute with signal, and return AnalyticsResult. Never accept raw SQL.
return runOrdersQuery({query, schema, subject, environment, signal})
}

A valid result for the initial query below has this shape:

{
rows: [{month: '2026-09-01T00:00:00.000Z', revenue: 184200}],
columns: [
{
field: 'month',
source: {kind: 'dimension', id: 'createdAt'},
valueType: 'datetime',
timeGrain: 'month',
},
{
field: 'revenue',
source: {kind: 'measure', id: 'revenue'},
valueType: 'number',
format: 'currency:USD',
},
],
rowCount: 1,
queryId: 'warehouse-query-7f3a',
schemaRevision: 'orders-v1',
}

field is the key in every row; source maps it back to the governed semantic ID. queryId, schemaRevision, rowCount, nextCursor, totals, and warnings are optional result metadata. They support tracing, cache/version correlation, pagination, totals, and partial-result notices without changing the row contract.

3. Add a trusted Web API handler

The handler uses the standard Request/Response API. This Next.js App Router example authenticates inside server code and constructs the subject there:

app/api/analytics/query/route.ts
import {createNextAnalyticsHandler} from '@zuilib/analytics/server'

import {ordersSchema} from '../../../../analytics/orders-schema'
import {authenticateRequest} from '../../../../server/auth'
import {queryOrders} from '../../../../server/query-orders'

export const POST = createNextAnalyticsHandler({
resources: {
orders: {schema: ordersSchema, query: queryOrders},
},
getSubject: async (request) => {
const session = await authenticateRequest(request)
return session
? {
id: session.userId,
groups: session.roles,
attributes: {tenantId: session.tenantId},
}
: undefined
},
getEnvironment: () => process.env.NODE_ENV,
authorize: ({subject}) =>
subject?.groups?.includes('analytics') === true,
})

For any other Web API runtime, use createAnalyticsHandler with the same options and call the returned function from your framework route. The Next.js helper is a compatibility alias for that standard handler.

The request body is only an AnalyticsQuery; it cannot provide identity or credentials. The handler resolves the resource and trusted subject, authorizes access, validates the query, runs the executor with the request's abort signal, then validates the result. A denied resource and an unknown resource receive the same not-found response so callers cannot enumerate the catalog.

4. Connect the browser and workspace

The REST adapter sends validated JSON with POST and preserves cancellation and stable analytics errors. A same-origin endpoint uses the host application's normal browser session; do not put API keys in the schema, query, or component props.

app/orders/analytics.tsx
'use client'

import {AnalyticsWorkspace} from '@zuilib/analytics/workspace'
import {createRestAnalyticsExecutor} from '@zuilib/analytics/rest'

import {ordersSchema} from '../../analytics/orders-schema'

const executor = createRestAnalyticsExecutor({
endpoint: '/api/analytics/query',
})

export default function OrdersAnalytics() {
return (
<AnalyticsWorkspace
schema={ordersSchema}
executor={executor}
editable
initialQuery={{
dimensions: [{id: 'createdAt', timeGrain: 'month'}],
measures: ['revenue'],
includeTotals: true,
}}
/>
)
}

Do not pass the signed-in user from browser state as an authority. The server subject created by getSubject is the identity used for resource, field, and row policy. Keep those checks in the host even if the workspace only displays fields allowed by its schema.

Validation and stable errors

The schema, query, and result are separate contracts. Catalog schemas and queries carry version 1; a resource can carry its own opaque revision:

  • validateAnalyticsSchema checks IDs, references, capabilities, JSON safety, and rejects credential- or executable-looking schema fields.
  • validateAnalyticsQuery checks version 1, the resource, selected fields, time grains, filters, sorting, grouping, pagination, totals, and parameters.
  • validateAnalyticsResult checks row/column consistency and that result sources match the executed query and schema.

The workspace and REST adapter validate before execution, and the server validates again at the trust boundary. Failures use AnalyticsError and a stable JSON envelope with a machine-readable code, bounded details, and an required requestId. Successful results may carry a data-source queryId; that is distinct from the HTTP request ID. Transport telemetry reports timing, counts, IDs, and status—not result rows.

Use normalizeAnalyticsQuery and createAnalyticsQueryKey when saved queries or caches need deterministic identity. Use createAnalyticsAuthoringMetadata or createAnalyticsAIContext to give a builder or model the same governed field choices; generated queries still pass the normal validators.

Where ZUI Cloud fits

ZUI Core remains the interactive data plane shown above:

browser -> your authenticated API -> your data source

ZUI Cloud is an optional, out-of-band control plane for managed Config Store, PDF Export, and Scheduled Delivery. AnalyticsWorkspace exposes onSave, onShare, onExport, and onSchedule intent callbacks; it shows only the actions you connect. A callback can send its intent to a host API, which re-authorizes it before making any server-side Cloud call. Cloud credentials and warehouse credentials never belong in the browser, schema, or saved query, and adding Cloud does not reroute interactive queries through it.

Next steps