Skip to content

Embed AskDB in a Node app

Integrator guide

Drop @askdb/core into a TypeScript service. AskDB loads your schema artifact, 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/core @ai-sdk/openai pg
PackageWhy
@askdb/coreThe pipeline — loadSchema, ask, validation.
Dialects for all four engines are built into @askdb/core; install @askdb/postgres only if this service also runs askdb introspect programmatically.
@ai-sdk/openaiOpenAI’s AI SDK provider — bring your own model provider.
pgThe Postgres driver. AskDB doesn’t open the connection; your app does.

The schema artifact is read from disk. Load it at startup and keep it in memory — it doesn’t need to be reloaded per request.

schema.ts
import { loadSchema } from "@askdb/core";
// Point at the artifact directory that `askdb introspect` wrote —
// your `introspection.outputDir` (e.g. ./my-app.schema or the default ./askdb).
// loadSchema also accepts a bundled .bundle.json file.
export const schema = await loadSchema("./my-app.schema");

./my-app.schema is a directory, not a file — <name>.schema/ is AskDB’s convention for the introspected-and-enriched artifact. loadSchema autodetects a directory, a bundled JSON, or a bare schema.json.

ask-handler.ts
import { ask } from "@askdb/core";
import { openai } from "@ai-sdk/openai";
import { Pool } from "pg";
import { schema } from "./schema.js";
const model = openai("gpt-4o-mini");
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 ask({
question,
schema,
dialect: "postgres",
model,
});
// Audit hook — log every question and generated SQL.
console.log({ question, sql });
const { rows } = await pool.query(sql);
return { sql, rows };
}

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

Shorter: let config resolve schema + model

Section titled “Shorter: let config resolve schema + model”

If you already use askdb.config.ts, @askdb/client removes the manual schema load and model construction. createAskDb resolves both from config; you pass only the question.

Terminal window
npm install @askdb/client @askdb/ai @askdb/ai-openai @askdb/config pg
ask-handler.ts
import { createAskDb } from "@askdb/client";
import { getAskDbRuntimeConfig } from "@askdb/config";
import { createAiRegistry } from "@askdb/ai";
import { openaiProvider } from "@askdb/ai-openai";
import { Pool } from "pg";
const askdb = createAskDb({
config: getAskDbRuntimeConfig(), // after bootstrapAskDbEnv() at startup
registry: createAiRegistry([openaiProvider]),
schema: { path: "./my-app.schema" }, // or set host.schemaPath in config and omit
});
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function askQuestion(question: string) {
const { sql } = await askdb.ask(question);
const { rows } = await pool.query(sql);
return { sql, rows };
}

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 { createAiRegistry } from "@askdb/ai";
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(),
registry: createAiRegistry([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.