Runtime Adapter Authoring
Build Runtime Engine store, wake, or full host adapters with the shared contracts and conformance tests.
Most runtime adapters should implement one composable piece rather than a whole engine. Keep correctness in the core kernel; adapters persist rows, deliver wake messages, verify setup, and expose honest capabilities.
Adapter shapes
| Shape | Implement when |
|---|---|
| Store adapter | You provide durable state/events/waiters/timers/outbox/leases, such as Postgres or DynamoDB. |
| Wake adapter | You provide at-least-once delivery or HTTP wake, such as QStash or a queue. |
| Host-bound runtime | Store and wake require host context, such as Convex. |
Conformance tests
Use @use-crux/core/runtime/testing for adapter CI:
import { runStoreAdapterTests } from '@use-crux/core/runtime/testing'
runStoreAdapterTests({
name: 'acme-store',
createStore: async () => acmeStore({ namespace: 'test' }),
})Full runtime adapters can use runRuntimeEngineAdapterTests() to verify wake, retry, leases, stale-lease fencing, timers, waiter races, and idempotency through the public kernel.
Capability rules
Capabilities are diagnostic-facing contracts. Report only what the adapter really supports:
wake.signedonly when incoming wake verification is enforced.timers.maxDelayMsonly when native delayed delivery is bounded.setup.canCheckandsetup.canApplyonly when those operations exist and follow the setup contract.- Edge/serverless/multi-process support must be explicit.
Missing capability should fail early with CAPABILITY_MISSING; it must not silently downgrade durability.
Wake verification
HTTP wake handlers fail closed in production when no verifier is configured. Store-backed serverless adapters should provide a wake verifier, and hand-written custom handlers should pass one explicitly:
import { createRuntimeHandler, hmacWakeVerifier } from '@use-crux/core/runtime'
export const { GET, POST } = createRuntimeHandler({
targets: [reviewFlow, embedDocument],
verify: hmacWakeVerifier({ secret: process.env.CRUX_RUNTIME_WAKE_SECRET! }),
})Use verify: allowUnsignedDevWake only for trusted local endpoints.
Setup rules
setup.check() never mutates resources. setup.apply() may perform safe additive setup only. Do not create paid services or perform destructive migrations automatically.
Store snapshot rules
When state.markSnapshotDelivered() is called, persist the delivered suspend with both the durable eventId and the copied JSON payload. Runtime-backed flow replay reads that snapshot payload directly and must not need to scan the event log after delivery.
Outbox rules
outbox.listByWork(workId, { namespace?, state?, limit? }) is part of the stable store contract. The kernel uses it to check whether a specific work item still has pending wake delivery before requeueing orphaned pending work, so implementations should avoid namespace-wide scans.
Outbox dispatch is bounded and defaults to eight in-flight deliveries per pass. Adapters must not rely on delivery order across separate outbox rows for correctness.
Store retention rules
Store ports expose bounded prune methods for their own record classes:
events.prune()deletes events appended before the cutoff.state.pruneTerminalWork()deletes completed, cancelled, and dead-lettered work.state.pruneTerminalSnapshots()deletes completed, blocked, and cancelled snapshots.state.pruneIdempotencyKeys()deletes completed idempotency markers.timers.prune(),waiters.prune(), andoutbox.prune()delete only settled/confirmed records.
Every prune method must respect limit and return { removed, truncated }. Adapters may keep settled timestamps internally; Postgres-style adapters should add nullable settled/confirmed columns and treat older already-settled rows with null stamps as eligible.
Lease rules
leases.extend() must preserve the token returned by leases.claim(). The kernel records that token on leased work and checks it inside final commit transactions; a stale token becomes LEASE_LOST instead of a retry or dead letter.
Composite commit rules
Kernel operations that need multiple writes use named composites such as task.enqueue, event.emit, wake.complete, and maintenance.reclaim-lease.
Most store adapters only need transact(fn). Core's default composite runner executes the kernel-owned body inside that transaction, so retry policy, waiter races, idempotency, lease fencing, and idle accounting stay in core.
Implement runComposite(kind, input) only when your platform has a stronger native atomic boundary than a direct function callback. Convex uses this path to run each composite inside one component mutation while importing the same core body registry. Custom runComposite implementations must preserve the exact named composite inputs/results and should be covered by runStoreAdapterTests().
Related
- Reference: Runtime Engine
- Guide: Building Custom Adapters
- Recipe: Generic Queue and BullMQ