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-sdkRuntime: Node.js 18+, Deno, or Bun. The SDK uses the Web Crypto API for JWT signing and the native fetch API.
Quickstart
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".
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.",
});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
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" });cd node-sdk
bun install
bun run buildEmits 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.
// 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-sdkImport 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.
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 docsStart with the QuerypanelEmbedded and QueryPanelProvider stories, then explore inputs, states, and layout pieces as you need them.
License
MIT (per package README).