The AskSQL server sidecar. It holds database credentials, enforces the SQL guard server-side (the browser never gets a raw database connection), applies your auth hook per request, and streams chat responses over SSE. An Express adapter is included.
npm i @asksql/core @asksql/server @asksql/postgres pg express @ai-sdk/groq
@asksql/core is a peer dependency, and yarn (or npm with legacy-peer-deps) will not install it
for you, so name it explicitly as above.
npx --package=@asksql/server asksql serve --provider ollama --model qwen2.5-coder:7b
The bin is asksql but the package is @asksql/server, so --package is
required - a bare npx asksql resolves to a package that does not exist. If you
would rather have it installed once, npm i -g @asksql/server (plus your engines'
connector packages, e.g. @asksql/postgres pg) then just
asksql serve ….
Listens on 127.0.0.1:3000 and accepts database connections from the client at
runtime, so the AskSQL browser extension can offer an engine/host/port/user/password
form (PostgreSQL, MySQL, Oracle, MongoDB, SQLite, DuckDB). Point the extension at
http://localhost:3000.
Runtime connections are what make that form possible, so the server stays on loopback
unless you pass --host and --allow-host <db-host> naming which databases it may
open. On a loopback bind it also accepts SQLite/DuckDB file paths from the client and
requires a loopback Host header; bind anywhere else and both of those switch off, so a
client-supplied file path is refused. asksql serve --help lists every flag.
Opening a database needs that engine's connector next to the server - with npx,
chain packages (npx --package=@asksql/server --package=@asksql/postgres --package=pg asksql serve ...);
with a global install, npm i -g @asksql/postgres pg once. The OpenAI-compatible SDK
ships with the server, so --provider ollama, nvidia or openai-compatible (LM
Studio, gateways) needs nothing extra; openai, anthropic, google, azure and
groq need their own @ai-sdk/* package installed alongside, plus --api-key.
To embed the server instead, construct AskSqlServer yourself and pass
dynamicConnections: { enabled: true } only if you want that same behaviour.
import express from 'express';
import { asksqlMiddleware } from '@asksql/server/express';
import { PostgresConnector } from '@asksql/postgres';
import { resolveModel } from '@asksql/core';
const app = express();
const connector = new PostgresConnector({
id: 'main',
name: 'Production (read-only)',
connectionString: process.env.DATABASE_URL,
});
const model = await resolveModel({
provider: 'groq',
model: 'openai/gpt-oss-20b', // an example: use whatever your provider lists at /models
apiKey: process.env.GROQ_API_KEY,
});
app.use('/asksql', asksqlMiddleware({
connectors: [connector],
engine: { model },
// Required: resolve identity + which connections this caller may reach.
// Derive it from YOUR session/JWT; never trust a client-supplied id.
// `req.headers` keys are lowercased; there is no `req.header()` accessor.
auth: (req) => ({ userId: req.headers['x-user'] ?? 'anon', allowedConnectionIds: ['main'] }),
// Optional: observe every failure the server turns into a response.
onError: (err, { method, path }) => console.error('asksql error', method, path, err),
}));
app.listen(3000);
Point @asksql/react or
@asksql/widget at /asksql and you have a full
chat UI. Credentials and the model key stay on the server.
| Field | Required | Notes |
|---|---|---|
connectors |
yes | The database connections the server may reach. |
engine |
yes | Shared engine settings, at least { model }. |
auth |
yes | (req) => { userId, allowedConnectionIds }. No anonymous default. |
audit |
no | Sink called for every executed query. |
onError |
no | Best-effort hook for every error turned into a response (throwing from it is swallowed). |
maxBodyBytes |
no | Request body cap on the Express adapter. Default 64 KB. |
suggestFixOnError |
no | Offer a corrected query on a DB error. Default true. |
requireLoopbackHost |
no | Reject any request whose Host header is not loopback, with 403 SERVER_AUTHZ. Default off. asksql serve turns it on when it binds a loopback address. |
dynamicConnections |
no | Runtime connection management. Off unless { enabled: true }; see below. |
dynamicConnections takes four fields:
| Field | Notes |
|---|---|
enabled |
Must be true, or POST /connections and DELETE /connections/:id return 404. |
allowedHosts |
Database hosts the server may open. Unset means any host, minus the link-local block below. Not consulted for file engines. |
allowFileEngines |
Allow client-supplied SQLite/DuckDB file paths. Unset means no. |
allowedFileRoots |
Directories a file path must resolve inside, symlinks resolved first. |
A client-supplied file path is refused (INVALID_INPUT, 400) unless you set allowFileEngines: true or a non-empty allowedFileRoots. Setting only allowFileEngines allows any path the
server process can read, so pair it with allowedFileRoots unless the server is single-user on
loopback. asksql serve sets allowFileEngines only when it binds a loopback address.
With suggestFixOnError on, a SQL query that fails at the database gets a second model call for
a corrected query, returned as suggestedSql for the user to review and re-run; it never
auto-runs. The request must carry the original question - without the intent a repair is
guesswork - and the MongoDB path does not offer one. The driver's error text is redacted before
that call: row values are stripped out, the column, table and constraint names the model needs
are kept.
The wire response never includes internal error detail (hostnames, driver text); only a code and a
safe userMessage. Use onError if you need the full error server-side.
Every endpoint runs your auth hook first and checks the caller's connection scope.
POST /chat - streams the answer as SSE: stage/token events, then a final sql
event (with explanation and autoLimited), then done. For MongoDB the sql field
carries the aggregation pipeline plus a collection.POST /execute - runs a statement through the server-side guard; accepts maxRows.
On a database error the response can carry suggestedSql for the user to review.POST /explain - plain-language explanation of a statement (Mongo pipelines included).POST /explainSchema - schema questions in prose on any engine (MongoDB answers in
collections and $lookup), including advisory add-a-column / index / performance
suggestions as never-executed proposals. A question unrelated to data is declined.GET /schema - cached catalog; refresh=1 re-introspects.GET /history - the caller's query history, paginated (page, per_page).POST /feedback - marks a question/SQL pair good; stored per user as a few-shot example
when engine.fewShots is configured.GET /health - liveness plus the connections this caller may reach.POST /connections / GET /connections / DELETE /connections/:id - runtime
connection management. POST and DELETE return 404 unless dynamicConnections is
enabled; GET always lists the connections this caller may reach. Creating one needs the
wildcard scope: auth must return allowedConnectionIds: ['*'] (ANY_CONNECTION), the same
authorization that lets a caller reach every connection. DELETE uses the ordinary per-id check.A dropped client connection aborts the work behind it: the request's AbortSignal is passed to
ask, execute, explain and explainSchema, so Postgres and MySQL cancel the running query at
the database rather than leaving it to finish unread.
ssl: 'disable' | 'trust' | 'verify'.allowedConnectionIds: ['*'] (exported as ANY_CONNECTION) to grant every
connection - the right scope for a single-user sidecar, where runtime connections
get server-generated ids no static list could name.@asksql/postgres, @asksql/mysql, @asksql/oracle, @asksql/mongodb,
@asksql/sqlite, @asksql/duckdb).GET/HEAD/OPTIONS must be application/json (else 415). This is
in the framework-agnostic handler, so every adapter gets it. The requirement forces a CORS
preflight, so a cross-site "simple request" can never reach /execute. The Express adapter
takes its own options as a second argument -
asksqlMiddleware(config, { cors: ['https://app.example.com'] }) - and keeps SSE alive behind
proxies (heartbeats, anti-buffering headers).requireLoopbackHost check both run before your auth hook,
so a rejected request never reaches it.Full documentation: https://github.com/rahulmahadik/AskSQL
API reference: rahulmahadik.github.io/AskSQL