Skip to content

Durable async flows for Node.js and Next.js (server runtime) built on BullMQ + Redis

License

Notifications You must be signed in to change notification settings

Fulwing/Durableflow

Repository files navigation

DurableFlow (durableflow)

DurableFlow is a server-only (Node.js runtime) durable async "flow" runtime built on BullMQ + Redis.

It works for:

  • Plain Node.js apps (Express/Fastify/etc.)
  • Next.js (App Router route handlers + server actions) in the Node.js runtime

Install

npm install durableflow bullmq ioredis

Redis key design (v0.1)

  • Run state: durableflow:run:<runId>
  • Step result idempotency: durableflow:run:<runId>:step:<stepName>
  • Signals: durableflow:signal:<runId>:<signalName>

You can override the durableflow prefix via createRuntime({ keyPrefix: "..." }).

Quickstart (Node.js)

Define a flow:

import { defineFlow, step, sleep, waitForSignal } from "durableflow/server";

defineFlow<{ name: string }, void>("hello", async (input) => {
  const msg = await step("build_message", async () => `hello ${input.name}`);
  await step("log", async () => console.log(msg));
  await sleep(1000);
  const sig = await waitForSignal<{ ok: boolean }>("approved", { timeoutMs: 10_000 });
  await step("after_signal", async () => console.log("approved:", sig.ok));
});

Start a worker process:

import { createRuntime } from "durableflow/server";

const runtime = createRuntime({ redis: process.env.REDIS_URL ?? "redis://localhost:6379" });
await runtime.startWorker();

Start a run from anywhere on the server:

const runId = await runtime.client.start("hello", { name: "world" });

Send a signal (e.g. from a webhook handler):

await runtime.client.signal(runId, "approved", { ok: true });

Next.js guide (App Router)

DurableFlow is NOT for the browser or Edge runtime.

  • Workers must run as a separate Node.js process/service (do not start BullMQ workers inside Next.js request handlers).
  • Ensure route handlers run in the Node.js runtime (not Edge). If needed, set:
export const runtime = "nodejs";

app/api/start/route.ts

export const runtime = "nodejs";

import { createRuntime } from "durableflow/server";

const durableflow = createRuntime({ redis: process.env.REDIS_URL! });

export async function POST(req: Request) {
  const input = await req.json();
  const runId = await durableflow.client.start("hello", input);
  return Response.json({ runId });
}

app/api/signal/[runId]/[name]/route.ts

export const runtime = "nodejs";

import { createRuntime } from "durableflow/server";

const durableflow = createRuntime({ redis: process.env.REDIS_URL! });

export async function POST(
  req: Request,
  { params }: { params: { runId: string; name: string } }
) {
  const payload = await req.json();
  await durableflow.client.signal(params.runId, params.name, payload);
  return Response.json({ ok: true });
}

Public API (v0.1)

  • defineFlow(name, handler)
  • createRuntime(config) => { startWorker(), stopWorker(), client }
  • client.start(flowName, input, options?) => runId
  • client.signal(runId, signalName, payload)
  • client.getRun(runId) => RunState
  • client.cancel(runId)
  • step(name, fn, opts?)
  • sleep(ms)
  • waitForSignal(name, opts?) => payload

Development

npm install
npm test
npm run build

Running the examples

Start Redis:

docker run --rm -p 6379:6379 --name durableflow-redis redis:7-alpine

Run the basic example:

npx tsx examples/node-basic.ts

Run the HTTP server example:

npx tsx examples/server.ts

Then in another terminal:

curl -s -X POST http://localhost:8787/start -H "content-type: application/json" -d '{"userId":"u1"}'

Take the returned runId, then:

curl -s -X POST "http://localhost:8787/signal/<runId>/webhook" -H "content-type: application/json" -d '{"message":"hello"}'

Notes / Limitations (MVP)

  • v0.1 supports linear flows only.
  • Step results and signal payloads must be JSON-serializable.
  • No filesystem access at runtime.

About

Durable async flows for Node.js and Next.js (server runtime) built on BullMQ + Redis

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published