Crux
CookbookRuntime Recipes

Named defer on serverless and Convex

Await durable acceptance with config-only ambient mode or a strict host boundary.

Named defer stages Runtime work and resolves after durable acceptance.

Config-only ambient mode

On an ambient-capable platform, configure Runtime and retention once:

crux.config.ts
import { config } from "@use-crux/core";
import { serverless } from "@use-crux/core/runtime";
import { vercel } from "@use-crux/vercel";

export default config({
  host: vercel(),
  runtime: serverless({ store, wake }),
});

Then await acceptance at the call site:

import { defer } from "@use-crux/core";
import { durableTask } from "@use-crux/core/runtime";

const postProcess = durableTask("post-process", {
  run: async (input: { jobId: string }) => input.jobId,
});

export async function POST(request: Request) {
  const { jobId } = await request.json();
  const ref = await defer(postProcess, { jobId });
  return Response.json({ accepted: true, workId: ref.workId });
}

The user's await is the acceptance barrier. Each ambient call owns an ephemeral root; it does not classify the enclosing handler or provide a strict response commit barrier.

Strict serverless boundary

Use withServerlessDefer when handler outcome and response commitment belong to one real invocation:

import { withServerlessDefer } from "@use-crux/core/defer/serverless";

export const handler = withServerlessDefer(appHandler, { binding });

Lambda-class hosts without a retention port should use withNamedOnlyDefer. Inline callbacks then fail explicitly with DEFER_CAPABILITY_MISSING; named Runtime work remains available:

export const handler = withNamedOnlyDefer(
  async (event: { jobId: string }) => {
    const ref = await defer(postProcess, { jobId: event.jobId });
    return { accepted: true, workId: ref.workId };
  },
  { host: "lambda", durableFinalization: true },
);

If staging or strict finalization fails, the wrapper reports DEFER_COMMIT_FAILED and must not commit a successful response.

Convex

Configure runtime: convex() and execute through createCruxConvex().run(). The bridge installs its named-only execution scope; await named work and keep targets JSON-safe and exported for wake.

On this page