Documentation

Headless and full UI SDKs

Prefilled from the node-sdk and react-sdk package READMEs — edit the docs components in components/docs/* to refine.

Code blocks use highlight.js (same stack as the blog and HighlightedCode). The embed demo setup tab uses hand-colored spans for a fixed “editor” look — see comments in docs-code.tsx and demo/embed/page.tsx.

Node SDK

A TypeScript-first client for the QueryPanel API. Its primary function is to generate SQL from natural language, but it also signs JWTs with your service private key, syncs database schemas, enforces tenant isolation, and wraps public routes (query, ingest, charts, active charts, knowledge base).

Package: @querypanel/node-sdk (published name; some older README snippets may show @querypanel/sdk).

Installation

npm install @querypanel/node-sdk
# or
bun add @querypanel/node-sdk

Runtime: Node.js 18+, Deno, or Bun. The SDK uses the Web Crypto API for JWT signing and the native fetch API.

Quickstart

attach databases, sync schema, ask()
import { QueryPanelSdkAPI } from "@querypanel/node-sdk";
import { Pool } from "pg";

const qp = new QueryPanelSdkAPI(
  process.env.QUERYPANEL_URL!,
  process.env.PRIVATE_KEY!,
  process.env.QUERYPANEL_WORKSPACE_ID!,
  { defaultTenantId: process.env.DEFAULT_TENANT_ID },
);

const pool = new Pool({ connectionString: process.env.POSTGRES_URL });

const createPostgresClient = () => async (sql: string, params?: unknown[]) => {
  const client = await pool.connect();
  try {
    const result = await client.query(sql, params);
    return {
      rows: result.rows,
      fields: result.fields.map((field) => ({ name: field.name })),
    };
  } finally {
    client.release();
  }
};

qp.attachPostgres(
  "pg_demo",
  createPostgresClient(),
  {
    database: "pg_demo",
    description: "PostgreSQL demo database",
    tenantFieldName: "tenant_id",
    enforceTenantIsolation: true,
    allowedTables: ["orders"],
  },
);

qp.attachClickhouse(
  "clicks",
  (params) => clickhouse.query(params),
  {
    database: "analytics",
    tenantFieldName: "customer_id",
    tenantFieldType: "String",
  },
);

await qp.syncSchema("analytics", { tenantId: "tenant_123" });

const response = await qp.ask("Top countries by revenue", {
  tenantId: "tenant_123",
  database: "analytics",
});

console.log(response.sql);
console.log(response.params);
console.table(response.rows);
console.log(response.chart.vegaLiteSpec);

Custom system instructions (v2) & VizSpec

Inject extra system prompt text for tenant policies (e.g. data retention). Restrict chart kinds with supportedChartTypes when using chartType: "vizspec".

systemPrompt on ask()
const response = await qp.ask("Revenue by product", {
  tenantId: "tenant_123",
  database: "analytics",
  pipeline: "v2",
  systemPrompt: "Retention policy: only query data from the last 30 days.",
});
supportedChartTypes
import { QueryPanelSdkAPI, ALL_VIZ_CHART_TYPES, type ChartType } from "@querypanel/node-sdk";

const allowed: ChartType[] = ["line", "bar", "column", "pie"];

const qp = new QueryPanelSdkAPI(url, privateKey, workspaceId, {
  supportedChartTypes: allowed,
});

await qp.ask("Revenue by month", {
  tenantId: "t1",
  database: "analytics",
  chartType: "vizspec",
  pipeline: "v2",
  supportedChartTypes: allowed,
});

Use ALL_VIZ_CHART_TYPES when you need the full list to filter client-side.

Session history & context-aware queries

Link follow-ups (e.g. "filter that to Europe") by reusing querypanelSessionId from the previous response.

const first = await qp.ask("Revenue by country", {
  tenantId: "tenant_123",
  database: "analytics",
});

const followUp = await qp.ask("Now filter that to Europe", {
  tenantId: "tenant_123",
  database: "analytics",
  querypanelSessionId: first.querypanelSessionId,
});

Managing session history

const sessions = await qp.listSessions({
  tenantId: "tenant_123",
  pagination: { page: 1, limit: 20 },
  sortBy: "updated_at",
});

const session = await qp.getSession("session_abc123", {
  tenantId: "tenant_123",
  includeTurns: true,
});

await qp.updateSession(
  "session_abc123",
  { title: "Q4 Revenue Analysis" },
  { tenantId: "tenant_123" },
);

await qp.deleteSession("session_abc123", { tenantId: "tenant_123" });

Saving & managing charts

QueryPanel stores chart definition (SQL, parameters, Vega-Lite spec) — not result rows. Data is loaded live from your database when charts render.

const response = await qp.ask("Show revenue by country", {
  tenantId: "tenant_123",
  database: "analytics",
});

if (response.chart.vegaLiteSpec) {
  const savedChart = await qp.createChart(
    {
      title: "Revenue by Country",
      prompt: "Show revenue by country",
      sql: response.sql,
      sql_params: response.params,
      vega_lite_spec: response.chart.vegaLiteSpec,
      query_id: response.queryId,
      target_db: response.target_db,
    },
    { tenantId: "tenant_123", userId: "user_456" },
  );
}

const charts = await qp.listCharts({ tenantId: "tenant_123" });

List all & bulk fetch charts

listAllCharts() returns { data, pagination } (same as listCharts()). Older code expecting a plain array should migrate to result.data.

const { data, pagination } = await qp.listAllCharts({
  tenantId: "tenant_123",
  pagination: { page: 1, limit: 50 },
  includeData: true,
});

const { data: bulk, missingIds } = await qp.getChartsByIds(
  ["550e8400-e29b-41d4-a716-446655440000"],
  { tenantId: "tenant_123", includeData: true },
);

Modifying charts

modifyChart() edits SQL and/or visualization, re-executes, and regenerates charts. Works with fresh ask() results or saved charts. Combine vizModifications, sqlModifications, and optional querypanelSessionId for follow-up context.

Visualization only

const modified = await qp.modifyChart(
  {
    sql: response.sql,
    question: "revenue by country",
    database: "analytics",
    vizModifications: {
      chartType: "bar",
      xAxis: { field: "country", label: "Country" },
      yAxis: { field: "revenue", label: "Total Revenue", aggregate: "sum" },
    },
  },
  { tenantId: "tenant_123" },
);

Time granularity & SQL changes

const monthly = await qp.modifyChart(
  {
    sql: response.sql,
    question: "revenue over time",
    database: "analytics",
    sqlModifications: {
      timeGranularity: "month",
      dateRange: { from: "2024-01-01", to: "2024-12-31" },
    },
  },
  { tenantId: "tenant_123", querypanelSessionId: response.querypanelSessionId },
);

Custom SQL & saved charts

const customized = await qp.modifyChart(
  {
    sql: response.sql,
    question: "revenue by country",
    database: "analytics",
    sqlModifications: {
      customSql: `SELECT country, SUM(revenue) as total_revenue
        FROM orders WHERE status = 'completed' GROUP BY country`,
    },
  },
  { tenantId: "tenant_123" },
);

const savedChart = await qp.getChart("chart_id_123", { tenantId: "tenant_123" });
const fromSaved = await qp.modifyChart(
  {
    sql: savedChart.sql,
    question: savedChart.prompt ?? "original question",
    database: savedChart.target_db ?? "analytics",
    params: savedChart.sql_params as Record<string, unknown>,
    vizModifications: { chartType: "line" },
  },
  { tenantId: "tenant_123" },
);

Active charts & dashboards

Pin saved charts to a dashboard, order tiles, and load live data. listAllActiveCharts() calls GET /active-charts/all when you need every pin without walking pages. getActiveChartsByIds() bulk-fetches by active-chart row IDs (up to 100 UUIDs).

const activeChart = await qp.createActiveChart(
  {
    chart_id: "saved_chart_id_from_history",
    order: 1,
    meta: { width: "full", variant: "dark" },
  },
  { tenantId: "tenant_123" },
);

const dashboard = await qp.listActiveCharts({
  tenantId: "tenant_123",
  withData: true,
});

const all = await qp.listAllActiveCharts({
  tenantId: "tenant_123",
  withData: true,
});

const { data, missingIds } = await qp.getActiveChartsByIds(
  ["550e8400-e29b-41d4-a716-446655440000"],
  { tenantId: "tenant_123", withData: true },
);

Deno support & building locally

Deno (e.g. Supabase Edge)
import { QueryPanelSdkAPI } from "https://esm.sh/@querypanel/node-sdk";

const qp = new QueryPanelSdkAPI(
  Deno.env.get("QUERYPANEL_URL")!,
  Deno.env.get("PRIVATE_KEY")!,
  Deno.env.get("QUERYPANEL_WORKSPACE_ID")!,
);

const response = await qp.ask("Show top products", { tenantId: "tenant_123" });
Build from source (monorepo)
cd node-sdk
bun install
bun run build

Emits dual ESM/CJS + types to dist/ via tsup.

Authentication, errors & SQL retry

Requests are signed with RS256 using your private key. The payload includes organizationId and tenantId; add userId / scopes per call when needed. Pass extra headers through the constructor for custom middleware.

HTTP errors surface as Error with status and optional details. syncSchema skips embedding when unchanged unless forceReindex: true.

Automatic SQL repair

const response = await qp.ask("Show revenue by country", {
  tenantId: "tenant_123",
  maxRetry: 3,
});

console.log(`Query succeeded after ${response.attempts} attempt(s)`);

Without maxRetry, execution errors throw immediately.

React SDK

Ship customer-facing analytics in React: the main product story is the embedded dashboard — a single component that loads a published workspace with AI editing, charts, and optional tenant customization. Your workspace private key stays on the server; you mint a short-lived tenant JWT there, and the embed calls the QueryPanel API at apiBaseUrl with Authorization: Bearer — no secrets in the browser.

Package: @querypanel/react-sdk

Try it: the interactive embedded dashboard demo walks through JWT setup and a live QuerypanelEmbedded run — same pattern you will copy into your app.

Embedded dashboard — one component

QuerypanelEmbedded is the fastest path to a full analytics experience: pass a dashboard id, the QueryPanel API base URL (the same host you use with the Node SDK, e.g. your cloud region or self-hosted deployment), and a JWT you generate for the end user. The component issues authenticated requests to that API. Optional allowCustomization enables copy-on-write forks; darkMode, colorPreset, theme, and branding cover look-and-feel and white-label copy.

On the server, use the Node SDK to sign a tenant-scoped token (e.g. createJwt with tenantId, userId, and scopes). Pass the resulting JWT to the browser; the embed sends it to QueryPanel — not a copy of your workspace private key.

Server: mint JWT, then client: one embed
// Your API route (Node) — e.g. matches /demo/embed “Run embed” flow
import { QueryPanelSdkAPI } from "@querypanel/node-sdk";

const qp = new QueryPanelSdkAPI(apiBaseUrl, privateKeyPem, organizationId);

const jwt = await qp.createJwt({
  tenantId: "tenant_abc",
  userId: "user_123",
  scopes: ["dashboards:read", "charts:read"],
});

// In your customer-facing React app:
import { QuerypanelEmbedded } from "@querypanel/react-sdk";

export function CustomerAnalytics() {
  return (
    <QuerypanelEmbedded
      dashboardId="YOUR_DASHBOARD_UUID"
      apiBaseUrl="https://api.querypanel.io" // same QueryPanel API base URL as the Node SDK
      jwt={jwt}
      allowCustomization
      darkMode
    />
  );
}

The live demo at /demo/embed pairs this with a session generator and a full-screen preview so you can validate the end-to-end flow in minutes.

Callbacks: onLoad, onError, onCustomize (when a customer forks). See the TypeScript QuerypanelEmbeddedProps type in the package for the full list.

Installation

npm install @querypanel/react-sdk
# or
pnpm add @querypanel/react-sdk
# or
yarn add @querypanel/react-sdk

Import the distributed CSS from the package when your bundler requires an explicit stylesheet entry (see package exports and README).

Custom NL-to-chart UI (provider)

If you are building a bespoke experience instead of the full embed, use QueryPanelProvider and wire your own /api/ask routes. This path gives you QueryInput, QueryResult, and loading/empty/error states.

QueryPanelProvider + useQueryPanel
import {
  QueryPanelProvider,
  QueryInput,
  QueryResult,
  LoadingState,
  EmptyState,
  ErrorState,
  useQueryPanel,
} from "@querypanel/react-sdk";

function App() {
  return (
    <QueryPanelProvider
      config={{
        askEndpoint: "/api/demo/ask",
        modifyEndpoint: "/api/demo/modify",
        colorPreset: "default",
      }}
    >
      <Dashboard />
    </QueryPanelProvider>
  );
}

function Dashboard() {
  const { query, result, isLoading, error, ask, modify, colorPreset } = useQueryPanel();
  // ... QueryInput, QueryResult, state components
}

Composable building blocks

Mix and match lower-level pieces — VegaChart, DataTable, ChartControls — for screens that you fully control. Same theming entry points as the provider.

import { VegaChart, DataTable, ChartControls } from "@querypanel/react-sdk";
import { getColorsByPreset } from "@querypanel/react-sdk/themes";

const colors = getColorsByPreset("ocean");
// <ChartControls ... /> <VegaChart spec={...} colors={colors} /> <DataTable ... />

Theming

Presets: default, sunset, emerald, ocean.

import { getColorsByPreset, createTheme } from "@querypanel/react-sdk/themes";

const customTheme = createTheme({
  colors: { primary: "#FF6B6B", secondary: "#4ECDC4" },
  borderRadius: "1rem",
  fontFamily: "Inter, sans-serif",
});

White-labeling

Components accept a colors prop; embeds support branding for toolbar and AI copy.

<VegaChart
  spec={spec}
  colors={{
    primary: "#YOUR_BRAND_COLOR",
    text: "#ffffff",
    muted: "#888888",
  }}
/>

Types

interface QueryResult {
  success: boolean;
  sql?: string;
  rows?: Array<Record<string, unknown>>;
  fields?: string[];
  chart?: {
    vegaLiteSpec?: Record<string, unknown>;
    specType: "vega-lite" | "vizspec";
  };
}

More components & live examples

Beyond the embed and provider stack, the React SDK includes dashboard editors, AI chart flows, and many composable details. This page is a high-level map; for every prop, story, and edge case, use the interactive catalog.

Open Storybook — all components, controls, and docs

Start with the QuerypanelEmbedded and QueryPanelProvider stories, then explore inputs, states, and layout pieces as you need them.

License

MIT (per package README).