FabricFabricHarness
EcosystemDatabases

Postgres

Persist Fabric session state and artifacts in Postgres.

pnpm add @fabric-harness/node pg

Configure the unified persistence bundle without placing the connection string in source:

.fabricharness/config.ts
import { postgresPersistence, type FabricHarnessConfig } from '@fabric-harness/node';
import { Pool } from 'pg';

export default function config(): FabricHarnessConfig {
  const pool = new Pool({ connectionString: process.env.DATABASE_URL });
  return {
    persistence: postgresPersistence({
      client: pool,
      tablePrefix: 'fabric_harness',
    }),
  };
}

postgresPersistence() migrates and wires session history, submissions, conversation offsets, attachments, finite run/event records, and cost-budget totals. fh dev and generated Node servers consume persistence.serverOptions() automatically, health checks can call persistence.health(), and server shutdown closes the injected pool. Use a distinct tablePrefix per environment or tenant boundary where operational separation is required.

The lower-level PostgresSessionStore, PostgresSubmissionStore, PostgresConversationStreamStore, and PostgresAttachmentStore remain public for hosts that need to compose stores independently.

For a durable single-node deployment, use the same surface with SQLite:

.fabricharness/config.ts
import { sqlitePersistence } from '@fabric-harness/node';

export default {
  persistence: sqlitePersistence({
    path: '.fabricharness/fabric-harness.sqlite',
  }),
};

The SQLite bundle includes sessions, submissions, conversation offsets, attachments, workflow run events, and atomic cost-budget totals. Use Postgres or Lakebase when multiple server replicas need to coordinate.

For governed model-facing data access, scaffold a fixed-statement tool:

fh add database postgres
import { postgresTool } from '@fabric-harness/databases/postgres';

const lookup = postgresTool({
  client: pool,
  name: 'lookup_account',
  description: 'Read one account in the current tenant.',
  statement: 'select id, status from accounts where tenant_id = $1 and id = $2 limit 1',
  parameters: ({ tenantId, accountId }) => [tenantId, accountId],
});

The model supplies parameter values, never SQL text. Read tools reject mutation statements and multiple statements; all tools cap rows, result bytes, and execution time and declare an effect for policy/approval enforcement. See examples/with-postgres-store and run pnpm run run:tool for a runnable example.

The same optional DATABASE_URL configuration is used by examples/react-chat and examples/with-channel-adapters, so the UI and channel examples can be run infrastructure-free or against durable Postgres without changing application code. The CI suite runs the published bundle contract against Postgres 16, including concurrent cost increments, lifecycle CAS behavior, cascade deletion, and close/reopen recovery.