Skip to content

App Backends Pack

The App Backends Pack (@hellmai/lumenflow-packs-app-backends) defines what an application backend is, independent of where its database runs, and gives provider authors the means to prove their adapter honours that definition.

  • The contract (/contract): zod schemas and types for backends, operations, migrations, restore points, exports and the capability document an adapter declares. Enums are closed; provider-specific values carry an x- prefix. See Contract.
  • The adapter SPI (/spi): the AppBackendAdapter interface with 25 operations, the operation context, the error taxonomy with retryable and outcome flags, and deadlines. See Adapter SPI.
  • Idempotency (/idempotency): RFC 8785 request hashes, tenant-scoped operation records and a shared operation pipeline that gives an adapter the idempotency, deadline, capability and ownership rules without re-deriving them.
  • A conformance kit (/conformance): a runner, a catalogue of 32 cases that include negative and adversarial ones, and the harness interfaces an adapter author implements. A bad adapter fails. See Conformance kit.
  • An in-memory reference adapter (/testing): a complete adapter for tests and examples, not a production backend.
  • A bring-your-own Postgres adapter (/adapters/postgres-url): runs backends as separate databases on a Postgres server you operate. See Postgres adapter.
  • A standalone export verifier and importer (/export-format, and the app-backends-export command): checks a neutral-jsonl-v1 export and loads it into any plain Postgres, with no adapter, control plane or LumenFlow configuration. See Export format.

The data plane that serves a backend to an application (the gateway, the data API and the auth processes) is separate and is not in this pack. So is the custody service that holds credentials in production. The pack defines the sealed references that cross that boundary and the two-method sealing port the Postgres adapter needs; it does not implement production custody.

An adapter that passes the conformance kit is a statement about how it behaves at the SPI boundary. It is not a statement about the data plane in front of it.

pnpm add @hellmai/lumenflow-packs-app-backends
# only when you use the Postgres adapter:
pnpm add pg

Requires Node.js 26 or later. pg is an optional peer dependency (version 8.11 or later); every subpath except /adapters/postgres-url/pg imports without it, and the Postgres adapter loads it on first use.

Subpaths: . (contract, SPI and idempotency), /contract, /spi, /idempotency, /conformance, /testing, /adapters/postgres-url, /adapters/postgres-url/pg and /adapters/postgres-url/conformance.

Run the conformance suite against the in-memory reference adapter. The adapter and its harness come from createMemoryAdapter(), and runConformance takes them together.

import {
  assertConformant,
  formatReport,
  runConformance,
} from '@hellmai/lumenflow-packs-app-backends/conformance';
import { createMemoryAdapter } from '@hellmai/lumenflow-packs-app-backends/testing';

const subject = createMemoryAdapter(); // { adapter, harness }
const report = await runConformance(subject);

console.log(formatReport(report)); // one PASS / FAIL / N/A / HARN line per case
assertConformant(report); // throws, with the full report, unless the adapter is conformant

Call the adapter directly with a context built by buildOpContext, which computes the request hash for you:

import { buildOpContext } from '@hellmai/lumenflow-packs-app-backends/spi';
import { sampleProvisionRequest } from '@hellmai/lumenflow-packs-app-backends/testing';

const { adapter } = subject;
const isolation = { workspaceId: 'ws_1', tenantSubject: 'tenant_1', appId: 'app_1' };
const req = sampleProvisionRequest();

const created = await adapter.provision(
  req,
  buildOpContext({ operation: 'provision', req }, isolation),
);
if (!created.ok) throw new Error(`${created.error.code}: ${created.error.detail}`);

const handle = created.value;
const status = await adapter.status(
  handle,
  buildOpContext({ operation: 'status', handle }, isolation),
);
// status.ok && status.value.state === 'ready'

// Another tenant's context cannot see it:
const other = { ...isolation, tenantSubject: 'tenant_2' };
await adapter.status(handle, buildOpContext({ operation: 'status', handle }, other));
// { ok: false, error: { code: 'not_found', retryable: false, outcome: 'none', ... } }

To see the kit reject a bad adapter, build the reference adapter with an injected defect:

const bad = createMemoryAdapter({ defects: ['idempotency-ignores-tenant'] });
const badReport = await runConformance(bad);
badReport.conformant; // false
badReport.cases.filter((c) => c.status === 'failed').map((c) => c.id); // ['c1b.tenant-scoped-keys']

MEMORY_DEFECTS lists every defect the reference adapter can be built with, and MEMORY_DEFECT_EXPECTED_FAILURES maps each one to the case ids that must fail.

See also the pack catalog and Pack Contract v2.