Postgres-backed durable persistence for Flue applications on the Node.js target.
// src/db.ts
import { postgres, type PostgresQuery } from '@flue/postgres';
import sql from 'postgres';
const db = sql(process.env.DATABASE_URL!);
export default postgres({
query: (text, params) => db.unsafe(text, params),
transaction: <T>(fn: (tx: { query: PostgresQuery }) => Promise<T>) =>
db.begin((tx) => fn({ query: (text, params) => tx.unsafe(text, params) })) as Promise<T>,
close: () => db.end(),
});Default-export the adapter from a source-root db.ts. Flue discovers it at
build time and wires it into the generated Node server. The adapter's
migrate() hook runs once at startup and creates its tables idempotently, so
there is no separate migration step.
This adapter persists Flue runtime state:
- the canonical append-only conversation stream for each agent instance;
- immutable external attachments referenced by conversation records;
- accepted direct prompts and
dispatch(...)submissions, with durable claims and leases; - workflow-run records, event streams, and run indexing.
The canonical stream is the sole transcript and is replayed from its beginning; replay acceleration and persisted-log compaction are deferred. Sessions append for the instance lifetime and have no per-session deletion. Whole-instance stream and attachment deletion methods are low-level primitives, not public orchestration.
It does not store your application's business data. Keep customer records, tickets, and payments in your own tables.
@flue/postgres does not pick or bundle a database driver. It runs against
a small runner you wrap around your configured driver, so you own driver
choice, pooling, TLS, and every other connection option.
A runner is three functions:
query(text, params)— run a SQL string with numbered$Nplaceholders and positional parameters, resolving to the result rows as plain objects.transaction(fn)— runfninside a single transaction on one connection, committing on resolve and rolling back on throw. Thetxpassed tofnonly needsquery.close()— close the underlying driver.
The example above wraps the postgres
(porsager) driver. With pg (node-postgres),
transaction checks out a single client and issues BEGIN/COMMIT/ROLLBACK
itself (a pool cannot run a transaction across arbitrary connections):
import { postgres } from '@flue/postgres';
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export default postgres({
query: async (text, params) => (await pool.query(text, params)).rows,
transaction: async (fn) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await fn({ query: async (t, p) => (await client.query(t, p)).rows });
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
},
close: () => pool.end(),
});Reach for @flue/postgres when state must survive host replacement or be
shared across multiple application replicas — for example, when another Node
process must recover accepted work after a host failure, or when several
replicas need the same workflow-run history. For a single host, the built-in
file-backed sqlite() adapter from @flue/runtime/node is enough.
This adapter targets Node.js. The Cloudflare target uses Durable Object
SQLite automatically and rejects a db.ts file at build time, so a database
adapter does not apply there.
flue add database postgresflue add database postgres installs the package, helps you pick a driver, and writes
the db.ts. See the Postgres guide
for setup and the Data Persistence API
for the adapter contract.