Skip to content

About

Node.js service runtime for discovery, routing, backpressure, streaming, live config, Redis control plane, and observability.

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

OpenMesh for Node.js

Start as an API. Grow into a mesh.

OpenMesh is a TypeScript-first Node.js runtime for building ordinary HTTP APIs that can grow into an application-native service mesh: discovery, routing, backpressure, retries, circuits, streaming, traffic policy, live configuration, and observability stay inside the runtime instead of requiring a sidecar on every service.

npm CI Node License

npm install openmesh-node

v0.5.0 is the current npm release. It ships ESM + CommonJS + declarations and keeps the native HTTP core at zero runtime dependencies.

v0.5.0 is released. The current developer surface is intentionally small: typed app.get/post/... for normal APIs, then app.mesh('service') when the application grows. See Application-native Mesh and Advanced HTTP Contracts.

Website · Docs · Get started · 0.5 architecture · Application-native mesh · Databases · Control plane · Observability · API


0.5: from API to mesh

Normal API development stays small:

import openmesh, { created } from 'openmesh-node';
import { bodyParser } from 'openmesh-node/plugins';

const app = openmesh();
app.use(bodyParser());

app.post('/users/:id', {
  body: NewUser,
  response: {
    201: User,
    409: Problem
  }
}, async ({ body, params }) => {
  return created(await users.create({
    id: params.id,
    ...body
  }));
});

Bring the database library you already use. OpenMesh does not add a query language:

import { database } from 'openmesh-node/db';

const db = database(prisma, {
  connect: client => client.$connect(),
  disconnect: client => client.$disconnect(),
  transaction: (client, work) => client.$transaction(work)
});

app.register(db);

app.get('/users', async () => db.client.user.findMany());

The same resource API works with connectionless clients such as Drizzle, explicit lifecycle clients such as TypeORM, and transaction builders such as Kysely. The original client type is preserved. See Databases and ORMs.

When that application splits into services, the programming model grows instead of changing:

const app = openmesh({
  service: 'gateway',

  mesh: {
    control: {
      url: process.env.OPENMESH_CONTROL_URL,
      token: process.env.OPENMESH_TOKEN
    },

    services: {
      payments: {
        traffic: {
          split: [
            { match: { version: 'v1' }, weight: 90 },
            { match: { version: 'v2' }, weight: 10 }
          ]
        }
      }
    }
  }
});

const payments = app.mesh('payments');

const charge = await payments.post('/charges', {
  key: user.id,
  body: {
    userId: user.id,
    amount: order.total
  }
});

That call reuses OpenMesh's existing discovery watch, per-service bulkhead, deadlines, retry policy, circuit state, peer routing, tracing, and metrics. Traffic rules, weighted canaries, locality preference/failover, and live control-plane policy updates sit above the same warm service pool. There is no sidecar hop.

OpenMesh 0.5 also includes SPIFFE-style workload identity and service-to-service mTLS: the same application identity config protects inbound HTTPS and authenticates outbound app.mesh() calls. OpenMesh can hot-rotate already-issued workload certificates without restarting the process; certificate issuance and revocation distribution remain external, so it should not be described as a drop-in Istio/Linkerd replacement.

Why OpenMesh

Most Node.js HTTP libraries stop at the server boundary. OpenMesh keeps going.

Problem OpenMesh primitive
How do I call healthy service instances? Discovery-backed ServicePool
What happens when downstream is overloaded? Bounded admission, queues, bulkheads, adaptive concurrency
How do I fail over without replaying unsafe work? Deadlines, retry policy, circuit breaking, idempotency-aware rules
How do long-lived responses behave? True streaming with explicit post-header no-replay semantics
How does topology update without restarts? Registration leases + push watches
How do I change config safely at runtime? Immutable snapshots + revision/epoch CAS
How do I use my existing ORM or SQL client? Typed database resources with lifecycle, health and optional transaction adapters
How do I persist the control plane? Durable Redis adapters
How do services authenticate each other? SPIFFE-style workload identity + mutual TLS + inbound allow-lists
How do I limit control-plane access? Scoped credentials + service/namespace boundaries
How do I export telemetry? Request/server/peer lifecycle events + OpenTelemetry-compatible metrics bridge

The goal is a small runtime with clear failure semantics—not a hidden cluster platform.

Architecture

                         ┌──────────────────────────┐
                         │      Control Plane       │
                         │ registration • config    │
                         │ scopes • watches • CAS   │
                         └────────────┬─────────────┘
                                      │
                               in-memory / Redis
                                      │
                                      ▼
┌──────────────┐      ┌───────────────────────────────┐
│ Your service │ ───▶ │       OpenMesh runtime        │
│ routes/hooks │      │                               │
└──────────────┘      │ ServicePool                   │
                      │   └─ discovery watch          │
                      │   └─ per-service bulkhead     │
                      │   └─ adaptive concurrency     │
                      │                               │
                      │ PeerPool                      │
                      │   └─ P2C / rendezvous         │
                      │   └─ queue / deadline         │
                      │   └─ retry / circuit          │
                      │   └─ buffered / streaming     │
                      └───────────────┬───────────────┘
                                      │ HTTP
                        ┌─────────────┼─────────────┐
                        ▼             ▼             ▼
                     service-a     service-b     service-c

No sidecar is required. No Go process sits in the request path. Redis, OpenTelemetry, Express, and Fastify integrations are optional boundaries, not core runtime dependencies.

Start in 30 seconds

Requires Node.js 22+.

import openmesh from 'openmesh-node';
import { jsonBody } from 'openmesh-node/plugins';

const app = openmesh();

app.use(jsonBody());

app.get('/', () => ({ hello: 'OpenMesh' }));
app.get('/users/:id', ctx => ({ id: ctx.params.id }));
app.post('/echo', ctx => ctx.requestBody);

await app.listen({ port: 3000 });

process.once('SIGTERM', () => app.close());
process.once('SIGINT', () => app.close());

Run:

node server.mjs

CommonJS is supported too:

const openmesh = require('openmesh-node');

Connect services

OpenMesh can own service registration, discovery, routing, and per-service pressure isolation.

Start a control plane:

import openmesh from 'openmesh-node';
import { controlPlane } from 'openmesh-node/services';

const control = openmesh();

control.register(controlPlane({
  token: process.env.OPENMESH_TOKEN
}));

await control.listen({ port: 4000 });

Register a service:

import {
  ControlClient,
  serviceRegistration
} from 'openmesh-node/services';

const client = new ControlClient({
  url: 'http://127.0.0.1:4000/_mesh',
  token: process.env.OPENMESH_TOKEN
});

app.onClose(() => client.close());

app.register(serviceRegistration({
  client,
  service: 'users',
  id: 'users-a',
  ttl: 30_000,
  url: address => 'http://127.0.0.1:' + address.port
}));

Then call the service through a managed pool:

const users = await client.service('users', {
  maxInflight: 64,
  maxQueue: 128,
  adaptiveConcurrency: true
});

const response = await users.request('/users/42', {
  key: '42'
});

console.log(response.peer.id, response.json());

Membership changes are followed automatically. Each service gets isolated queue, concurrency, circuit, and adaptive state.

Streaming that knows when a response is committed

const stream = await users.requestStream('/events', {
  timeout: 5_000,
  idleTimeout: 30_000
});

stream.body.pipe(process.stdout);

For streaming calls, the normal timeout covers admission + response headers. After non-5xx headers are handed to the caller, OpenMesh treats the response as committed and will not replay it on another peer if the body later fails.

That behavior matters for SSE, LLM responses, large downloads, and any operation where invisible retries would be dangerous.

Durable Redis control plane

The default registry/config stores are in-memory. For durable deployments, use the Redis adapters:

import { createClient } from 'redis';
import {
  RedisRegistryAdapter,
  RedisConfigAdapter
} from 'openmesh-node/services/redis';

const redis = createClient({
  url: process.env.REDIS_URL
});

await redis.connect();

control.register(controlPlane({
  credentials,
  registry: new RedisRegistryAdapter({
    client: redis,
    prefix: 'openmesh-prod'
  }),
  config: new RedisConfigAdapter({
    client: redis,
    prefix: 'openmesh-prod'
  })
}));

Lease ownership, renewal, deregistration, configuration CAS, and revision updates use Redis-side atomic transitions. Registry watches combine Pub/Sub with TTL-expiry detection, so Redis keyspace notifications are not required.

Redis adapter details

Least-privilege control plane

Simple deployments can use one token. Larger deployments can scope credentials by action and resource.

control.register(controlPlane({
  credentials: [
    {
      token: process.env.DISCOVERY_TOKEN,
      scopes: ['meta:read', 'services:read'],
      services: ['users', 'payments']
    },
    {
      token: process.env.CONFIG_TOKEN,
      scopes: ['config:read', 'config:write'],
      namespaces: ['users']
    }
  ]
}));

A valid credential without the required scope or resource grant receives 403; an unknown credential receives 401.

Live configuration

const current = await client.getConfig('users');

await client.setConfig(
  'users',
  { greeting: 'Hello' },
  {
    expectedRevision: current.revision,
    expectedEpoch: current.epoch
  }
);

const config = await client.watchConfig('users', {
  validate(values) {
    if (typeof values.greeting !== 'string') {
      throw new Error('greeting must be a string');
    }
  }
});

Updates replace the whole namespace and reject stale writes. Invalid or unavailable updates preserve the last accepted snapshot.

Observability without coupling the core to an SDK

OpenMesh emits exporter-neutral lifecycle events only when observers are enabled.

import { metrics } from '@opentelemetry/api';
import { createOpenTelemetryObservers } from 'openmesh-node/otel';

const telemetry = createOpenTelemetryObservers({
  meter: metrics.getMeter('users-service'),
  attributes: {
    service: 'users',
    region: 'nz'
  }
});

const app = openmesh({
  onEvent: telemetry.onAppEvent
});

The bridge records bounded, low-cardinality request/peer/admission/concurrency metrics and does not emit raw request paths, peer URLs, bodies, headers, config values, or tokens.

Observability details

Use the ecosystem you already have

OpenMesh can host existing Express/Fastify components without pretending they are native OpenMesh middleware.

app.useExpress(cors());

app.mount('/legacy', expressApp);

app.fastify('/validated', async host => {
  host.get('/hello', async () => ({
    engine: 'fastify'
  }));
});

Those integrations are optional. The native request path does not require them.

Compatibility guide

Public package surface

Import Purpose
openmesh-node Typed HTTP runtime + app.mesh() facade
openmesh-node/plugins Native plugins and body parsers
openmesh-node/http Advanced functional contract API
openmesh-node/db ORM/database resource lifecycle, health and transaction adapters
openmesh-node/mesh Low-level peer routing / client data plane
openmesh-node/services Registration, discovery, config, control plane
openmesh-node/services/redis Durable Redis adapters
openmesh-node/services/testing Adapter conformance harness
openmesh-node/otel OpenTelemetry-compatible metrics bridge

Every published subpath is exercised through real packed-tarball CJS + ESM smoke tests in CI.

Performance discipline

OpenMesh does not market itself as “the fastest Node.js framework.”

The primary 0.5 benchmark measures a complete service call with the same Node caller and target while changing only the data plane. On the recorded Intel macOS run, median throughput was 1,807 req/s for OpenMesh, 1,281 req/s for a two-proxy Envoy path, and 1,243 req/s for a two-sidecar Dapr path. OpenMesh also used less summed CPU than either sidecar topology in that run.

The point is architectural, not language bragging:

How much does the service-mesh data path cost after the application has decided to make a service call?

The older OpenMesh/Fastify benchmark remains in CI as an internal native-HTTP regression guard. It is no longer the headline comparison.

Method, raw rounds, limits, and reproducibility

Run the complete microservice demo

git clone https://github.com/yaohuangguan/openmesh-node.git
cd openmesh-node
npm ci --ignore-scripts
npm run demo:services

The demo starts:

control plane
    │
    ├── users-a
    ├── users-b
    │
    └── gateway

It then changes configuration live, routes through discovery, shuts down the selected instance, and verifies traffic moves to the remaining service.

What OpenMesh is — and is not

OpenMesh is:

  • a Node.js service runtime;
  • an application-level data plane for known HTTP services;
  • a discovery/configuration control plane with pluggable storage;
  • a place to make retry, overload, streaming, and shutdown semantics explicit.

OpenMesh is not:

  • a Kubernetes replacement;
  • a distributed consensus system;
  • a NAT traversal layer or DHT;
  • a hidden sidecar mesh;
  • a claim that every application should stop using Express or Fastify.

The project is pre-1.0. Public APIs are usable, but minor versions can still evolve compatibility.

Development

npm ci --ignore-scripts
npm test
npm run test:types
npm run test:package
npm run demo:services
npm run bench

CI covers Node 22/24 on Linux and Windows, real Redis integration, packed-package imports, examples, and normalized benchmark regression.

Documentation

Website Docs · 0.5 Architecture · Application-native Mesh · API · Advanced HTTP Contracts · Databases & ORMs · Services & control plane · Peer routing · Observability · Plugins · Performance · Releasing · Changelog

Contributing

Bug reports, production edge cases, benchmark reproductions, adapter implementations, and focused PRs are welcome.

See CONTRIBUTING.md.


OpenMesh 0.5.0 · MIT licensed · npm · source · changelog

About

Node.js service runtime for discovery, routing, backpressure, streaming, live config, Redis control plane, and observability.

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages