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.
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:
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:
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.
'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:
validateAnalyticsSchemachecks IDs, references, capabilities, JSON safety, and rejects credential- or executable-looking schema fields.validateAnalyticsQuerychecks version1, the resource, selected fields, time grains, filters, sorting, grouping, pagination, totals, and parameters.validateAnalyticsResultchecks 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.