Crux
API Reference

@use-crux/postgres

PostgreSQL JSON records, pgvector and lexical SearchStore retrieval, composed storage, and durable Runtime Engine storage.

Peer dependency: pg >=8.22.0

Connected Knowledge storage

import {
  postgresRecordStore,
  postgresSearchStore,
  postgresStorage,
} from "@use-crux/postgres";

All three factories accept url, pool, poolOptions, and schema. url defaults to DATABASE_URL; schema defaults to crux_storage. A caller-supplied Pool is never closed. When Crux creates the pool, call the adapter's close() method during application shutdown.

postgresRecordStore(options?)

The record adapter stores JSONB values with lazy expiry and a bigint CAS version. It supports ordered opaque-cursor listing, escaped literal prefixes, exact top-level scalar filters, batches, and linearizable versioned mutation.

const records = postgresRecordStore({ url: process.env.DATABASE_URL });
await records.setup.apply();
await records.put("docs:1", { title: "PostgreSQL" }, { ttlMs: 300_000 });

postgresSearchStore(options)

At least one of dimensions, sparseDimensions, or lexical is required. Set dimensions for a dense pgvector leg, sparseDimensions for a sparse pgvector leg, and lexical for PostgreSQL native full-text search. Crux sparse indices are zero-based; the adapter converts them to pgvector's one-based sparsevec representation.

const search = postgresSearchStore({
  dimensions: 1536,
  sparseDimensions: 30_000,
  lexical: { configuration: "simple" },
});
await search.setup.apply();

Dense search uses cosine HNSW indexes and exact JSONB metadata prefilters. HNSW is approximate. Lexical search stores chunk content, indexes a generated tsvector with GIN, parses queries with websearch_to_tsquery(), and ranks lexical candidates with PostgreSQL full-text ranking. Multi-leg search runs in one SQL statement with per-leg candidate CTEs and deterministic normalized RRF with stable key tie-breaking. Non-RRF fusion throws StorageError("unsupported_capability").

PostgreSQL lexical search is not BM25 in this release. True BM25 is deferred to RFC #352, and learned sparse providers are deferred to RFC #353. Sparse search remains a composable leg when sparseDimensions is configured.

setup.check() never mutates the database. It reports missing columns, generated tsvector expression/configuration mismatches, missing GIN indexes, pgvector requirements, dimensions, and the Crux-owned presence constraint. setup.apply() safely creates missing schemas, tables, nullable columns, extension requirements, constraints, and indexes. After enabling lexical search for an existing knowledge base, run knowledgeBase.reindex() so active chunks populate content; check() reports POSTGRES_SEARCH_LEXICAL_CONTENT_MISSING until reindexing catches up.

postgresStorage(options)

The composed factory shares one pool, one setup lifecycle, and one close boundary:

const storage = postgresStorage({
  dimensions: 1536,
  lexical: true,
});
const result = await storage.setup.check();
if (!result.ok) await storage.setup.apply();

Setup is explicit and idempotent. check() never mutates the database; apply() creates the vector extension when needed, the storage schema, tables, constraints, generated full-text columns, and indexes. Data operations never run DDL. Diagnostics never include connection strings, SQL values, or stored content.

When this bundle is assigned to config({ storage }), crux setup --check uses its setup capability and crux setup --apply runs the same safe-additive provisioning. The command releases pools created by the adapter afterward; caller-owned pools remain open.

Runtime Engine

import { postgres } from "@use-crux/postgres/runtime";

@use-crux/postgres/runtime provides the first-party durable store for the Runtime Engine. Use it with node({ store }) for long-lived Node processes or with serverless({ store, wake }) for HTTP-woken deployments.

postgres(options?)

import { config } from "@use-crux/core";
import { serverless } from "@use-crux/core/runtime";
import { postgres } from "@use-crux/postgres/runtime";
import { qstash } from "@use-crux/upstash/runtime";

export default config({
  runtime: serverless({
    store: postgres({
      url: process.env.DATABASE_URL,
      schema: "crux_runtime",
    }),
    wake: qstash(),
  }),
});
OptionTypeDefaultDescription
urlstringDATABASE_URLPostgres connection URL.
schemastring'crux_runtime'Dedicated schema for Crux-owned runtime tables and indexes.
poolPoolcreated from urlCaller-supplied pg pool.
poolOptionsPoolConfigpg defaultsOptions for the Crux-created pool.
setup.mode'verify' | 'create-if-missing'production verifiesControls whether setup may create missing Crux-owned resources.

The adapter stores work items, flow snapshots, durable events, waiters, timers, outbox rows, idempotency records, leases, scoped-idle counters, and durable Effect receipt/scope/unit/attempt/envelope rows. Runtime payloads are JSON data; application assets and large documents should stay in application storage.

Durable Effects on PostgreSQL support atomic multi-record operations, crash fencing, and exact reverse-plan reconstruction. Recovery still runs only when application code calls the Effects APIs with a matching Runtime program; an external worker that auto-drives Effect recovery after process kill is not included. See Durability and restarts.

Runtime setup

postgres() exposes a setup port used by crux setup:

crux setup --check
crux setup --apply

--check never mutates resources. --apply performs additive setup only: schema, tables, indexes, and advisory-lock migration state owned by Crux. Destructive migrations are not automatic.

In tests, the package conformance suite can run against CRUX_TEST_DATABASE_URL. Local package tests may start an embedded Postgres fallback when no URL is provided.

Deployment notes

  • Use a shared Postgres database for multi-process or serverless deployments.
  • Run exactly one crux runtime worker per namespace for the self-hosted Node topology; PostgreSQL advisory-lock ownership rejects an overlapping worker.
  • Configure poolOptions.max or a caller-owned pool's max to at least 2 for a Runtime worker. Its session-scoped ownership lease holds one connection while maintenance uses another.
  • Keep the runtime schema separate from application tables.
  • Pair Postgres with an external wake adapter such as qstash() in serverless deployments; a database row cannot wake a cold function by itself.
  • Namespace isolates environments sharing the same database. It is not an authorization boundary.

On this page