@dblm/middleware
v2.1.0
Published
REST middleware for dblm: self-service user registration, natural-language queries, and fassad templates, routed through the dblm broker (mTLS, role-based, horizontally scalable)
Maintainers
Readme
@dblm/middleware
HTTP middleware that exposes dblm over a REST API — register users, run natural-language queries, and run fassad templates — all routed through a dblm broker (dblm server). Flutter apps, dashboards, and other clients query your databases without ever touching a DSN, and the middleware itself holds no database credentials.
How it works
register once: end user ──► /register ──► broker issues a user + cert bundle
then, with the user's own cert (role-based):
end user ──► @dblm/middleware (HTTP) ──mTLS──► dblm server (broker fleet) ──► your databases
└─ shared Postgres: schema · knowledge ·
modules · history · fassad templates · usersThe middleware is a thin HTTP gateway in front of a dblm server broker. It exposes three surfaces — /register (provision a user + client cert), /qd (natural-language queries), and /fassad (stored query templates) — and forwards each to the broker over HTTP/2 + mTLS. Nothing is executed locally; the broker runs everything against the databases. Run the broker with state_backend=postgres and both the middleware and the broker scale horizontally — any instance serves any request.
Auth model. Every request (except /health) sends the service-level X-API-Key plus an X-User-Id identifying the end user. Browsing (GET /fassad, GET /fassad/:name) uses the middleware's service identity; executing (POST /qd, POST /fassad/:name/run) uses the end user's own client cert (from /register), so the broker enforces that user's role/ACL. For /qd, the user id also scopes conversation history.
Prerequisites
- A running dblm broker (
dblm server start) reachable over the network — started with--admin-token(for/register) and, for a shared/scalable fassad + user store,state_backend=postgres. - A service bundle for the middleware (
dblm server user add middleware-svc …). - Fassad templates created on the broker side with
dblm fassad create. - Node.js 18+ (the middleware does not need the
dblmbinary on its host).
Install
npm install -g @dblm/middleware
# or run without installing:
npx @dblm/middlewareConfiguration
Create a .env file (or set environment variables):
PORT=3000 # HTTP port (default: 3000)
API_KEY=secret # Required — all requests must send X-API-Key: <value>
# Broker connection (required — every endpoint routes through the broker).
# Preferred: the service bundle produced by `dblm server user add`
# (contains endpoint + CA + client cert/key).
DBLM_BROKER_BUNDLE=/etc/dblm/middleware.bundle.json
# ...or configure the pieces explicitly instead of a bundle:
# DBLM_BROKER_ENDPOINT=https://broker.internal:8443
# DBLM_BROKER_CLIENT_CERT=/etc/dblm/client.crt
# DBLM_BROKER_CLIENT_KEY=/etc/dblm/client.key
# DBLM_BROKER_CA_CERT=/etc/dblm/ca.crt
# DBLM_BROKER_TIMEOUT_MS=300000 # per-request timeout (default: 300000)
# User provisioning (required for /register). Must match the broker's
# `dblm server start --admin-token <value>`.
DBLM_ADMIN_TOKEN=change-me
# Role → grants mapping for /register: inline JSON, or a path via DBLM_ROLES_FILE.
DBLM_ROLES={"analyst":[{"connection":"pg","readOnly":true}],"ops":[{"connection":"pg"}]}
# DBLM_ROLES_FILE=/etc/dblm/roles.jsonThe middleware authenticates to the broker with its own service bundle
(DBLM_BROKER_BUNDLE). The broker must be started with a matching
--admin-token for /register to work.
API_KEY is required (service-level auth). Any request without a matching
X-API-Key header is rejected with 401.
Every request (except /health) must also send X-User-Id: <id> — the
unique id of the end user making the call. Requests missing it are rejected
with 400. The middleware authenticates to the broker as a single service
identity (the bundle above); the per-request X-User-Id is the end-user
identity, carried into the broker as the history session and LLM context.
Start
# using npx
npx @dblm/middleware
# or if installed globally
dblm-middlewareREST API
All endpoints (except /health) require both headers:
X-API-Key: <your-key> (service auth) and X-User-Id: <end-user-id> (caller identity).
GET /health
Health check — no auth required.
{ "status": "ok" }GET /fassad
List all fassad templates (served from the broker's shared template store).
[
{
"name": "top-films",
"mode": "sql",
"param_count": 1,
"created_at": "2024-01-15T10:00:00Z"
}
]GET /fassad/:name
Get full details for a template, including its parameters. Placeholders in the
body use {name} syntax.
{
"name": "top-films",
"mode": "sql",
"connection": "pg",
"body": "SELECT title FROM film ORDER BY rental_rate DESC LIMIT {limit}",
"params": [
{ "name": "limit", "kind": "int", "required": true }
],
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}POST /fassad/:name/run
Execute a template as the calling user, on the broker, against the fassad's
target connection. Like /qd, you present your own clientCert + clientKey
(from /register) so the broker enforces your role/ACL — a read-only grant
rejects a mutating template even with bound parameters. The cert's CommonName
must match X-User-Id.
Request: headers X-API-Key, X-User-Id; body:
{
"params": { "limit": "10" },
"clientCert": "-----BEGIN CERTIFICATE-----\n...",
"clientKey": "-----BEGIN EC PRIVATE KEY-----\n..."
}params— values for the template's declared parameters (bound positionally).clientCert+clientKey(required) — from your/registerbundle.db/module(optional) — override the fassad's stored target.
Response (SQL fassad): columns and rows.
{
"columns": ["title"],
"rows": [["ACE GOLDFINGER"], ["AFFAIR PREJUDICE"]],
"source": "pg"
}NLQ-mode fassads return the broker's full NLQ result instead.
POST /register
Self-service provisioning. Creates a user in the dblm broker with a role's
grants and issues them a client certificate. The desired user id is the
X-User-Id header; the role is in the body. Returns a bundle — keep the
client_key secret; you'll send the cert + key on every /qd call.
Request: headers X-API-Key, X-User-Id: [email protected]; body:
{ "role": "analyst", "days": 365 }Response 201: the user bundle.
{
"schema": "dblm-bundle/v1",
"endpoint": "https://broker.internal:8443",
"user": "[email protected]",
"ca_cert": "-----BEGIN CERTIFICATE-----\n...",
"client_cert": "-----BEGIN CERTIFICATE-----\n...",
"client_key": "-----BEGIN EC PRIVATE KEY-----\n..."
}Errors: 400 unknown/missing role (response includes knownRoles); 409 the
user already exists (re-issuance is an admin/CLI operation).
POST /qd
Run a natural-language query ("qd") through the broker as the calling user.
You present your own client_cert + client_key (from /register), so the
broker enforces your role/ACL. The cert's CommonName must match X-User-Id.
Request: headers X-API-Key, X-User-Id; body:
{
"question": "how many films are rated PG-13?",
"module": "sales",
"sessionId": "alice-2024-06-30",
"clientCert": "-----BEGIN CERTIFICATE-----\n...",
"clientKey": "-----BEGIN EC PRIVATE KEY-----\n..."
}question(required).clientCert+clientKey(required) — from your/registerbundle.moduleorconnections(mutually exclusive; optional) — omit both to use every connection your user is granted.sessionId(optional) — name an explicit conversation thread (use different ids for separate threads). Omitting it defaults the session to your user id, so each user has one continuing conversation by default.stateless(optional,true) — opt out of memory entirely for a genuine fire-and-forget query (no prior turns injected, no continuation).raw,noSummary,forceSummary,rule,context— optional.
Session modes: omit sessionId → your default per-user thread (continuity);
pass sessionId → a separate named thread; pass stateless: true → one-off, no
memory.
Response: the broker's NLQ result (sources, generated SQL, rows, summary).
Use as Express middleware
Instead of running a standalone server, you can mount the fassad router into your existing Express app:
import express from 'express';
import { createFassadRouter, createQdRouter, createRegisterRouter } from '@dblm/middleware/router';
const app = express();
app.use(express.json());
app.use('/register', createRegisterRouter({ apiKey: 'your-secret' }));
app.use('/fassad', createFassadRouter({ apiKey: 'your-secret' }));
app.use('/qd', createQdRouter({ apiKey: 'your-secret' }));
app.listen(3000);All the same REST endpoints are available under the mount paths. API-key auth
and the X-User-Id requirement are applied automatically. All three routers
forward to the broker, so they need the broker env vars described under
Configuration.
Example: curl
# 1. Register to get a bundle (save client_cert / client_key from the response)
curl -X POST http://localhost:3000/register \
-H "X-API-Key: secret" -H "X-User-Id: alice" \
-H "Content-Type: application/json" \
-d '{"role": "analyst"}'
# 2. List templates (uses the middleware's service identity)
curl -H "X-API-Key: secret" -H "X-User-Id: alice" http://localhost:3000/fassad
# 3. Run a template as alice (clientCert/clientKey from the bundle above)
curl -X POST http://localhost:3000/fassad/top-films/run \
-H "X-API-Key: secret" -H "X-User-Id: alice" \
-H "Content-Type: application/json" \
-d '{"params": {"limit": "10"}, "clientCert": "-----BEGIN CERTIFICATE-----\n...", "clientKey": "-----BEGIN EC PRIVATE KEY-----\n..."}'Using with the Flutter SDK
This middleware is the backend for the fassad_ui Flutter package. Point FassadClient at the middleware with your apiKey and the end user's X-User-Id.
Note: execution is now role-based through the broker. A client must first
POST /registerto obtain a bundle, then send that bundle'sclientCert+clientKeyon each/fassad/:name/runand/qdcall. Browsing (GET /fassad) needs only the API key +X-User-Id. (Older SDK versions that calledrunwithout a cert target the pre-broker middleware and need updating.)
License
MIT
