Mesh

Distributed Actors ​

Autonomous clusters: This page covers the actor primitives. Use Autonomous Clusters for manifest-driven routing, admission, continuity, and scaling; Cluster Operations for operator controls; and Distributed Proof for the mandatory PostgreSQL-backed Docker release proof.

Mesh's actor model extends across machines. Once nodes authenticate and connect, remote PIDs use the same typed send and receive model as local actors.

There are two startup paths:

  • Node.start_from_env() is the recommended application bootstrap. It chooses standalone or clustered mode from the public environment contract and starts autonomous runtime components when configured.
  • Node.start(name, cookie) and Node.connect(name) are the manual compatibility primitives for local experiments and explicitly managed protocol-one clusters.

Manual protocol-one connections use TLS encryption plus an HMAC-SHA256 cookie challenge. Autonomous protocol-two peers additionally require mutual TLS and a signed, cluster-scoped node identity. Do not treat the cluster cookie as the complete autonomous trust model.

Call Node.start_from_env once during application startup:

mesh
fn main() do
  case Node.start_from_env() do
    Ok(status) ->
      println(
        "mode=#{status.mode} node=#{status.node_name} port=#{status.cluster_port}"
      )
    Err(error) -> println("runtime bootstrap failed: #{error}")
  end
end

BootstrapStatus exposes:

FieldDescription
mode"standalone" or "cluster"
node_nameAdvertised node name, empty in standalone mode
cluster_portBound/discovered cluster port; the default is 4370
discovery_seedConfigured discovery seed, empty in standalone mode

When no cluster hints and no MESH_CLUSTER_COOKIE are present, bootstrap succeeds in standalone mode. Cluster mode reads MESH_NODE_NAME/MESH_NODE_HOST, MESH_CLUSTER_PORT, MESH_CLUSTER_COOKIE, and MESH_DISCOVERY_SEED; supported platform metadata can supply the node identity. Manifest-driven autonomous applications also embed their validated controller, routing, continuity, and capacity policy during the build.

Manual Node Startup ​

A Mesh runtime becomes a named, addressable node by calling Node.start. This binds a TCP listener and makes the process ready to accept connections from other nodes:

mesh
fn main() do
  let status = Node.start("app@localhost:4000", "a-development-cookie")
  if status == 0 do
    println("node started")
  else
    println("node start failed: #{status}")
  end
end

The first argument is the node name in "name@host:port" format. The second argument is the shared secret cookie. Node.start returns 0 on success, -1 when the runtime has already started, -2 for a listener bind failure, and -3 for invalid identity or authentication configuration.

Behind the scenes, Node.start:

  1. Parses the node address and binds a TCP listener on the given port
  2. Builds the transport configuration for the selected protocol
  3. Starts an accept loop to handle incoming connections from other nodes

Connecting Nodes ​

Once a node is started, it can connect to other nodes with Node.connect:

mesh
fn main() do
  Node.start("app@localhost:4000", "my_cookie")
  let status = Node.connect("worker@localhost:4001")
  if status == 0 do
    println("connected to worker")
  else
    println("connection failed: #{status}")
  end
end

Node.connect returns 0 after an authenticated session is established, -1 if the local node has not started, -2 for a TCP connection failure, and -3 for invalid input or handshake failure. After authentication, nodes exchange their global registry state; a successful Node.connect returns once the peer's global names resolve locally.

Querying the Cluster ​

You can inspect the cluster state with Node.self and Node.list:

mesh
fn main() do
  Node.start("app@localhost:4000", "my_cookie")
  Node.connect("worker@localhost:4001")

  let me = Node.self()
  println("I am: ${me}")

  let nodes = Node.list()
  println("Connected nodes: ${nodes}")
end
FunctionReturnsDescription
Node.self()StringCurrent node name, or "" before clustered startup
Node.list()List<String>Authenticated connected node names

Remote Actors ​

Once nodes are connected, you can spawn actors on remote nodes and communicate with them using the same send and receive primitives you use locally.

A message crosses to another node whole: its strings, lists, maps, structs and other values are copied into the receiving actor, as they are between actors on one node. A pid in a message still names its process on the other node, so a request can carry self() for the reply:

mesh
actor greeter() do
  receive do
    (reply_to, name) -> send(reply_to, "hello #{name}")
  end
  greeter()
end

actor asker() do
  send(Global.whereis("greeter"), (self(), "remote"))
  receive do
    reply -> println(reply)
  end
end

Functions and runtime handles (database connections, pools and other opaque values) cannot leave their node: a send of a message that holds one returns 6 and sends nothing.

Spawning on a Remote Node ​

Use Node.spawn to start an actor on a specific remote node:

mesh
actor worker(prefix :: String) do
  receive do
    msg -> println("#{prefix}: #{msg}")
  end
end

actor coordinator() do
  let pid = Node.spawn("worker@localhost:4001", worker, "remote")
  send(pid, "hello from app node")
end

Node.spawn accepts the actor's normal arguments and returns a PID that is valid across nodes. Remote arguments may currently be Int, Float, Bool, String, Pid, or Unit. The target program must define the actor under the same name, whether or not it spawns it itself: every actor a program defines can be spawned from another node. Its message type must be settled there too, by a typed spawn or by how the actor uses its messages ("relayed " <> text makes them Strings); otherwise the actor is error E0079. Call remote spawn from an actor: the caller waits cooperatively for the spawn reply. PID 0 reports a failed spawn, such as a missing connection, unsupported argument, or unknown actor entry: it shows as <0.0> and equals no process, so pid == Process.whereis("none-such") tells a failed spawn apart.

The compiler checks a remote spawn as it checks spawn: the node name must be a String, the arguments must match the actor's parameters, and the result is the actor's Pid<M>, so sending it a message of another type is an error.

Use Node.spawn_link to spawn a remote actor and establish a bidirectional link in one step. If either the local or remote actor crashes, the other receives an exit signal:

mesh
actor task() do
  receive do
    _ -> println("task completed")
  end
end

actor coordinator() do
  let pid = Node.spawn_link("worker@localhost:4001", task)
  send(pid, "start")
end

This is the distributed equivalent of spawn_link: the remote-spawn request asks the target to create the actor with a bidirectional link to the caller.

Local Process Registry ​

Use Process names for services within one runtime:

mesh
actor cache() do
  receive do
    message -> println("cache: " <> message)
  end
end

fn main() do
  let pid = spawn(cache)
  if Process.register("cache", pid) == 0 do
    let found = Process.whereis("cache")
    send(found, "warm")
  end
end

Process.register returns 0 on success and 1 on failure. Process.whereis returns PID 0 when the name is absent. Local names are not replicated; use Global only when another node must resolve the actor.

Global Registry ​

The global registry provides cluster-wide process name registration. Unlike local process names (which are scoped to a single node), global names are replicated across all connected nodes.

Registering a Name ​

Use Global.register to assign a name to a process globally:

mesh
actor db_service() do
  receive do
    message -> println("query: " <> message)
  end
end

fn main() do
  Node.start("app@localhost:4000", "my_cookie")

  let pid = spawn(db_service)
  Global.register("db_service", pid)
  println("Registered as db_service")
end

When a name is registered, it is broadcast to all connected nodes. Every node holds a complete replica of the name table, so lookups are always local (no network round-trip).

Looking Up a Name ​

Use Global.whereis to find a process by its global name:

mesh
fn main() do
  Node.start("app@localhost:4000", "my_cookie")
  Node.connect("db@localhost:4001")

  let pid = Global.whereis("db_service")
  send(pid, "query")
end

Since every node has a full replica of the global registry, Global.whereis returns immediately without a network call. PID 0 means no live registration is known.

Unregistering a Name ​

Use Global.unregister to remove a global registration:

mesh
actor temp_worker() do
  receive do
    _ -> nil
  end
end

fn main() do
  Node.start("app@localhost:4000", "my_cookie")

  let pid = spawn(temp_worker)
  Global.register("temp_worker", pid)
  # ... do some work ...
  Global.unregister("temp_worker")
end
FunctionReturnsDescription
Global.register(name, pid)IntRegister globally; 0 is success and 1 is failure
Global.whereis(name)PidResolve locally from the replicated registry; 0 means absent
Global.unregister(name)IntRemove a name; 0 is success and 1 means absent

Automatic Cleanup ​

The global registry automatically cleans up registrations when:

  • A process exits -- all global names registered by that process are removed
  • A node disconnects -- all global names owned by that node are removed

This means you do not need to manually unregister names in crash or disconnect scenarios. The cleanup is broadcast to all remaining nodes in the cluster.

Node Monitoring ​

Node.monitor(name, message) sends the calling actor message, one of its own messages, once, when the node name disconnects; at once when it is not connected. It returns 0 on success and 1 before node startup. Like Process.monitor, it is only available inside an actor.

Process.monitor(pid, message) watches a process on another node as it does a local one: the message comes when the process ends, or when its node disconnects.

API Reference ​

FunctionReturnsDescription
Node.start_from_env()Result<BootstrapStatus, String>Bootstrap standalone or clustered runtime state
Node.start(name, cookie)IntManually start a named node; 0 is success
Node.connect(name)IntConnect and authenticate; 0 is success
Node.self()StringCurrent node name
Node.list()List<String>Connected node names
Node.spawn(node, actor, args...)Pid<M>Spawn remotely; 0 signals failure. Arguments and messages are checked against the actor
Node.spawn_link(node, actor, args...)Pid<M>Spawn remotely and link, checked like Node.spawn
Node.monitor(name, message)IntSend the calling actor message when node name disconnects; 0 is success and 1 is failure
Process.register(name, pid)IntRegister a local process
Process.whereis(name)PidResolve a local process
Global.register(name, pid)IntRegister a process name cluster-wide
Global.whereis(name)PidResolve a global process name
Global.unregister(name)IntRemove a global registration

Next Steps ​

Edit this page on GitHub
v0.1.8 Last updated: September 27, 2026