npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nanobpm/urban

v0.86.0

Published

Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.

Readme

@nanobpm/urban

Build and run code-first apps on Nano — the runtime, the derivation toolkit, and the urban CLI in one package, on Node or Deno.

An Urban app is a directory with a nano.app.json manifest that declares its processes, forms, datasources, workers, HTTP surfaces and triggers. This package brings it to life and gives you a library API to embed or extend it. Author your durable processes in code with @nanobpm/workflow (defineFlow) — re-exported from here for convenience.

Install

npm i -g @nanobpm/urban      # install the `urban` command
# or run without installing:
npx @nanobpm/urban new my-app
# or on Deno:
deno run -A npm:@nanobpm/urban run

Requires Node ≥ 22.6 or Deno. It ships as compiled JavaScript with .d.ts type declarations: Node can't strip types under node_modules, so the published package carries dist/ and needs no build step or --experimental-strip-types flag to run. Deno users can still import the TypeScript source directly via the ./source export.

The urban CLI

| Command | What it does | |---|---| | urban new <name> | scaffold a new app in a new directory | | urban check | validate the app's nano.app.json manifest | | urban gen | generate the nano-generated/ artifacts (migrations, worker I/O) | | urban gen --check | fail if the generated artifacts are out of date (a CI drift gate) | | urban run | generate, then run the app — starts its workers and serves its surfaces | | urban dev | run the app (hot-reload is not yet implemented) | | urban deploy | deploy the app's models to the engine, then exit |

Options

| Flag | Purpose | Default | |---|---|---| | --root <dir> | app directory | . | | --manifest <file> | manifest filename | nano.app.json | | --port <n> | HTTP port for surfaces and triggers (integer 0–65535) | $PORT or 8090 | | -h, --help | show help | | | -v, --version | print the version | |

The engine address comes from $CAMUNDA_REST_ADDRESS (default http://localhost:8080/v2). Transport comes from $CAMUNDA_TRANSPORT (default auto): the @nanobpm/nano-sdk client upgrades instance creation and job serving to Falcon on a Nano server and falls back to REST elsewhere. Set it to rest, falcon, or embedded to pin a specific transport.

Network bind interface

By default an app's HTTP server binds to loopback only (127.0.0.1) — secure by default, so surfaces, triggers and capability hooks are unreachable from other machines. To make the app reachable from other hosts on the LAN (e.g. a distributed worker fleet), opt in with the app-level network.bind manifest setting:

// nano.app.json
{
  "network": { "bind": "loopback" }  // default — 127.0.0.1, refuses off-box connections
  // "network": { "bind": "all" }    // 0.0.0.0 — reachable across the LAN
}

| network.bind | Bind address | Reachability | |---|---|---| | "loopback" (default) | 127.0.0.1 | this machine only | | "all" | 0.0.0.0 | every interface / the LAN |

Ops can override the manifest at deploy time with the URBAN_BIND environment variable (URBAN_BIND=all or URBAN_BIND=loopback); a valid value wins over the manifest, the manifest stays the declarative default. Binding to all interfaces emits a startup warn log because it exposes the app off-box.

Security: binding to all interfaces exposes the app's token-gated capability hooks on the LAN. Any LOCAL-mode "well-known localhost" credential (e.g. the agentic channel's LOCAL mode) must not be served on a non-loopback bind — a consumer that mints one must gate it on a loopback bind.

A typical session

urban new invoices && cd invoices
urban gen        # generate nano-generated/
urban check      # validate the manifest
urban run        # start workers and serve surfaces

Library API

Everything the CLI does is available programmatically. Import the whole surface from @nanobpm/urban, or the focused subpaths @nanobpm/urban/runtime, @nanobpm/urban/toolkit, and @nanobpm/urban/effect.

Runtime — run an app

import { runFromEnv } from "@nanobpm/urban";

const app = await runFromEnv();          // reads ./nano.app.json and the environment
console.log(app.inspect());              // { app, name, httpPort, ... }

runFromEnv reads the engine address and transport from the environment, starts the app (validate → deploy → provision datasources → start workers → serve surfaces and webhook + cron triggers), and installs SIGINT/SIGTERM handlers for a graceful shutdown. For full control, assemble the pieces yourself:

import { createUrbanApp, selectHost, createNanoSdkEngineClient } from "@nanobpm/urban";

const host = selectHost();                       // picks the Node or Deno adapter
const engine = await createNanoSdkEngineClient({
  restAddress: process.env.CAMUNDA_REST_ADDRESS!,
  transport: process.env.CAMUNDA_TRANSPORT,      // "auto" (default) | "rest" | "falcon" | "embedded"
});
const app = await createUrbanApp({ host, engine, root: "." });
await app.start();
// ... later:
await app.stop();                                // releases workers, server, datasources

The runtime has a single engine client, SdkEngineClient, backed by one @nanobpm/nano-sdk client (a direct dependency). createNanoSdkEngineClient selects the wire transport via CAMUNDA_TRANSPORT: auto (default) upgrades to Falcon on a Nano server and falls back to REST elsewhere.

Structured logging

Every worker handler and API route delegate receives an AppApi whose log is a level-tagged structured logger (ADR 0061):

app.log.info("charge captured", { amount, currency });
app.log.warn("retrying", { attempt });
app.log.error("charge failed", { code });
app.log.debug("gateway response", { raw });   // hidden unless URBAN_LOG_LEVEL=debug

const orderLog = app.log.child({ orderId });    // bind context for a scope
orderLog.info("shipped");                        // every line carries orderId

The runtime auto-correlates: a worker handler's app.log is pre-bound to { jobKey, jobType, processInstanceKey, elementId } and a route delegate's to { method, path, operationId }, so every line you emit is tied to its job/request for free.

The UrbanApp handle returned by runFromEnv/createUrbanApp also carries an app-level app.log (no per-request correlation) for the entrypoint's boot/shutdown lines:

const app = await runFromEnv();
app.log.info("started", { httpPort: app.httpPort });

Records are written as NDJSON — one JSON object per line, {"ts":…,"level":…,"msg":…, …fields} — with warn/error on stderr and debug/info on stdout. URBAN_LOG_LEVEL (default info) sets the minimum level.

Custom hosts: the HostContext.log sink now accepts "debug" in addition to "info" | "warn" | "error". A custom host that typed log with the narrower union must add a "debug" branch to keep satisfying the contract — a breaking change for that surface only.

Toolkit — derive artifacts (urban gen)

import { runGen, createNodeGenIO } from "@nanobpm/urban";

const io = createNodeGenIO();
await runGen({ root: ".", io });                 // writes nano-generated/
const { drift } = await runGen({ root: ".", io, check: true });  // CI drift gate

Each deriver is a pure (input) → artifacts function you can also call directly:

| Deriver | Input | Output | |---|---|---| | deriveMigrations | the manifest's datasource types | nano-generated/<source>.schema.sql (CREATE TABLE per type) | | deriveWorkerBindings | BPMN service tasks + their data-envelope I/O | nano-generated/worker-io.d.ts (typed worker input/output) |

Derivers are deterministic — the same input produces byte-identical output — so generated files are safe to commit and to gate in CI.

Code-first processes

Author durable processes in code with defineFlow, re-exported from @nanobpm/workflow:

import { defineFlow, WorkflowClient, Worker } from "@nanobpm/urban";

const flow = defineFlow("pr-review", (w) => {
  w.run("fetchDiff", async (job) => ({ files: 3 }));
  w.signal("humanApproval", { correlationKey: "prId" }); // durable human wait
  w.run("merge", async (job) => ({ merged: true }));
});

The SDK derives the executable BPMN, the job types, and the message/correlation wiring; WorkflowClient deploys and starts, Worker hosts your run steps. deploy emits an auto-generated diagram (DI) so the deployed model is inspectable in a modeller/Operate — @nanobpm/urban bundles bpmn-auto-layout so this works out of the box.

Deploy by convention (resources/)

Deployables are discovered by convention: with no models block in the manifest, @nanobpm/urban deploys everything under resources/ — walked recursively, every file at any depth. Content type is inferred by extension: .bpmn/.dmn → BPMN/DMN, .form → form-js form, .mdtext/markdown, .jsonapplication/json, .txttext/plain, and anything else → an application/octet-stream generic resource. Non-model files (.md, .txt, .json, unknown) deploy as generic resources (this is how agent prompts, RPA/script files, etc. are deployed — see below).

resources/
  processes/  order.bpmn         ← deployed as a BPMN process
  decisions/  route.dmn          ← deployed as a DMN decision
  forms/      approve.form       ← deployed as a form
  prompts/    review.md          ← generic resource, resourceId "prompts/review.md"
  prompts/pr/ summarize.md       ← generic resource, resourceId "prompts/pr/summarize.md"

resources/ is deploy-only: everything under it deploys, and nothing outside it ever does — so docs (docs/, AGENTS.md, top-level *.md) live outside resources/ and are never swept into a deployment.

A convention resource's resourceId is its path relative to resources/ (POSIX-normalised), including the extensionresources/prompts/review.mdprompts/review.md. Using the relative path (not the bare filename) preserves sub-directory structure, so resources/a/x.md and resources/b/x.md deploy as two distinct resources (a/x.md, b/x.md) rather than colliding. A service task links a generic resource by that exact id:

<zeebe:linkedResource linkName="prompt" resourceType="GenericScript"
                      bindingType="latest" resourceId="prompts/review.md" />

Content is deployed verbatim — there is no deploy-time {{token}} substitution (removed in ADR 0062), so a prompt/script file's text is deployed as-is. The deploy pipeline is UTF-8 text only, end-to-end (host.readTextFile()content: string), so "verbatim" means the UTF-8 text is passed through unchanged — not a promise of byte-for-byte binary fidelity; a non-UTF-8/binary file swept into resources/ is not a supported input (application/octet-stream is only a conservative MIME label for an unrecognised text resource). Re-deploying an unchanged file is a no-op (the engine's name+checksum duplicate rule skips it — no version bump); changing its content deploys a new version and the bindingType:latest pointer advances, so a running process picks up the new prompt on its next job activation with no model redeploy. urban gen follows the same convention: with no models, its nano:shape/code-first model scan also walks resources/ recursively for .bpmn/.dmn, and derived models are written to resources/processes/, exactly where the convention deploy then finds them.

To opt out of the convention, declare models globs — they are used verbatim and the resources/ walk is skipped (override resources are keyed by basename, so a basename collision across the declared globs is a hard error):

{
  "models": {
    "processes": ["src/models/*.bpmn"],
    "forms": ["src/forms/*.form"]
  }
}

Modular prompts — the blessed path (zeebe:linkedResource)

To keep a large agent prompt out of a model's XML, author it as its own file under resources/prompts/ (deployed as a GenericScript) and link it into the service task with a zeebe:linkedResource bound to the latest deployed version by its relative-path resourceId; the runtime resolves it with the appendPrompt FEEL helper at execution time:

<!-- resources/processes/agent.bpmn, linking resources/prompts/review.md -->
<zeebe:linkedResource linkName="prompt" resourceType="GenericScript"
                      bindingType="latest" resourceId="prompts/review.md" />

This is the only supported prompt-modularity mechanism — there is no deploy-time string substitution ({{name}} templating has been removed).

Value-injection caveat. Injecting a value into a model at deploy time (a URL, a feature flag, an environment-specific constant) must use runtime variables / FEEL, never string substitution baked into the model. A model is deployed once and shared across environments; bake a value in and you fork the model per environment. Pass the value as a process variable and reference it with FEEL instead.

Triggers — the inbound I/O edge

Declare triggers[] in the app manifest to turn outside events into engine calls (start a process or publish a message). Two source kinds are built in:

  • webhook — mounts an HTTP POST route (/hooks/<id> by default), with optional hmac:<connection> signature verification and delivery-id idempotency.
  • cron — arms a background timer from a 5-field crontab spec (evaluated in UTC), firing its action on schedule and rescheduling itself.
{
  "triggers": [
    { "id": "nightly", "type": "cron", "spec": "0 6 * * *",
      "action": { "start": "daily-report" } },
    { "id": "gh", "type": "webhook", "auth": "hmac:github",
      "action": { "message": "pr-opened", "correlationKey": "= body.number" } }
  ]
}

Cron scheduling is app-side: per-replica, in-memory, and it stops when the process stops — glue for invoking handlers on a clock, not a durable clustered scheduler. It therefore only honours onMissed: "skip" (the default); a declared "once"/"all" catch-up needs a persisted last-fire the runtime does not keep, so it warns and degrades to skip. For durable, clustered scheduling that survives restarts, model a timer start/intermediate event instead with w.startOn(...) / w.timer(...) from @nanobpm/workflow — the engine owns those.

Effect — typed errors & scoped resources (@nanobpm/urban/effect)

A tiny, zero-dependency, Effect-like core for the imperative seams (workers, provisioning, resource lifecycles) — without pulling in the effect package or its viral paradigm. It gives you the three ergonomics you actually reach for:

  • Typed-error Result<A, E> with generator do-notation. gen(function* … )
    • yield* threads success values and short-circuits on the first failure, automatically inferring the union of every failure type into E — like Effect.gen + yield*.
  • Tagged errors + exhaustive matching. tag("NotFound") builds a discriminated error; matchTags forces you to handle every variant (omitting one is a compile error), like Data.TaggedError + catchTags.
  • Scoped resources. scoped + acquireRelease run every release on every exit — success, failure, or thrown — LIFO, like Effect.scoped.
import { gen, ok, fail, tag, matchTags, scoped, acquireRelease } from "@nanobpm/urban/effect";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const parse = (s: string) => (s ? ok(s.length) : fail(tag("Empty")));
const check = (n: number) => (n > 3 ? fail(tag("TooLong", { n })) : ok(n));

const run = (s: string) =>
  gen(function* () {
    const n = yield* parse(s);        // E gains "Empty"
    return yield* check(n);           // E gains "TooLong"
  });                                 // Result<number, {_tag:"Empty"} | {_tag:"TooLong", n:number}>

const r = run("hello");
if (r._tag === "Fail") {
  matchTags(r.error, {                // must handle both — omit one and it won't compile
    Empty: () => "was empty",
    TooLong: (e) => `too long: ${e.n}`,
  });
}

// `acquireRelease`'s `acquire` is synchronous, so use sync fs APIs (or `await`
// the acquisition yourself and register the disposer with `scope.add`).
await scoped(async (scope) => {
  const dir = acquireRelease(
    scope,
    () => mkdtempSync(join(tmpdir(), "urban-")),
    (d) => rmSync(d, { recursive: true, force: true }),
  ); // released on any exit
  // …use dir…
});

Related packages