Skip to content

Mesh

@chimpbase/mesh adds Moleculer-style services with a distributed registry built on Chimpbase primitives — no broker required. Every participating node advertises itself into a Postgres registry table and discovers peers via LISTEN/NOTIFY. Actions can be called locally or routed to a peer over HTTP.

Install:

bash
bun add @chimpbase/mesh

service()

Group related actions, events, settings, and lifecycle hooks. Actions are registered under v{version}.{name}.{action}.

ts
import { service } from "@chimpbase/mesh";

const users = service({
  name: "users",
  version: 1,
  settings: { maxPerPage: 50 },
  methods: {
    normalize(email: string) {
      return email.toLowerCase();
    },
  },
  actions: {
    create: async (ctx, args: { email: string }, self) => {
      const email = self.methods.normalize(args.email);
      await ctx.db.query("INSERT INTO users (email) VALUES (?1)", [email]);
      return { email };
    },
  },
  events: {
    "user.deleted": async (_ctx, payload: { id: string }) => {
      console.log("deleted", payload.id);
    },
  },
});

mixins deep-merge actions, events, methods, and settings before registration.

chimpbaseMesh(options)

Register services with the runtime and opt into distributed discovery and HTTP RPC.

ts
import { chimpbaseMesh } from "@chimpbase/mesh";

host.register(
  chimpbaseMesh({
    services: [users, orders],
    transport: "http",             // default — set "local-only" for single-node deployments
    advertisedUrl: "http://api:3000",
    meshToken: "MESH_TOKEN",       // secret name
    heartbeatMs: 10_000,           // default
    offlineAfterMs: 30_000,        // default
    gcAfterMs: 600_000,            // default
    defaultStrategy: "local-first",
    defaultTimeoutMs: 5_000,
  }),
);

Options

OptionDefaultPurpose
servicesServices to advertise and register. Required.
transport"http""local-only" disables cross-node RPC and meshToken/advertisedUrl requirements.
advertisedUrlenv/hostname fallbackURL peers use to reach this node's RPC endpoint.
meshTokenSecret name (via ctx.secret) for authenticating inbound RPC. Required when transport: "http".
rpcPath/__chimpbase/mesh/rpcRoute registered on this node to receive RPC.
heartbeatMs10000Interval between heartbeats.
offlineAfterMs30000Peers with no heartbeat within this window are treated as offline.
gcAfterMs600000Cron sweep removes rows older than this.
defaultStrategy"local-first"local-first · round-robin · random · cpu.
defaultTimeoutMs5000Per-call deadline.
defaultRetries0Retry attempts on failure.
middleware[]Functions wrapping ctx.mesh.call (circuit breakers, tracing).
meta{}Published in the announce payload (e.g., { cpuLoad: 0.3 }).

ctx.mesh

Every handler context receives a mesh client:

ts
actions: {
  confirm: async (ctx, args: { orderId: string }) => {
    const summary = await ctx.mesh!.call<Summary>(
      "v1.billing.summarize",
      { orderId: args.orderId },
      { timeoutMs: 2000, retry: { attempts: 2, delayMs: 100 } },
    );

    await ctx.mesh!.emit("order.confirmed", { orderId: args.orderId });
    return summary;
  },
}

Methods:

  • call(name, args?, options?) — resolve action, prefer local, then peers by strategy.
  • emit(event, payload, { balanced }) — balanced routes through a queue worker (exactly-once). Default broadcasts via pubsub.
  • nodeId() — this node's UUID (regenerated each boot).
  • peers() — current live peers from the local cache.

Registry

The plugin creates _chimpbase_mesh_nodes on start:

sql
CREATE TABLE IF NOT EXISTS _chimpbase_mesh_nodes (
  node_id            TEXT PRIMARY KEY,
  advertised_url     TEXT,
  metadata_json      TEXT,
  services_json      TEXT,
  started_at_ms      BIGINT,
  last_heartbeat_ms  BIGINT
);
  • HeartbeatsetInterval updates last_heartbeat_ms and publishes __chimpbase.mesh.info.heartbeat.
  • Announce / leave — emitted via ctx.pubsub.publish on plugin start/stop (reuses PostgresListenEventBus).
  • Cache — every node keeps a live in-memory peer cache refreshed by announce/leave/heartbeat events; falls back to a direct SELECT on startup.
  • GC — cron * * * * * sweeps rows older than gcAfterMs.

Balanced events

To make an event dispatch to exactly one node, declare it with balanced: true:

ts
events: {
  "order.paid": {
    balanced: true,
    handler: async (ctx, payload) => { /* runs on one node only */ },
  },
}

ctx.mesh.emit("order.paid", p, { balanced: true }) enqueues a job on __chimpbase.mesh.balanced.order.paid; the queue lease keeps it exactly-once across the cluster.

Broadcast events use the existing pubsub path — every subscribed node processes them.

HTTP RPC

When transport: "http", the plugin registers:

  • Route POST /__chimpbase/mesh/rpc — validates the x-chimpbase-mesh-token header (timing-safe compare against ctx.secret(meshToken)) and forwards to the target action.
  • Action __chimpbase.mesh.rpc.execute — the per-call dispatch invoked by the RPC route.

Interaction with @chimpbase/auth

Register chimpbaseMesh before chimpbaseAuth so the mesh route short-circuits its own path before the auth guard fires. Otherwise /__chimpbase/mesh/rpc will return 401 from auth.

Versioning

service({ version: 2 }) prefixes actions with v2.{name}.. Multiple versions can coexist in the same plugin — peers see all prefixed names.

Troubleshooting

  • MeshNoAvailableNodeError — no peer has advertised the action. Verify both nodes registered the same service and that the registry table has rows for both node_ids.
  • MeshTimeoutError — increase defaultTimeoutMs or per-call timeoutMs. Confirm the peer's advertised_url is reachable from this node (NAT/container networking).
  • unauthorized mesh rpc — check that both nodes resolve the same value via ctx.secret(meshToken).
  • Peers missing after restart — heartbeat interval × 2 > offlineAfterMs, so the window matters. Peers re-announce on onStart.