blendx docs

Configuration and deployment

blendx.config.ts

The CLI reads blendx.config.ts in the app folder, and server.ts imports it to open the database. Every key is optional:

import { defineConfig } from 'blendx';

export default defineConfig({
  database: process.env.DATABASE_URL ? { driver: 'pg' } : { driver: 'pglite', url: './.data' },
  openapi: { title: 'Expenses API', version: '1.0.0' },
});
KeyDefault
schema./schema.dbmlthe schema
blends./blendsthe folder of blends
app./src/app.tsthe module that default-exports defineApp(...)
generated./src/generatedwhere blendx generate writes
review./reviewwhere blendx review writes, and where the examples are
migrations./drizzlethe migrations
database.driver'pg''pg', 'postgres-js', 'bun-sql' or 'pglite'
database.urlDATABASE_URLwhere the database is
openapi.title, openapi.version'blendx API', '0.1.0'the OpenAPI document's info

Paths are relative to the config file. defineConfig checks the values and throws on a bad one, such as an unknown driver.

Databases

blendx runs on PostgreSQL only. Pick a driver and install its package; blendx imports only the one you choose, and a missing one is an error that names the package.

DriverPackageRuns on
pgpgBun and Nodethe default
postgres-jspostgresBun and Node
bun-sqlnone: it is Bun's own SQLBun
pglite@electric-sql/pgliteBun and NodePostgreSQL in-process: development and tests

The server drivers take database.url, or DATABASE_URL when the config has none. PGlite takes only database.url: a folder to keep its data in, or none (or memory://) for an in-memory database. Only one process at a time can open a PGlite folder, so stop the server before a script or blendx migrate up opens it.

The same conformance suite passes on Bun with PGlite, Bun with pg, Bun with bun-sql, and Node 24 with pg.

In code, createDatabase(config) opens the configured database:

import { createDatabase } from 'blendx';
import config from './blendx.config.ts';

const database = await createDatabase(config);
database.db;            // the Drizzle database, for createServer and for scripts
await database.migrate('./drizzle'); // applies pending migrations; resolves to how many ran
await database.close();

Migrations in production

Two ways to apply migrations when you deploy:

  • At start-up, as the examples' server.ts does: await database.migrate(join(import.meta.dir, 'drizzle')) before serving. The production image then needs the drizzle/ folder, and not the CLI.
  • As a release step: bunx blendx migrate up, which needs @blendx/cli installed.

Either way, migrations are written during development (blendx migrate generate) and committed; production never generates them.

Serving on Bun

The examples' server.ts default-exports what Bun.serve takes:

export default {
  port: Number(process.env.PORT ?? 3000),
  fetch: createServer({ app, db: database.db, routes }).fetch,
};

bun server.ts serves it.

Serving on Node

Node 24 runs blendx's TypeScript directly (type stripping), with the pg, postgres-js or pglite driver. Add @hono/node-server (blendx is tested with 2.1.1) and serve the same server:

import { join } from 'node:path';
import { serve } from '@hono/node-server';
import { createDatabase, createServer } from 'blendx';
import config from './blendx.config.ts';
import app from './src/app.ts';
import { routes } from './src/generated/routes.gen.ts';

const database = await createDatabase(config);
await database.migrate(join(import.meta.dirname, 'drizzle'));

serve({
  fetch: createServer({ app, db: database.db, routes }).fetch,
  port: Number(process.env.PORT ?? 3000),
});

node server.node.ts serves it. Note import.meta.dirname, where Bun also has import.meta.dir. The blendx command itself still needs Bun, so development and CI run on Bun.

createServer

createServer(options) returns a Hono app:

Option
appthe app (src/app.ts)
dbdatabase.db
routesthe generated routes
basePathwhere the routes are mounted, such as '/api'; '/' by default
onErrorcalled with every unexpected error before the 500 is sent, and with what an after hook throws, whose reply stands (Hooks); console.error by default

Since it is a Hono app, you can add routes of your own to it, such as the OpenAPI document (The HTTP API). An unexpected error never reaches the client: it gets a bare 500 problem, and onError gets the error, which is where to log it or send it to an error tracker.

Running the outbox worker

An app with later hooks (Hooks) needs something to run its outbox entries. In a long-running server, start the worker beside it:

import { createDatabase, createServer, startOutbox } from 'blendx';
import { resources, routes } from './src/generated/routes.gen.ts';

// After the database and its migrations, as above:
startOutbox({ app, db: database.db, resources });

It drains the due entries every second (every, in seconds, changes that) until its stop() is called. Several servers can each run one: entries are claimed with FOR UPDATE SKIP LOCKED, so no two run the same entry at once. On a platform that stops the process between requests, call drainOutbox({ app, db: database.db, resources }) from a scheduled job instead: it runs every due entry once, and says how many ran, will run again, or failed. Both take onError, which gets what a later hook throws, and the retry settings: attempts (10), lease (300 seconds, how long a worker holds an entry it claimed) and retryDelay (2, 4, 8 and so on seconds after each failure, at most an hour). An app without later hooks can call either; they do nothing.

A failed entry stays in the blendx_outbox table with its failed_at and last_error, for you to look at. Clearing its failed_at gives it one more attempt.

Checklist

  • DATABASE_URL set, and the driver's package installed.
  • Migrations applied, at start-up or as a release step.
  • With later hooks: an outbox worker running, or drainOutbox on a schedule.
  • .data/ (PGlite) and .env files kept out of git and out of the image.
  • onError sends errors somewhere you will see them.
  • CI runs bunx blendx generate --check, bunx blendx review --check, bunx tsc --noEmit and bun test.