Skip to content

Embed AskDB in a Node app

Integrator guide

Drop @askdb/client into a TypeScript service. It reads the same askdb.config.ts the CLI and Studio use, generates checked SQL from a question, and hands it back. Your service runs it through its own connection pool against a read-only role.

Terminal window
npm install @askdb/client @askdb/core @askdb/config ai @ai-sdk/openai pg
PackageWhy
@askdb/clientcreateAskDb() — resolves the schema, model, and dialect from config.
@ai-sdk/openaiThe AI SDK package for the provider your config selects. Swap it for @ai-sdk/anthropic, @ai-sdk/google, or @ai-sdk/azure (the Vercel AI Gateway needs none). AskDB loads it on first use.
@askdb/configLoads .env and askdb.config.ts at startup.
@askdb/core, aiRequired peer dependencies of @askdb/client (ai at AI SDK 7). npm and pnpm add them for you; Yarn doesn’t, so list them.
pgThe Postgres driver. AskDB doesn’t open the connection; your app does.

Call createAskDb once at startup, then askdb.ask(question) per request. Schema, model, and dialect all resolve from askdb.config.ts — the same file askdb init scaffolded.

ask-handler.ts
import { createAskDb } from "@askdb/client";
import { bootstrapAskDbEnv, getAskDbRuntimeConfig } from "@askdb/config";
import { Pool } from "pg";
bootstrapAskDbEnv({ cwd: process.cwd() });
const askdb = createAskDb({
config: getAskDbRuntimeConfig(), // ai.provider picks the model
schema: { path: "./my-app.schema" }, // or set host.schemaPath in config and omit
});
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Run under a read-only role for defense in depth.
statement_timeout: 5000,
max: 10,
});
export async function askQuestion(question: string) {
const { sql } = await askdb.ask(question);
// Audit hook — log every question and generated SQL.
console.log({ question, sql });
const { rows } = await pool.query(sql);
return { sql, rows };
}

./my-app.schema is a directory, not a file — <name>.schema/ is AskDB’s convention for the introspected-and-enriched artifact. The schema loads once at startup and stays in memory; call askdb.reload() if the artifact changes while the process runs.

That’s the whole pipeline. AskDB returns checked SQL — your handler logs it, runs it, and returns the result.

askdb.ask() (and the direct ask() below) return AskPipelineResult. All fields except sql and verdict are optional:

FieldTypeWhen present
sqlstringAlways. The generated, checked SQL string.
verdictGuardrailVerdictAlways. The guardrail findings and the outcome ("allow" or "warn"; a "deny" throws). See Guardrail verdict.
unboundSqlstring | undefinedWhen parameterize is on (default) and the model returned consistent extras. Driver markers.
paramsQueryParamSlot[] | undefinedValues for unboundSql, in marker order (includes tenant IDs in tenantSqlMode: "sql-params").
parametersQueryParameterBinding[] | undefinedNamed bindings for form UIs (includes values).
preparedQueryPreparedQuery | undefinedDefinitions + template for askdb.bind() / bindPreparedQuery() (no values).
usageAskUsage | undefinedWhen the model provider reports token counts: promptTokens, completionTokens, totalTokens (each number | null). Use it for cost tracking or quota enforcement.
explainunknown | undefinedWhen explain: true is passed. Heuristic guardrail metadata — which read-only rules fired and why.
tenantGuardrailTenantGuardrailResult | undefinedWhen a tenant policy is active. Whether the SQL references tenant-scoped tables correctly.
tenantParamsunknown[] | undefinedWhen tenantSqlMode: "sql-params" is set. Tenant IDs for the markers in sql — run sql with tenantParams, or unboundSql with params; never concatenate them.
tenantBindingsTenantBinding[] | undefinedWhen tenant scope bindings were resolved. Structured binding metadata for audit logging.
const result = await askdb.ask(question, { tenantScope });
await pool.query(result.sql);
await pool.query(result.unboundSql!, result.params);
// A new value, no model call: re-checked under the client's schema and this scope.
const rebound = await askdb.bind(result.preparedQuery!, { state_name: "Utah" }, { tenantScope });
await pool.query(rebound.sql);
if (result.usage) {
console.log(`Tokens — prompt: ${result.usage.promptTokens}, completion: ${result.usage.completionTokens}`);
}

Every ask() is still one model call. Set { parameterize: false } to skip the extra output tokens. askdb.bind() re-runs the guardrails on the stored template and renders tenant IDs from the tenantScope you pass, so authorizing that scope stays your job, as for ask(). See client.bind().

The full result and option types are in the @askdb/core API reference.

createAskDb is a convenience layer over @askdb/core’s pure ask() — not a replacement. Drop down to ask() when you construct the model yourself, serve multiple schemas from one process, or want zero dependency on @askdb/config / @askdb/ai.

Terminal window
npm install @askdb/core ai @ai-sdk/openai pg

ai is a required peer dependency of @askdb/core (AI SDK 6 or 7) — your app owns its version.

Dialects for all four engines are built into @askdb/core — install @askdb/postgres only if this service also runs askdb introspect programmatically.

ask-handler.ts
import { ask, loadSchema } from "@askdb/core";
import { openai } from "@ai-sdk/openai";
import { Pool } from "pg";
// loadSchema is synchronous; it autodetects the artifact directory,
// a bundled .bundle.json, or a bare schema.json. Load once at startup.
const schema = loadSchema("./my-app.schema");
const model = openai("gpt-4o-mini");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function askQuestion(question: string) {
const { sql } = await ask({
question,
schema,
dialect: "postgres",
model,
});
const { rows } = await pool.query(sql);
return { sql, rows };
}

Every option ask() takes — modes, tenant scope, retrieval, sensitive-field handling — is in the @askdb/core API reference. The same options work as per-call overrides on askdb.ask(question, overrides).

Same ask() call, different dialect string. Change dialect: "postgres" to "mysql", "sqlite", or "sqlserver". See Switch engines for the full migration matrix.

The snippet below is a copy-paste-ready Express server. Schema, model, and dialect are fully config-driven — askdb.config.ts sets host.schemaPath and the AI provider; server.ts only handles HTTP.

The /ask endpoint accepts an optional execute flag: false (default) returns just the SQL; true also runs it through a pg.Pool and returns the rows. This keeps SQL generation and execution decoupled — your service controls both.

server.ts
import { createAskDb } from "@askdb/client";
import { bootstrapAskDbEnv, getAskDbRuntimeConfig } from "@askdb/config";
import express, { Request, Response } from "express";
import pg from "pg";
bootstrapAskDbEnv({ cwd: process.cwd() });
const app = express();
app.use(express.json());
const askdb = createAskDb({
config: getAskDbRuntimeConfig(),
// schema resolved from host.schemaPath in askdb.config.ts
});
const pool = process.env.DATABASE_URL
? new pg.Pool({ connectionString: process.env.DATABASE_URL })
: null;
app.post("/ask", async (req: Request, res: Response) => {
const { question, execute } = req.body as { question?: string; execute?: boolean };
if (!question || typeof question !== "string" || question.trim() === "") {
res.status(400).json({ error: "question is required and must be a non-empty string" });
return;
}
const shouldExecute = execute === true;
if (shouldExecute && !pool) {
res.status(500).json({ error: "DATABASE_URL is not configured" });
return;
}
try {
const { sql } = await askdb.ask(question.trim());
console.log({ question, sql, execute: shouldExecute });
if (!shouldExecute) {
res.json({ sql });
return;
}
const result = await pool!.query(sql);
res.json({ sql, rows: result.rows, rowCount: result.rowCount });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
res.status(500).json({ error: message });
}
});
app.get("/health", (_req: Request, res: Response) => {
res.json({ status: "ok" });
});
app.listen(3000, () => console.log("listening on http://localhost:3000"));
askdb.config.ts
import { defineConfig, env, type AskDbConfig } from "@askdb/config";
export default defineConfig({
ai: {
provider: "openai",
providerConfig: { openai: { apiKey: env("OPENAI_API_KEY") } },
},
introspection: {
provider: "postgres",
providerConfig: { postgres: { databaseUrl: env("DATABASE_URL") } },
},
host: {
schemaPath: env("ASKDB_SCHEMA_PATH"),
},
} satisfies AskDbConfig);

The full working package (with package.json, tsconfig.json, and .env instructions) lives in examples/express-server in the repo.