@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(),
}),
});| Option | Type | Default | Description |
|---|---|---|---|
url | string | DATABASE_URL | Postgres connection URL. |
schema | string | 'crux_runtime' | Dedicated schema for Crux-owned runtime tables and indexes. |
pool | Pool | created from url | Caller-supplied pg pool. |
poolOptions | PoolConfig | pg defaults | Options for the Crux-created pool. |
setup.mode | 'verify' | 'create-if-missing' | production verifies | Controls 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 workerper namespace for the self-hosted Node topology; PostgreSQL advisory-lock ownership rejects an overlapping worker. - Configure
poolOptions.maxor a caller-owned pool'smaxto at least2for 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.
Related
- Reference: Runtime Engine
- Guide: Next/Vercel Runtime
- Guide: Long-lived Node Runtime
- Error: SETUP_REQUIRED