@midwess/worldant
v0.5.0
Published
**A self-hosted, durable _World_ for [Vercel Workflows](https://vercel.com/docs/workflow) — run your workflows anywhere, with zero-downtime redeploys and scale-to-zero, on nothing but embedded SQLite and your own Node.**
Readme
worldant
A self-hosted, durable World for Vercel Workflows — run your workflows anywhere, with zero-downtime redeploys and scale-to-zero, on nothing but embedded SQLite and your own Node.
worldant is a vercel-world: a drop-in implementation of the Workflow Development Kit's World interface (@workflow/world). You write ordinary Vercel Workflow code — "use workflow", "use step" — and worldant is the durable backend it runs on. No Vercel cloud. No Postgres, Redis, Temporal, Kafka, or Kubernetes. One npm install and a single supervisor binary.
Three ways to call a workflow
A durable workflow is only useful if something can reach it. worldant's job is to turn one workflow into every calling surface you need — the workflow underneath never changes, only how it is reached:
| Surface | What it means | Status | |---|---|---| | API | Your app's HTTP routes (NestJS / Express / Next) trigger workflows. The supervisor owns the public port and forwards every byte to your app. | Shipped | | Remote function | A deployed world serves itself as a live npm registry, so any consumer installs a generated, typed RPC client and calls your workflows like local functions (same-origin / cross-origin). | Shipped | | MCP | The same workflows surfaced as Model Context Protocol tools, so an agent can invoke them. | Roadmap |
Durable, with no complicated stack
The @midwess/worldant library is the durable World. Every workflow run, every step, every event, hook, and stream is persisted to an embedded SQLite database (bundled, WAL) — running in your app's own process, with no separate database to stand up.
- Exactly-once across restarts. Kill the process mid-workflow; a fresh process reopens the same data dir, finds the in-flight run, and resumes it from the log. A step that already committed
completedis memoized — its side effects never repeat. - Vercel Workflows, unchanged. worldant implements
@workflow/worldand registers itself with@workflow/core. Your"use step"/"use workflow"code, and packages like@workflow/nest, run against it as-is. - In-process queue. Queued jobs dispatch back into your app through a native callback, by queue-name prefix — no broker.
// at your app's entry, before it starts serving
import { install } from '@midwess/worldant';
await install(); // durable World on embedded SQLite, wired into @workflow/coreThat is the entire integration. The library is inert when run bare — it only relocates its socket and reports state when launched under the supervisor.
Zero-downtime redeploy
Re-run a live app and worldant hot-swaps it with no dropped connections — no flags, no orchestrator:
worldant run -- npm start # v1 is live
# ...edit code, then just run it again:
worldant run -- npm start # v2 swaps in, zero downtimeThe second run is recognized as the same app (by its code-first id), so instead of refusing as a duplicate, the daemon drains the old version, holds the public port open, closes the old process, starts the new one, and pushes the held connections to it — then returns success to you. Connections backlog during the swap window; none are refused. Proven on a live WebSocket: zero refused connections across a redeploy.
Scale to zero
An idle app drops to zero processes — only the thin supervisor and the SQLite file stay resident. The supervisor keeps the public port bound (connections backlog rather than refuse) and rebuilds the app on the first inbound request, or when a durable timer comes due. One cold start, nothing lost; the run resumes from the log and no step runs twice.
Quickstart
# 1. the durable World, in your app
npm install @midwess/worldant
# 2. the supervisor CLI (prebuilt native binary, no compiler)
npm install -g @midwess/worldant-cli
# 3. run your workflow app under it
worldant run -- npm startworldant ps # list hosted apps (id, pid, ports, state, uptime)
worldant logs <id> -f # tail an app's output
worldant stop <id> # stop one app
worldant install # run the daemon as a per-user OS service (boot + crash restart)See cli/README.md for the full supervisor surface (multi-app daemon, every-port forwarding, install-as-service) and examples/nestjs-checkout-saga for a complete NestJS checkout-saga app.
The two packages
| Package | What it is |
|---|---|
| @midwess/worldant | The durable World — a native (napi) addon over bundled SQLite that implements @workflow/world. You install() it in your app. |
| @midwess/worldant-cli | The worldant supervisor — a prebuilt native binary that owns your app's public port, forwards every byte, scales it to zero, and redeploys it with zero downtime. Also on crates.io as worldant-cli. |
The supervisor never opens the World or the database — durability lives entirely in the library, so the same binary supervises any app that speaks the protocol.
How it fits together
The library and the supervisor are one fate-shared unit, declared code-first: the app announces its own id and supervision config (grace, restart policy) via createWorld({ id?, config? }) — there are no CLI config flags. The supervisor passes the app a control pipe at boot; the library detects it, hijacks http.listen onto a private unix socket, and hands the supervisor the public port. If the supervisor dies, the app exits — no orphan holds the database.
Supported today: Node apps on macOS (arm64/x64) and Linux (x64, glibc). Deno and Windows are planned.
Calling a world's workflows (client integration)
A deployed world serves itself as a live npm registry, so any consumer can install a typed client for it:
npm install <world-id> --registry https://<world-host>/__worldant/v1/npmimport { checkout, chat } from '<world-id>';
await checkout(order); // value, typed
for await (const token of chat(prompt)) { } // stream, typedThree pieces:
worldant-client— the standalone, browser-safe transport (createClient→call/stream). The generated package depends on it; install it once, reuse it for every world.- The generated schema package (
<world-id>) — types +worldName+workflows+ a non-secretdefaultConfiguration({ worldId, baseUrl, prefix, pollMs }derived from the URL you installed from) + thin typed wrappers. worldant-react—useWorldant(schema)for Next.js frontends (same-origin default in the browser).
A value call() is a single request: it sends POST /runs?wait=<seconds> (default 120) and the server blocks on a completion event — no client-side polling. If the run is still durable-suspended (sleep, a hook) when the wait elapses, the server returns 202 { runId } and the client falls back to polling GET /runs/:id; the run keeps executing server-side regardless. Both endpoints accept ?wait (clamped to 300 s); without it, POST /runs returns { runId } immediately and GET /runs/:id returns a snapshot.
Next.js: withWorldant
Wrap your config — no gen-contract step, no worldant.catalog.json to manage:
// next.config.ts
import { withWorkflow } from 'workflow/next';
import { withWorldant } from '@midwess/worldant/next';
export default withWorkflow(withWorldant({ output: 'standalone' }, { client: true }));// instrumentation.ts
import { createWorld } from '@midwess/worldant';
export async function register() {
await createWorld({ id: 'orders', net: true }); // catalog auto-injected by withWorldant
}withWorldant discovers your 'use workflow' files at build, extracts the contract, and injects it so createWorld auto-loads it. With { client: true } it also generates a typed worldant/client.ts your frontend imports (this adds a worldant-client dependency):
// app/checkout-button.tsx
'use client';
import { placeOrder, trackOrder } from '../worldant/client';
await placeOrder(order); // typed, no key strings
for await (const s of trackOrder(order.id)) {} // SSEdefaultConfiguration never contains a secret — supply auth at runtime via configure({ token }).
For a cross-origin browser consumer, enable CORS on the world (net: { cors: ['https://app.example'] }) — the registry then sets the Access-Control-* headers and answers OPTIONS preflights.
Runnable examples
examples/nextjs-remote-functions— a Next.js orders app calling its own world same-origin (a rewrite), typed viaimport typewith no codegen. Shows a oneshot (placeOrder) and an SSE stream (trackOrder).examples/nestjs-vite-cross-origin— a NestJS "FX desk" world + a separate Vite/React app that installs the typed client cross-origin via npm and calls it (CORS). Shows a oneshot (quote) and an SSE stream (watchRate).
Apache-2.0
