Macula HTTP/3 Mesh - Quick Start Guide
View SourceGet a 3-node mesh running in 15 minutes
Prerequisites
Required Software
Erlang/OTP 26.0 or later
# Check version erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshellInstall from:
- Ubuntu/Debian:
sudo apt-get install erlang - macOS:
brew install erlang - From source: https://www.erlang.org/downloads
- Ubuntu/Debian:
Elixir 1.15 or later (optional, for Elixir examples)
# Check version elixir --versionInstall from:
- Ubuntu/Debian:
sudo apt-get install elixir - macOS:
brew install elixir - From source: https://elixir-lang.org/install.html
- Ubuntu/Debian:
Git
git --versionC Compiler (for building quicer NIF)
- Ubuntu/Debian:
sudo apt-get install build-essential cmake - macOS:
xcode-select --install
- Ubuntu/Debian:
System Requirements
- OS: Linux (Ubuntu 20.04+), macOS 11+, or Windows WSL2
- RAM: 512 MB minimum per node (2 GB recommended for development)
- Network: UDP port access (default: 4433)
- Disk: 100 MB for Macula + dependencies
Step 1: Download and Build Macula
Clone the Repository
cd ~/projects
git clone https://github.com/macula-io/macula.git
cd macula
Install Dependencies
# For Erlang (Rebar3)
rebar3 get-deps
# For Elixir (Mix)
mix deps.get
Build quicer (QUIC Library)
The quicer library includes native code and may take a few minutes to compile:
# Rebar3
rebar3 compile
# Mix
mix compile
Expected output:
===> Fetching quicer (from {git,"https://github.com/emqx/quic.git",...})
===> Compiling quicer
...
[100%] Built target msquic
===> Compiled quicerVerify Installation
# Erlang
rebar3 shell
> macula:version().
{ok, "0.1.0"}
# Elixir
iex -S mix
iex> Macula.version()
{:ok, "0.1.0"}
Step 2: Start Node 1 (Bootstrap Node)
Create Configuration File
Create config/node1.config:
%% config/node1.config
[
{macula, [
{node_id, <<"node1">>},
{realm, <<"org.example.mesh">>},
{listen_port, 4433},
{listen_address, "0.0.0.0"},
%% Discovery
{discovery, [
{methods, [static]}, % Use static bootstrap for this example
{static_nodes, []} % First node has no bootstrap peers
]},
%% Topology
{topology, [
{type, k_regular},
{k, 2} % Each node connects to 2 peers
]},
%% TLS/Certificates (auto-generate for demo)
{cert_mode, auto_generate},
%% Logging
{log_level, info}
]}
].Start Node 1
# Erlang
erl -config config/node1 -pa _build/default/lib/*/ebin -eval 'application:ensure_all_started(macula).'
# Elixir
iex --name node1@127.0.0.1 --cookie macula_demo -S mix run -e 'Application.ensure_all_started(:macula)' -- --config config/node1.config
Expected output:
[info] Macula node started: node1
[info] Listening on 0.0.0.0:4433 (UDP)
[info] Node ID: a3f5b2e1c4d8a7f9...
[info] Realm: org.example.mesh
[info] Topology: k_regular (k=2)
[info] Discovery: static
[info] Ready to accept connectionsKeep this terminal open - Node 1 is now running.
Step 3: Start Node 2 (Join the Mesh)
Create Configuration File
Create config/node2.config:
%% config/node2.config
[
{macula, [
{node_id, <<"node2">>},
{realm, <<"org.example.mesh">>},
{listen_port, 4434}, % Different port
{listen_address, "0.0.0.0"},
%% Discovery - bootstrap from Node 1
{discovery, [
{methods, [static]},
{static_nodes, [
{"127.0.0.1", 4433} % Node 1's address
]}
]},
%% Topology
{topology, [
{type, k_regular},
{k, 2}
]},
%% TLS/Certificates
{cert_mode, auto_generate},
%% Logging
{log_level, info}
]}
].Start Node 2 (in new terminal)
# Open new terminal
cd ~/projects/macula
# Erlang
erl -config config/node2 -pa _build/default/lib/*/ebin -eval 'application:ensure_all_started(macula).'
# Elixir
iex --name node2@127.0.0.1 --cookie macula_demo -S mix run -e 'Application.ensure_all_started(:macula)' -- --config config/node2.config
Expected output:
[info] Macula node started: node2
[info] Listening on 0.0.0.0:4434 (UDP)
[info] Node ID: b7c3d8e2f5a9b4c1...
[info] Realm: org.example.mesh
[info] Connecting to bootstrap node 127.0.0.1:4433...
[info] Connected to node1 (a3f5b2e1c4d8a7f9...)
[info] SWIM membership: 2 nodes alive
[info] Mesh topology establishedIn Node 1's terminal, you should see:
[info] New connection from 127.0.0.1:xxxxx
[info] Handshake complete: node2 (b7c3d8e2f5a9b4c1...)
[info] SWIM membership: 2 nodes aliveStep 4: Start Node 3 (Expand the Mesh)
Create Configuration File
Create config/node3.config:
%% config/node3.config
[
{macula, [
{node_id, <<"node3">>},
{realm, <<"org.example.mesh">>},
{listen_port, 4435},
{listen_address, "0.0.0.0"},
%% Discovery - can bootstrap from either node
{discovery, [
{methods, [static]},
{static_nodes, [
{"127.0.0.1", 4433}, % Node 1
{"127.0.0.1", 4434} % Node 2
]}
]},
%% Topology
{topology, [
{type, k_regular},
{k, 2}
]},
%% TLS/Certificates
{cert_mode, auto_generate},
%% Logging
{log_level, info}
]}
].Start Node 3 (in new terminal)
# Open new terminal
cd ~/projects/macula
# Erlang
erl -config config/node3 -pa _build/default/lib/*/ebin -eval 'application:ensure_all_started(macula).'
# Elixir
iex --name node3@127.0.0.1 --cookie macula_demo -S mix run -e 'Application.ensure_all_started(:macula)' -- --config config/node3.config
Expected output:
[info] Macula node started: node3
[info] Listening on 0.0.0.0:4435 (UDP)
[info] Node ID: c8d4e9f3a6b2c7d1...
[info] Realm: org.example.mesh
[info] Connecting to bootstrap nodes...
[info] Connected to node1 (a3f5b2e1c4d8a7f9...)
[info] Connected to node2 (b7c3d8e2f5a9b4c1...)
[info] SWIM membership: 3 nodes alive
[info] Mesh topology: k_regular (k=2)
[info] Routing table: 3 nodesCongratulations! You now have a 3-node mesh network running.
Step 5: Verify Mesh Topology
Check Membership (on any node)
In any node's console:
% Erlang
macula_membership:get_members().
% Expected output:
[
#{node_id => <<"a3f5b2e1...">>, state => alive, ...},
#{node_id => <<"b7c3d8e2...">>, state => alive, ...},
#{node_id => <<"c8d4e9f3...">>, state => alive, ...}
]# Elixir
Macula.Membership.get_members()
# Expected output:
[
%{node_id: "a3f5b2e1...", state: :alive, ...},
%{node_id: "b7c3d8e2...", state: :alive, ...},
%{node_id: "c8d4e9f3...", state: :alive, ...}
]Check Connections
% Erlang
macula_topology:get_connections().
% Expected output:
[
#{peer_id => <<"b7c3d8e2...">>, state => active, rtt_ms => 1.2},
#{peer_id => <<"c8d4e9f3...">>, state => active, rtt_ms => 1.5}
]Visualize Topology (ASCII Art)
% Erlang
macula_topology:print_topology().Expected output:
Mesh Topology (k-regular, k=2)
==============================
node1 (a3f5...) ←─→ node2 (b7c3...)
↑ ↑
└────────────→ node3 (c8d4...)
↑
└────────→ node1
3 nodes, 3 connections
Average RTT: 1.3msStep 6: Send Your First Message (Pub/Sub)
Subscribe to a Topic (on Node 3)
In Node 3's console:
% Erlang
{ok, Client} = macula:connect_local(#{}),
Callback = fun(Event) ->
#{topic := Topic, payload := Msg} = Event,
io:format("Received on ~s: ~p~n", [Topic, Msg])
end,
{ok, _Ref} = macula:subscribe(Client, <<"hello.world">>, Callback).# Elixir
{:ok, client} = :macula.connect_local(%{})
callback = fn event ->
%{topic: topic, payload: msg} = event
IO.puts("Received on #{topic}: #{inspect(msg)}")
end
{:ok, _ref} = :macula.subscribe(client, "hello.world", callback)Expected output:
[info] Subscribed to org.example.mesh.hello.world
okPublish a Message (on Node 1)
In Node 1's console:
% Erlang
{ok, Client} = macula:connect_local(#{}),
macula:publish(Client, <<"hello.world">>, #{
message => <<"Hello from Node 1!">>,
timestamp => erlang:system_time(millisecond)
}).# Elixir
{:ok, client} = :macula.connect_local(%{})
:macula.publish(client, "hello.world", %{
message: "Hello from Node 1!",
timestamp: System.system_time(:millisecond)
})Expected output on Node 1:
[info] Published to org.example.mesh.hello.world
okExpected output on Node 3 (subscriber):
Received on org.example.mesh.hello.world: #{
message => <<"Hello from Node 1!">>,
timestamp => 1704723456789,
publisher => <<"a3f5b2e1...">>
}Message flow: Node 1 → QUIC/HTTP3 → Node 3 (may route via Node 2 depending on topology)
Step 7: Make Your First RPC Call
Register RPC Endpoint (on Node 2)
In Node 2's console:
% Erlang
{ok, Client} = macula:connect_local(#{}),
EchoHandler = fun(Args) ->
{ok, #{echo => Args, node => node()}}
end,
{ok, _Ref} = macula:advertise(Client, <<"echo_service">>, EchoHandler).# Elixir
{:ok, client} = :macula.connect_local(%{})
echo_handler = fn args ->
{:ok, %{echo: args, node: Node.self()}}
end
{:ok, _ref} = :macula.advertise(client, "echo_service", echo_handler)Expected output:
[info] Registered RPC endpoint: org.example.mesh.echo_service
okCall RPC (from Node 1)
In Node 1's console:
% Erlang
{ok, Client} = macula:connect_local(#{}),
macula:call(Client, <<"echo_service">>, #{
test => <<"Hello RPC!">>,
value => 42
}, #{timeout => 5000}).# Elixir
{:ok, client} = :macula.connect_local(%{})
:macula.call(client, "echo_service", %{
test: "Hello RPC!",
value: 42
}, %{timeout: 5000})Expected output on Node 1:
{ok, #{
echo => #{test => <<"Hello RPC!">>, value => 42},
node => 'node2@127.0.0.1'
}}Expected output on Node 2 (handler):
[info] RPC call received: echo_service
[info] Args: #{test => <<"Hello RPC!">>, value => 42}RPC flow: Node 1 → finds registration via DHT → routes to Node 2 → executes handler → returns result
Step 8: Test Fault Tolerance
Stop Node 2
In Node 2's terminal, press Ctrl+C twice to stop the node.
Expected output on Node 1 and Node 3:
[warning] Connection lost to node2 (b7c3d8e2...)
[info] SWIM detected failure: node2
[info] SWIM membership: 2 nodes alive, 1 suspect
[info] Topology reconfiguring...
[info] New connection established: node1 ←→ node3
[info] SWIM membership: 2 nodes aliveVerify Mesh Adapted
On Node 1 or Node 3:
% Erlang
macula_topology:get_connections().
% Expected output (now only 1 connection):
[
#{peer_id => <<"c8d4e9f3...">>, state => active, rtt_ms => 1.1}
]The mesh automatically adapts - Node 1 and Node 3 now connect directly.
Restart Node 2
Restart Node 2 (using the same command from Step 3).
Expected output:
[info] Macula node started: node2
[info] Reconnecting to mesh...
[info] SWIM membership: 3 nodes alive
[info] Topology restoredThe mesh self-heals automatically.
Common Operations
List All Nodes in Mesh
% Erlang
macula_membership:list_nodes().# Elixir
Macula.Membership.list_nodes()Get Node Statistics
% Erlang
macula:stats().
% Output:
#{
messages_sent => 1543,
messages_received => 1687,
bytes_sent => 245678,
bytes_received => 267890,
active_connections => 2,
routing_table_size => 3,
uptime_seconds => 3600
}Subscribe with Pattern Matching
% Erlang - Subscribe to all topics starting with "sensor."
{ok, Client} = macula:connect_local(#{}),
{ok, _Ref} = macula:subscribe(Client, <<"sensor.*">>, Callback).
% Matches: sensor.temperature, sensor.humidity, etc.Publish with Options
% Erlang - Publish with options
{ok, Client} = macula:connect_local(#{}),
macula:publish(Client, <<"important.event">>, Data, #{
exclude_self => true % Don't deliver to self
}).Troubleshooting
Problem: "Port already in use"
Error:
{error, eaddrinuse}Solution: Change the listen_port in your config file to an unused port (e.g., 4436, 4437).
Problem: Nodes can't discover each other
Symptoms: Node 2 or 3 logs show "Connection timeout" or "No route to bootstrap node"
Checks:
Firewall: Ensure UDP port 4433-4435 are not blocked
# Ubuntu/Debian sudo ufw allow 4433:4435/udp # macOS # Check System Preferences → Security & Privacy → FirewallCorrect IP address: If running on different machines, replace
127.0.0.1with actual IP# Find your IP ip addr show # Linux ifconfig # macOSSame realm: All nodes must have the same
realmin config
Problem: "Certificate validation failed"
Error:
{error, {tls_alert, "certificate unknown"}}Cause: Certificate mismatch (usually in manual cert mode)
Solution: Use {cert_mode, auto_generate} for development, or ensure all nodes trust the same CA.
Problem: High latency or packet loss
Check network conditions:
% Erlang
macula_connection:ping(<<"node2_id">>).
% Output:
{ok, 1.2} % RTT in millisecondsIf RTT > 100ms on localhost, check:
- System load (CPU usage)
- Other applications using network
- Docker/VM networking overhead
Next Steps
Congratulations! You've successfully:
- ✅ Built Macula from source
- ✅ Started a 3-node mesh network
- ✅ Verified mesh topology
- ✅ Sent pub/sub messages across the mesh
- ✅ Made RPC calls between nodes
- ✅ Tested fault tolerance and self-healing
Learn More
- Hello World Tutorial - Build a complete application
- RPC Guide - Complete RPC documentation
- PubSub Guide - Pub/Sub patterns
- Development Guide - Contributing to Macula
Try More Advanced Features
- Realm isolation: Start nodes in different realms and use gateways
- NAT traversal: Run nodes on different networks (home, cloud, mobile)
- Large mesh: Scale to 10+ nodes and observe routing behavior
- Persistence: Add event sourcing with persistent subscriptions
- Monitoring: Set up Prometheus metrics and Grafana dashboards
Join the Community
- GitHub: https://github.com/macula-io/macula
- Discord: https://discord.gg/macula
- Docs: https://docs.macula.io
Happy meshing! 🎉