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 validated 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/ai-openai @askdb/config pg
PackageWhy
@askdb/clientcreateAskDb() — resolves the schema, model, and dialect from config.
@askdb/ai-openaiThe adapter your model is built from. Swap -openai for -anthropic, -google, or -azure.
@askdb/configLoads .env and askdb.config.ts at startup.
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 { openaiProvider } from "@askdb/ai-openai";
import { Pool } from "pg";
bootstrapAskDbEnv({ cwd: process.cwd() });
const askdb = createAskDb({
config: getAskDbRuntimeConfig(),
providers: [openaiProvider],
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 validated SQL — your handler logs it, runs it, and returns the result.

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

FieldTypeWhen present
sqlstringAlways. The generated, validated SQL string.
unboundSqlstring | undefinedWhen parameterize is on (default) and the model returned consistent extras. Driver markers.
paramsQueryParameterValue[] | undefinedPositional values for unboundSql. Prefer over tenantParams when present.
parametersQueryParameterBinding[] | undefinedNamed bindings for form UIs (includes values).
preparedQueryPreparedQuery | undefinedDefinitions + template for 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-only positional $N values.
tenantBindingsTenantBinding[] | undefinedWhen tenant scope bindings were resolved. Structured binding metadata for audit logging.
import { bindPreparedQuery } from "@askdb/core";
const result = await askdb.ask(question, { tenantScope });
await pool.query(result.sql);
await pool.query(result.unboundSql!, result.params);
const rebound = bindPreparedQuery(result.preparedQuery!, {
state_name: "Utah",
":tenant_agency_ids": authorizedAgencyIds,
});
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. bindPreparedQuery does not authorize tenant IDs.

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-sdk/openai pg

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 { openaiProvider } from "@askdb/ai-openai";
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(),
providers: [openaiProvider],
// 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.