Support/Builder Guide/Interactive API presentations for professional admin pages

Builder Guide

Interactive API presentations for professional admin pages

Build API-bound KPI strips, names and statuses, progress, charts, sortable grids and timelines from one bounded server-scoped presentation contract.

Presentation blocks
6
Batch bindings
Up to 24
Browser data source
Secured APIs
SQL in React
Never

Interactive API presentations let a designer turn one secured business capability into KPI cards, names and statuses, distributions, time-series charts, sortable visible-page grids and activity timelines. The browser consumes APIs — never SQL — while the server keeps routing, joins, authorization and result shaping behind reviewed contracts.

Note

This is an additive presentation layer. Existing module APIs remain the source for create, update, delete and detail journeys. A presentation binding summarizes or composes those capabilities for a particular screen; it does not turn every CRUD endpoint into an unbounded dashboard query.

What designers can do now

  • Place responsive KPI strips, entity/name lists, status distributions, interactive bar or line charts, typed sortable grids and activity timelines on any authored page.
  • Bind several blocks to one API result. The renderer deduplicates visible binding keys, makes one batch request, and uses each block's dataKey to select its child document.
  • Mix presentation blocks with first-class modules, menus, forms, Files, Workflow, Universal Inbox and ordinary layout components.
  • Use Puck to change safe scalar properties while preserving binding identity, exact-version publication and the registry-backed renderer vocabulary.
  • Apply a SaaS theme through design tokens and CSS variables, or use the normalized contracts in a completely custom headless React design.
  • Receive explicit loading, empty and safe error states without sending raw rows to the browser and filtering them there.
  • Keep stable recordId values on list, grid and timeline items so a row can open the correct authorized detail journey.

How one request can power a complete admin page

Authored Page
  └─ collect unique visible bindingKey values
       └─ POST /api/runtime/data/batch (maximum 24 keys)
            └─ verified host + tenant session + server-side routing
                 └─ reviewed binding contracts (maximum 4 in flight)
                      └─ bounded, already-authorized presentation documents
                           ├─ metrics     → core.metric-strip
                           ├─ byStatus    → core.progress-list
                           ├─ recent      → core.entity-list
                           ├─ trend       → core.series-chart
                           ├─ table       → core.presentation-grid
                           └─ timeline    → core.timeline

Repeated use of work-orders.presentation is intentional. The page renderer requests that key once, shares the result, and lets each block read only metrics, byStatus, recent, trend, table or timeline. Hidden, unauthorized and unknown blocks do not cause data requests.

The six normalized API shapes

KindRegistry blockUse it forHard maximum
metric-setcore.metric-stripCounts, currency, percent, duration and deltas12 metrics
entity-listcore.entity-listNames, images, context, status and trailing values100 items
progress-listcore.progress-listStatus mix, ranked progress and capacity100 items
series-chartcore.series-chartInteractive bar/line periods with an accessible value table5 periods; 8 series; 120 points each
data-gridcore.presentation-gridTyped visible-page rows and local sorting24 columns; 500 rows
timelinecore.timelineChronological activity, actor and status100 items

Every child declares presentationVersion: 1 and an exact kind. Keys, statuses, tones, URLs, timestamps, arrays and finite numbers are parsed fail-closed before rendering.

Copy a complete batch response example
{
  "contractVersion": 1,
  "bindings": [
    {
      "bindingKey": "work-orders.presentation",
      "status": "ok",
      "data": {
        "metrics": {
          "presentationVersion": 1,
          "kind": "metric-set",
          "title": "Work order overview",
          "items": [
            { "key": "open", "label": "Open", "value": 24, "format": "number", "tone": "primary" }
          ]
        },
        "recent": {
          "presentationVersion": 1,
          "kind": "entity-list",
          "title": "Recent work orders",
          "items": [
            {
              "id": "<work-order-id>",
              "recordId": "<record-id>",
              "primary": "WO-1048 · Inspect cooling system",
              "secondary": "North campus",
              "tertiary": "Urgent",
              "status": { "key": "in-progress", "label": "In progress", "tone": "primary" },
              "trailing": 2,
              "trailingLabel": "assignees"
            }
          ],
          "hasMore": false
        },
        "byStatus": {
          "presentationVersion": 1,
          "kind": "progress-list",
          "title": "Work orders by status",
          "items": [
            { "key": "requested", "label": "Requested", "value": 7, "maximum": 24, "displayValue": "7", "tone": "warning" }
          ]
        },
        "trend": {
          "presentationVersion": 1,
          "kind": "series-chart",
          "title": "Updated work orders by current status",
          "variant": "line",
          "defaultPeriodKey": "d7",
          "periods": [
            {
              "key": "d7",
              "label": "7 days",
              "labels": ["09/01", "09/02", "09/03"],
              "series": [
                { "key": "active", "label": "Active", "tone": "primary", "values": [3, 5, 4] }
              ]
            }
          ]
        },
        "table": {
          "presentationVersion": 1,
          "kind": "data-grid",
          "title": "Visible work orders",
          "columns": [
            { "key": "number", "label": "Work order", "type": "text", "align": "left" },
            { "key": "status", "label": "Status", "type": "status", "align": "left" }
          ],
          "rows": [
            {
              "id": "<work-order-id>",
              "recordId": "<record-id>",
              "cells": {
                "number": "WO-1048",
                "status": { "key": "in-progress", "label": "In progress", "tone": "primary" }
              }
            }
          ],
          "page": { "pageNumber": 1, "pageSize": 25, "hasMore": false, "totalRecords": 1, "totalIsExact": true }
        },
        "timeline": {
          "presentationVersion": 1,
          "kind": "timeline",
          "title": "Recently updated work orders",
          "items": [
            {
              "id": "<event-id>",
              "recordId": "<record-id>",
              "occurredUtc": "2026-09-04T18:00:00Z",
              "title": "WO-1048 updated",
              "description": "Inspect cooling system",
              "actor": "Avery Chen",
              "tone": "primary",
              "status": { "key": "in-progress", "label": "In progress", "tone": "primary" }
            }
          ],
          "hasMore": false
        }
      }
    }
  ]
}

Copy the native styled Work Orders page

This is the complete page installed and browser-tested in the retained battle SaaS. Paste it into the advanced Page JSON editor, or recreate it visually in Puck. The system theme supplies the polish; there is no page-local CSS or script.

{
  "blocks": [
    {
      "_id": "work-order-command-center-header",
      "_type": "core.page-header",
      "props": {
        "title": "Work order command center",
        "lead": "Live workload, status, trends and recent activity from one server-scoped API presentation.",
        "breadcrumb": ["Operations", "Work orders"]
      }
    },
    {
      "_id": "work-order-command-center-metrics",
      "_type": "core.metric-strip",
      "bindingKey": "work-orders.presentation",
      "dataKey": "metrics",
      "props": {}
    },
    {
      "_id": "work-order-command-center-summary",
      "_type": "core.container",
      "props": {
        "layout": "grid"
      },
      "children": [
        {
          "_id": "work-order-command-center-status",
          "_type": "core.progress-list",
          "bindingKey": "work-orders.presentation",
          "dataKey": "byStatus",
          "props": {}
        },
        {
          "_id": "work-order-command-center-recent",
          "_type": "core.entity-list",
          "bindingKey": "work-orders.presentation",
          "dataKey": "recent",
          "props": {}
        }
      ]
    },
    {
      "_id": "work-order-command-center-trend",
      "_type": "core.series-chart",
      "bindingKey": "work-orders.presentation",
      "dataKey": "trend",
      "props": {}
    },
    {
      "_id": "work-order-command-center-table",
      "_type": "core.presentation-grid",
      "bindingKey": "work-orders.presentation",
      "dataKey": "table",
      "props": {}
    },
    {
      "_id": "work-order-command-center-timeline",
      "_type": "core.timeline",
      "bindingKey": "work-orders.presentation",
      "dataKey": "timeline",
      "props": {}
    }
  ]
}

The chart period buttons and visible-page grid sorting are interactive. The chart keeps an accessible table, the grid exposes aria-sort, entity metadata stacks on small screens, and wide grids scroll inside their card rather than widening the document.

Call the presentation API directly

Inside the native runtime, use loadRuntimeDataBindings or the official tenant-session transport. It owns bearer authentication, refresh rotation, CSRF, same-origin credentials and safe error handling. Do not copy an access token into page JSON or call raw fetch without that lifecycle.

POST /api/runtime/data/batch
Content-Type: application/json
Authorization: Bearer <tenant access token>
X-CSRF-Token: <same-origin CSRF token>

{
  "bindingKeys": ["work-orders.presentation"]
}
Copy a typed loader with strict response parsing
import {
  parseDataGridPresentation,
  parseEntityListPresentation,
  parseMetricSetPresentation,
  parseProgressListPresentation,
  parseRuntimeDataBindingBatch,
  parseSeriesChartPresentation,
  parseTimelinePresentation,
} from "@buildwithhq/page-runtime";

// Use the official tenant-session transport here. It must add the bearer token,
// same-origin CSRF header, correlation ID, refresh-once behavior and safe errors.
export type AuthorizedFetch = (
  input: RequestInfo | URL,
  init?: RequestInit,
) => Promise<Response>;

export async function loadWorkOrderPresentation(
  authorizedFetch: AuthorizedFetch,
  signal?: AbortSignal,
) {
  const bindingKeys = ["work-orders.presentation"] as const;
  const response = await authorizedFetch("/api/runtime/data/batch", {
    method: "POST",
    body: JSON.stringify({ bindingKeys }),
    signal,
  });
  if (!response.ok) throw new Error("The command center could not be loaded.");

  const batch = parseRuntimeDataBindingBatch(await response.json(), bindingKeys);
  const item = batch.bindings[0];
  if (item.status !== "ok" || !item.data || Array.isArray(item.data) || typeof item.data !== "object") {
    throw new Error("The command center could not be loaded.");
  }

  return {
    metrics: parseMetricSetPresentation(item.data.metrics),
    recent: parseEntityListPresentation(item.data.recent),
    byStatus: parseProgressListPresentation(item.data.byStatus),
    trend: parseSeriesChartPresentation(item.data.trend),
    table: parseDataGridPresentation(item.data.table),
    timeline: parseTimelinePresentation(item.data.timeline),
  };
}

A batch returns one item per requested key in request order. Each item is independently ok, notFound, forbidden, invalid or error, so one unavailable panel does not require discarding successful siblings.

Copy an unstyled/headless React composition

This version deliberately contains semantic HTML and no design opinion. Keep the typed loader, replace the markup with your design system, and style it as aggressively as you like. The authorization and cardinality boundary remains the API response, not the component.

import { useEffect, useState } from "react";
import type { AuthorizedFetch } from "./workOrderPresentation";
import { loadWorkOrderPresentation } from "./workOrderPresentation";

type Presentation = Awaited<ReturnType<typeof loadWorkOrderPresentation>>;

export function HeadlessWorkOrderCommandCenter({
  authorizedFetch,
}: {
  authorizedFetch: AuthorizedFetch;
}) {
  const [data, setData] = useState<Presentation | null>(null);
  const [error, setError] = useState(false);

  useEffect(() => {
    const request = new AbortController();
    setError(false);
    loadWorkOrderPresentation(authorizedFetch, request.signal)
      .then(setData)
      .catch(() => { if (!request.signal.aborted) setError(true); });
    return () => request.abort();
  }, [authorizedFetch]);

  if (error) return <p role="alert">The command center could not be loaded.</p>;
  if (!data) return <p role="status">Loading command center…</p>;

  return (
    <main>
      <h1>Work order command center</h1>

      <dl aria-label={data.metrics.title ?? "Metrics"}>
        {data.metrics.items.map((metric) => (
          <div key={metric.key}><dt>{metric.label}</dt><dd>{metric.value}</dd></div>
        ))}
      </dl>

      <section aria-labelledby="status-heading">
        <h2 id="status-heading">{data.byStatus.title}</h2>
        {data.byStatus.items.map((item) => (
          <p key={item.key}>
            {item.label}: <progress value={item.value} max={item.maximum} /> {item.displayValue}
          </p>
        ))}
      </section>

      <section aria-labelledby="recent-heading">
        <h2 id="recent-heading">{data.recent.title}</h2>
        <ul>{data.recent.items.map((item) => (
          <li key={item.id} data-record-id={item.recordId}>
            <strong>{item.primary}</strong>{item.secondary && <> — {item.secondary}</>}
            {item.status && <span> · {item.status.label}</span>}
          </li>
        ))}</ul>
      </section>

      <table>
        <caption>{data.table.title}</caption>
        <thead><tr>{data.table.columns.map((column) => <th key={column.key}>{column.label}</th>)}</tr></thead>
        <tbody>{data.table.rows.map((row) => (
          <tr key={row.id}>{data.table.columns.map((column) => {
            const cell = row.cells[column.key];
            const text = typeof cell === "object" && cell !== null ? cell.label : String(cell ?? "");
            return <td key={column.key}>{text}</td>;
          })}</tr>
        ))}</tbody>
      </table>

      <ol aria-label={data.timeline.title}>{data.timeline.items.map((item) => (
        <li key={item.id}><time dateTime={item.occurredUtc}>{item.occurredUtc}</time> {item.title}</li>
      ))}</ol>
    </main>
  );
}
Tip

For ThemeForest-level visual quality, keep this data contract stable and invest in typography, spacing rhythm, responsive grid composition, empty/loading skeletons, restrained motion, branded chart palettes, dense-versus-comfortable modes and excellent record-detail transitions. Theme tokens can change the look without changing the API or security model.

Give another module the same presentation capability

Do not force every endpoint to manufacture all six shapes. Keep natural list/detail/write endpoints, then add one purpose-built presentation binding per meaningful screen. A Contacts overview might return metrics, lifecycle distribution and recent contacts; a Files page might return storage metrics, MIME distribution and recent uploads; Universal Inbox might return queue metrics, status progress, recent items and a timeline.

1. Pick a screen use-case, not a table: for example, "work-order command center".
2. Define one or more presentationVersion 1 children using only the six normalized shapes.
3. Implement a reviewed, bounded, secured server contract. Apply tenant, active-user,
   DataRole, Location, soft-delete and record-specific fences before aggregation.
4. Register a DataConnector and DataBinding. The binding key is public vocabulary;
   physical server and database names are routing details and never appear in page JSON.
5. Add registry manifests for the blocks the page uses; do not add renderer-specific scripts.
6. Compose the Page in Puck. Reuse one bindingKey and select children with dataKey.
7. Test exact/empty/maximum cardinality, forbidden and cross-tenant requests, stale sessions,
   per-item batch failures, keyboard access, mobile overflow, and real browser rendering.
8. Rehearse on a disposable SaaS triple, apply to the golden SaaS DB, re-export it,
   regenerate contracts/knowledge, and prove live/export parity before release.
Important

A page binding is not authorization. The reviewed server contract must derive SaaS app, account and user from verified identity and enforce DataRoles, Locations, soft delete, direct/private record fences and module permissions before it counts, groups or returns anything.

Loading, errors, pagination and performance

  • A page may request 1–24 unique binding keys. Duplicate keys are rejected by the client parser; the native renderer deduplicates them before requesting.
  • The server resolves at most four bindings concurrently and returns Cache-Control: private, no-store. Connector-specific short caching is server controlled.
  • Empty collections are [], never null. An empty tenant still renders a valid zero/empty presentation.
  • The grid sorts only the already-authorized visible page. Search, filtering and deep pagination stay server-side.
  • Return only display-ready fields needed by the blocks. Do not fetch a million rows, serialize them, and reduce them in React.
  • Presentation status and tone are display metadata only. They never grant permission or prove workflow state.
  • Use separate bindings when panels have different refresh rates or failure boundaries; use one composed binding when they share scope, cache lifetime and screen intent.

Release verification

npm --prefix apps\tenant-runtime test -- src\PresentationContracts.test.ts src\ProfessionalPrimitives.test.tsx src\PageRenderer.test.tsx src\api.test.ts src\PuckPageDesignerAdapter.test.ts
npm --prefix apps\tenant-runtime run build

# Database reconciliation is rehearsal-only unless --apply is supplied.
python tools\reconcile_work_order_presentations.py --help

# Run the real authenticated journey with credentials supplied through environment variables.
tests\e2e\node_modules\.bin\playwright.cmd test --config tests\e2e\interactive-presentations-live.config.ts

The retained Work Orders example was exercised through real tenant login, SQL routing, the API batch, the production React build and Chromium. The observed result was one binding request, 4 metrics, 6 status rows, 6 recent rows, 3 chart periods, 6 grid rows and 6 timeline events; desktop content was 1,010px wide, mobile overflow was zero, and no authenticated API, page, console or accessible-name failure occurred. That is functional evidence, not a production capacity claim.

Continue with the Professional Template Foundation, headless application kit, and complete component catalog.