SQL Database Adapters

Purpose and Scope

Use the SQL adapters when a Node-target Flue application needs durable, shared runtime state in an operational database instead of process-local memory or a single-host SQLite file. Flue persists its own harness state: canonical agent conversation streams, immutable external attachments, accepted direct prompts and dispatch(...) submissions, workflow-run records, event streams, and run indexes. The SQL adapters do not become your application database abstraction; customer records, tickets, payments, credentials, and external API side effects remain application-owned.

Sources: apps/docs/src/content/docs/ecosystem/databases/postgres.md, apps/docs/src/content/docs/ecosystem/databases/mysql.md, packages/postgres/README.md, packages/mysql/README.md

This page covers the relational adapters represented by @flue/postgres and @flue/mysql. Both are Node.js persistence adapters discovered through a source-root db.ts file and wired into the generated Node server at build time. On startup, each adapter runs its migrate() hook to create or verify the Flue-owned tables idempotently before the runtime uses them. Cloudflare deployments do not use these adapters because generated Durable Objects use SQLite automatically and reject db.ts for that target.

Sources: packages/postgres/README.md, packages/mysql/README.md, apps/docs/src/content/docs/ecosystem/databases/postgres.md, apps/docs/src/content/docs/ecosystem/databases/mysql.md

Relevant Source Files

  • apps/docs/src/content/docs/ecosystem/databases/postgres.md — first-party Postgres ecosystem guide, including the flue add database postgres flow, DATABASE_URL, Node-only target notes, and a pg-based db.ts example.
  • apps/docs/src/content/docs/ecosystem/databases/mysql.md — first-party MySQL ecosystem guide, including the flue add database mysql flow, MYSQL_URL, MySQL 8/InnoDB requirements, and a mysql2 pool example.
  • packages/postgres/README.md — package-level public documentation for @flue/postgres, including the runner contract, persisted state list, target support, and installation command.
  • packages/postgres/src/postgres-adapter.ts — implementation entrypoint that exports PostgresParameter, PostgresQuery, PostgresRunner, and the postgres(runner) factory returning a PersistenceAdapter.
  • packages/mysql/README.md — package-level public documentation for @flue/mysql, including the runner contract, durable-state boundaries, InnoDB requirement, and transaction guidance.
  • packages/mysql/src/mysql-adapter.ts — implementation entrypoint that exports MysqlParameter, MysqlQuery, MysqlRunner, and the mysql(runner) factory returning a PersistenceAdapter.

Quickstart and Configuration

Add the adapter with the matching blueprint for your database. The Postgres guide uses flue add database postgres, which installs @flue/postgres and either reuses your existing driver or adds pg and @types/pg by default. The MySQL guide uses flue add database mysql, which installs @flue/mysql and mysql2. In both cases, the blueprint writes a source-root db.ts file that default-exports the adapter so Flue can discover it during the Node build.

flue add database postgres
# or
flue add database mysql

Sources: apps/docs/src/content/docs/ecosystem/databases/postgres.md, apps/docs/src/content/docs/ecosystem/databases/mysql.md

The important configuration distinction is that connection strings are read by the driver at runtime, not baked into the generated server. Postgres examples use DATABASE_URL, such as postgresql://user:pass@host:5432/db. MySQL examples use MYSQL_URL, supplied by the database provider. For local development, the ecosystem guides point to flue dev --env <file> and flue run --env <file> for loading .env-format files. In production, keep these values in the platform secret store and configure driver TLS when your provider requires it.

AdapterPackageBlueprintRuntime variableTargetDatabase requirement
Postgres@flue/postgresflue add database postgresDATABASE_URLNode.jsPostgres-compatible driver and SQL with $N placeholders
MySQL@flue/mysqlflue add database mysqlMYSQL_URLNode.jsMySQL 8 with InnoDB

Runner Contract

Neither SQL adapter bundles or chooses your production database driver. Instead, each accepts a small runner object that wraps the driver your application already configures. This is the seam where you own pooling, TLS, credentials, provider-specific connection options, and lifecycle management. The adapter only needs a query function for ordinary SQL, a transaction function that runs callback work on one connection, and a close function that shuts down the underlying driver or pool.

Sources: packages/postgres/README.md, packages/postgres/src/postgres-adapter.ts, packages/mysql/README.md, packages/mysql/src/mysql-adapter.ts

Postgres and MySQL differ mainly in placeholder style and driver behavior. PostgresQuery accepts SQL with numbered $1, $2, and later placeholders plus positional parameters, returning rows as plain objects. MysqlQuery accepts SQL with ? placeholders and also returns plain-object rows; with mysql2, non-row results should map to an empty array. In both adapters, transaction(fn) must commit when fn resolves, roll back when it throws, and pass a transaction-scoped query function to the callback. The adapter never needs nested transactions.

Public nameSource packageShape or behavior
postgres(runner)@flue/postgresCreates a Postgres-backed PersistenceAdapter from PostgresRunner.
PostgresRunner@flue/postgres{ query, transaction, close }, using $N placeholders.
PostgresQuery@flue/postgres(text: string, params?: PostgresParameter[]) => Promise<Record<string, unknown>[]>.
mysql(runner)@flue/mysqlCreates a MySQL-backed PersistenceAdapter from MysqlRunner.
MysqlRunner@flue/mysql{ query, transaction, close }, using ? placeholders.
MysqlQuery@flue/mysql(text: string, params?: MysqlParameter[]) => Promise<Record<string, unknown>[]>.

Adapter Examples

A typical Postgres db.ts wraps a driver pool or client and delegates all SQL execution through the runner. The package README shows the postgres driver using db.unsafe(text, params) and db.begin(...); the ecosystem guide shows pg using pool.query(...) for ordinary queries and a checked-out client with explicit BEGIN, COMMIT, and ROLLBACK for transactions. The crucial rule is that callback queries must run on the same connection for the whole transaction, not an arbitrary pool connection.

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(),
});

Sources: apps/docs/src/content/docs/ecosystem/databases/postgres.md, packages/postgres/README.md

The MySQL example follows the same lifecycle with mysql2/promise. Ordinary queries use pool.execute(text, params), while transactions first call pool.getConnection(), then beginTransaction(), then execute every callback query through that checked-out connection. The package README explicitly warns not to issue callback queries through the pool, because a pool can choose a different connection and move the query outside the transaction. InnoDB is required because Flue relies on transactions and row locking for durable admission, claims, leases, and ordered event handling.

import { mysql, type MysqlQuery } from '@flue/mysql';
import mysql2 from 'mysql2/promise';
 
const pool = mysql2.createPool(process.env.MYSQL_URL!);
const toRows = (result: unknown): Record<string, unknown>[] =>
  Array.isArray(result) ? result.map((row) => ({ ...row })) : [];
 
export default mysql({
  query: async (text, params = []) => {
    const [result] = await pool.execute(text, params);
    return toRows(result);
  },
  transaction: async <T>(fn: (tx: { query: MysqlQuery }) => Promise<T>) => {
    const connection = await pool.getConnection();
    try {
      await connection.beginTransaction();
      const result = await fn({
        query: async (text, params = []) => {
          const [rows] = await connection.execute(text, params);
          return toRows(rows);
        },
      });
      await connection.commit();
      return result;
    } catch (error) {
      await connection.rollback();
      throw error;
    } finally {
      connection.release();
    }
  },
  close: () => pool.end(),
});

Sources: apps/docs/src/content/docs/ecosystem/databases/mysql.md, packages/mysql/README.md

Runtime State and Schema Behavior

At runtime, both factories return a PersistenceAdapter with migrate, connect, and close behavior. The adapter connect() result wires Flue stores for submissions, workflow runs, event streams, conversation streams, and attachments. The Postgres implementation imports store builders such as the attachment store and conversation stream store, while the MySQL implementation constructs analogous MysqlSubmissionStore, MysqlRunStore, MysqlEventStreamStore, conversation stream store, and attachment store. The shared contract is more important than the implementation names: Flue expects one adapter to provide every durable store needed by the harness.

Sources: packages/postgres/src/postgres-adapter.ts, packages/mysql/src/mysql-adapter.ts

The MySQL implementation snippet exposes the table inventory it verifies, including flue_meta, flue_image_chunks, flue_agent_session_locks, flue_agent_submissions, flue_agent_dispatch_receipts, flue_agent_attempt_markers, flue_runs, flue_event_streams, flue_event_stream_entries, flue_conversation_streams, flue_conversation_stream_batches, and flue_attachments. These names show how Flue separates admission and lease bookkeeping, workflow history, event streams, conversation batches, and attachment bytes. The Postgres adapter follows the same persistence-adapter responsibilities and validates the Flue schema version through runtime adapter helpers.

Operational Guidance

Choose Postgres when you need state to survive host replacement or when several Node replicas must share workflow history and accepted work recovery. Choose MySQL when your deployment already standardizes on MySQL 8 and InnoDB, or when operations, backups, and access controls are built around that environment. Both choices allow replicas to share durable state, but they do not make a single agent instance active-active; each agent instance still needs one live Node owner at a time.

Sources: apps/docs/src/content/docs/ecosystem/databases/postgres.md, apps/docs/src/content/docs/ecosystem/databases/mysql.md, packages/postgres/README.md, packages/mysql/README.md

For smaller deployments, the built-in file-backed sqlite() adapter from @flue/runtime/node may be simpler because it avoids an external database. The SQL adapters become valuable when process replacement, multiple replicas, or existing database operations matter more than simplicity. After adding one, verify that db.ts is at the source root, the correct environment variable is present in local and production environments, the driver can create or verify tables on startup, and your transaction wrapper never leaks callback work outside the checked-out connection.