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:
bun add @chimpbase/meshservice()
Group related actions, events, settings, and lifecycle hooks. Actions are registered under v{version}.{name}.{action}.
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.
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
| Option | Default | Purpose |
|---|---|---|
services | — | Services to advertise and register. Required. |
transport | "http" | "local-only" disables cross-node RPC and meshToken/advertisedUrl requirements. |
advertisedUrl | env/hostname fallback | URL peers use to reach this node's RPC endpoint. |
meshToken | — | Secret name (via ctx.secret) for authenticating inbound RPC. Required when transport: "http". |
rpcPath | /__chimpbase/mesh/rpc | Route registered on this node to receive RPC. |
heartbeatMs | 10000 | Interval between heartbeats. |
offlineAfterMs | 30000 | Peers with no heartbeat within this window are treated as offline. |
gcAfterMs | 600000 | Cron sweep removes rows older than this. |
defaultStrategy | "local-first" | local-first · round-robin · random · cpu. |
defaultTimeoutMs | 5000 | Per-call deadline. |
defaultRetries | 0 | Retry 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:
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:
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
);- Heartbeat —
setIntervalupdateslast_heartbeat_msand publishes__chimpbase.mesh.info.heartbeat. - Announce / leave — emitted via
ctx.pubsub.publishon plugin start/stop (reusesPostgresListenEventBus). - Cache — every node keeps a live in-memory peer cache refreshed by announce/leave/heartbeat events; falls back to a direct
SELECTon startup. - GC — cron
* * * * *sweeps rows older thangcAfterMs.
Balanced events
To make an event dispatch to exactly one node, declare it with balanced: true:
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 thex-chimpbase-mesh-tokenheader (timing-safe compare againstctx.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 bothnode_ids.MeshTimeoutError— increasedefaultTimeoutMsor per-calltimeoutMs. Confirm the peer'sadvertised_urlis reachable from this node (NAT/container networking).unauthorized mesh rpc— check that both nodes resolve the same value viactx.secret(meshToken).- Peers missing after restart — heartbeat interval × 2 >
offlineAfterMs, so the window matters. Peers re-announce ononStart.