ZenZip logozenzip

Quickstart

A durable queue, a cron schedule, and a crash-proof workflow in one file — backed by a single SQLite database.

Install#

Scaffold a new project (recommended):

$npm create zenzipjs-app@latest my-app

Or add ZenZip to an existing project:

$npm install zenzipjs

Package name

ZenZip publishes to npm as zenzipjs — the zenzip name is reserved on the registry. The import and public API are still zenzip: import { zenzip } from "zenzipjs". Prebuilt native binaries ship for Windows, macOS (x64 + arm64), and Linux (glibc + musl, x64 + arm64), so npm install needs no Rust toolchain.

There is nothing else to install. No Redis, no Docker compose, no broker. The runtime stores everything in .zenzip/zenzip.db — a SQLite database in WAL mode, created on first start.

Your first app#

One file demonstrating all three primitives shipped so far:

app.ts
import { zenzip } from "zenzipjs";

const app = zenzip({ dataDir: ".zenzip" });

// 1. A durable queue — retries with backoff, dead-letters when exhausted.
const emails = app.queue<{ to: string; subject: string }>("emails", {
  concurrency: 5,
  retries: 3,
  backoff: { delay: "500ms", maxDelay: "5s" },
});

emails.process(async (job) => {
  console.log(`sending to ${job.data.to} (attempt ${job.attempt})`);
  if (Math.random() < 0.25) throw new Error("smtp: connection reset");
});

// 2. A persisted schedule — survives restarts, timezone-aware.
app.schedule("digest", { every: "10s" }, async () => {
  await emails.push({ to: "digest@example.com", subject: "Your digest" });
});

// 3. A durable workflow — every step is journaled.
const onboard = app.workflow<{ email: string }, string>(
  "onboard",
  async ({ step, input }) => {
    await step.run("welcome", () =>
      emails.push({ to: input.email, subject: "Welcome!" }),
    );
    await step.sleep("wait", "5s"); // survives kill -9
    await step.run("follow-up", () =>
      emails.push({ to: input.email, subject: "How's it going?" }),
    );
    return "onboarded";
  },
);

await app.start();

await emails.push({ to: "you@example.com", subject: "Hello from ZenZip" });
await onboard.trigger({ email: "you@example.com" });

Three things worth noticing:

  • Definition before start. Queues, schedules, and workflows are declared first; app.start() boots the Rust runtime with everything registered. Pushing or triggering before start throws a clear error.
  • Throw = retry. The flaky email handler above will be retried with exponential backoff up to its retry budget, then dead-lettered — inspect with emails.deadJobs().
  • Typed payloads. app.queue<T> and app.workflow<I, O> flow types end to end. Add a zod schema via the schema option for runtime validation on push.

Run it#

$npx tsx app.ts

You'll see the five welcome emails process (some retrying), the digest schedule firing every 10 seconds, and the onboarding workflow sending its follow-up 5 seconds after the welcome.

// Graceful shutdown: stops claiming, drains in-flight handlers.
// SIGINT/SIGTERM handlers are installed automatically (opt out with
// handleSignals: false).
const clean = await app.stop({ timeout: "10s" });

Now kill it#

While it runs, kill the process — not Ctrl-C, actually kill it (taskkill /F on Windows, kill -9 elsewhere). Then start it again:

  • In-flight queue jobs are redelivered after their lease expires, with the failed attempt counted against the retry budget.
  • A workflow mid-sleep resumes at the right time; its completed steps fast-forward from the journal and never re-execute.
  • The schedule's next fire time was persisted — missed ticks follow your catchup policy.

This is tested, not aspirational

The repository's CI includes harnesses that SIGKILL worker processes mid-job and mid-workflow (four times in a row, at random points) and assert correct recovery. See Durability & Semantics.

Next steps#