macula-io/macula

A sovereign peer-to-peer mesh for the BEAM: QUIC transport, DHT discovery, RPC and pubsub, no cloud in the data path

15

stars

814

commits

Erlang

primary language

Sep 10, 2026

updated

README

Macula SDK

License BEAM Hex.pm GitHub Sponsors

Macula

Erlang/OTP client SDK for the Macula HTTP/3 mesh


Since 10.5.0: every supervised primitive pair is complete and symmetric, each wrapping its raw SDK primitive as an OTP behaviour with a simple_one_for_one factory supervisor, mesh-visible protocol facts (sharing.*_v1, streaming.*_v1, rpc.*_v1) around its own side of the operation, and both a pooled and a direct-dial (resolve + one-hop dial) mode:

  • RPCmacula_request/macula_response, unary call/reply.
  • Pub/Submacula_publisher/macula_subscriber, publish and per-publisher-ordered subscribe.
  • Content sharingmacula_feeder/macula_download, built on the addressable macula_content_transfer primitive: a genuinely peer-visible cancel (a real QUIC RESET_STREAM, not a local kill), pause/resume between chunks, and parallel multi-stream chunk transfer.
  • Streaming RPCmacula_streamer/macula_stream_sink, server / client / bidi modes, with an optional client_stream receive loop and terminal-reply callback, and abort-wired cancel.
  • Push-initiated content transfermacula_pusher/macula_upload push a file at a specific, already-known recipient (rather than into content-addressed storage for someone to discover and pull later), with the same chunk/hash/verify integrity guarantees, over client_stream.
  • NEW: overlay (HyParView + Plumtree) — realm-scoped bounded partial views and epidemic broadcast trees, absorbed from the standalone macula-hyparview/macula-plumtree packages. No supervised wrapper yet — see the HyParView and Plumtree guides.

All additive since 9.2.0, no breaking changes. See CHANGELOG.md for the full version-by-version history.

What is Macula?

Macula SDK Component and Feature Model

Macula is an Erlang/OTP client SDK for building applications on a mesh of stations — realm-agnostic relays that route over QUIC (HTTP/3) and form a Kademlia DHT. Your service or daemon connects outbound to one or more stations: no open ports, NAT-friendly, no VPN. It provides:

  • RPC (request/response) — discover a provider in the DHT, then dial its serving station directly (one hop), with optional realm-CA trust verification.
  • Pub/Sub — topic-based event fan-out across stations, with per-publisher ordered delivery.
  • Content — content-addressed sharing and live streaming (MCID).
  • DHT records — signed, TTL'd records (advertisements, endpoints, more).
  • Erlang distribution over meshnet_adm:ping across firewalls, no VPN.
  • Identity — Ed25519 keypairs, UCAN tokens, DID documents (NIF-accelerated).
  • MRI — typed, hierarchical resource identifiers.
  • Zero-config LAN clustering — UDP-multicast gossip.

The station (routing, DHT, SWIM, peering) is a separate repo, macula-station; this package is the client you build against.


Quick Start

Add to rebar.config:

{deps, [{macula, "~> 10.5"}]}.

Or in Elixir mix.exs:

defp deps do
  [{:macula, "~> 10.5"}]
end

SDK Connect Flow

application:ensure_all_started(macula),

%% Connect a pool to one or more stations (seed URLs). The pool owns one
%% QUIC link per seed, reconnecting and replaying subscriptions as needed.
{ok, Pool} = macula:connect([<<"quic://boot.macula.io:443">>], #{}),

%% A realm is a 32-byte tag derived from a name; it scopes every call.
%% Keep the name around too — topics are built from it, not the tag.
RealmName = <<"io.example.myapp">>,
Realm     = macula_realm:id(RealmName),

%% Topics/procedures are built via macula_topic, never hand-typed — a
%% typo becomes a wrong VALUE your own tests catch, not two strings
%% silently drifting apart. Facts (pub/sub) are past tense; hopes (RPC)
%% are present tense. See docs/guides/shared/TOPIC_NAMING_GUIDE.md.
Topic     = macula_topic:app_fact(RealmName, <<"example">>, <<"myapp">>,
                                  <<"sensors">>, <<"temperature_measured">>, 1),
Procedure = macula_topic:app_hope(RealmName, <<"example">>, <<"myapp">>,
                                  <<"math">>, <<"add">>, 1),

%% Subscribe (delivers {macula_event, Ref, Topic, Payload, Meta} to a pid),
{ok, Ref} = macula:subscribe(Pool, Realm, Topic, self()),

%% or subscribe with a callback fun(Topic, Payload, Meta):
{ok, Ref2} = macula:subscribe_callback(
    Pool, Realm, Topic,
    fun(_Topic, Payload, _Meta) -> io:format("~p~n", [Payload]) end),

%% Publish. Entity IDs go in the PAYLOAD, never in the topic.
ok = macula:publish(Pool, Realm, Topic,
                    #{sensor => <<"kitchen">>, value => 23.5}),

%% Advertise an RPC procedure (open to any identified caller here),
ok = macula:advertise(Pool, Realm, Procedure,
                      fun(#{<<"a">> := A, <<"b">> := B}) -> {ok, A + B} end,
                      #{}),

%% Call it — the SDK resolves the provider and dials its station directly.
{ok, 5} = macula:call(Pool, Realm, Procedure,
                      #{<<"a">> => 2, <<"b">> => 3}, 5_000).

Identity and Crypto (NIF-accelerated)

Identity and Crypto Stack

Rust NIFs with pure-Erlang fallbacks:

%% Ed25519 keypair (a #{public := _, private := _} map)
KP  = macula_identity:generate(),
Sig = macula_identity:sign(<<"hello">>, KP),
true = macula_identity:verify(<<"hello">>, Sig, macula_identity:public(KP)),

%% BLAKE3 hashing
Hash = macula_blake3_nif:hash(<<"hello">>),

%% UCAN capability tokens — Issuer/Audience are DIDs (binaries), not raw keys
Issuer   = <<"did:macula:io.example.myapp">>,
Audience = <<"did:macula:io.example.otherapp">>,
Caps     = [#{with => <<"mri:sensor:io.example.myapp/kitchen">>,
              can  => <<"read">>}],
{ok, Token}   = macula_ucan_nif:create(Issuer, Audience, Caps,
                                       macula_identity:private(KP)),
{ok, Payload} = macula_ucan_nif:verify(Token, macula_identity:public(KP)).

Documentation

GuideDescription
ConnectingPools, seeds, TLS policy, reconnection
PubSub GuideFan-out + per-publisher delivery ordering
PubSub ProtocolRaw subscribe/publish primitives
Topic NamingEvent-type topics, IDs in payloads
RPC GuideDirect-dial request/response
RPC ProtocolRaw advertise/call primitives, error codes
Content GuideContent-addressed blobs (MCID), push/upload
Content ProtocolRaw put_content/get_content, MCID format, discovery
Records GuideSigned, TTL'd facts in the DHT — your own record types
Streaming GuideStreaming RPC (server / client / bidi)
Streaming ProtocolRaw call_stream/advertise_stream primitives
HyParView GuideBounded partial-view realm membership
Plumtree GuideEpidemic broadcast trees, realm PubSub, OR-Set CRDT
Distribution Over MeshErlang dist through the mesh
ClusteringLAN gossip clustering
AuthorizationDID / UCAN / cert-chain trust
MRI GuideResource identifiers
DevelopmentBuilding and testing
GlossaryTerminology

The station server lives in macula-station.


ProjectDescription
macula-stationThe station: DHT, SWIM, routing, peering
macula-realmManaged-realm identity + certificate authority
macula-mri-khepriDistributed MRI persistence (Khepri/Raft)
macula-ecosystemDocumentation hub

License

Apache 2.0 — see LICENSE.


Built with the BEAM

Contributors

rgfaber

814 commits

macula-io/macula

A sovereign peer-to-peer mesh for the BEAM: QUIC transport, DHT discovery, RPC and pubsub, no cloud in the data path

15

stars

814

commits

Erlang

primary language

Sep 10, 2026

updated

README

Macula SDK

License BEAM Hex.pm GitHub Sponsors

Macula

Erlang/OTP client SDK for the Macula HTTP/3 mesh


Since 10.5.0: every supervised primitive pair is complete and symmetric, each wrapping its raw SDK primitive as an OTP behaviour with a simple_one_for_one factory supervisor, mesh-visible protocol facts (sharing.*_v1, streaming.*_v1, rpc.*_v1) around its own side of the operation, and both a pooled and a direct-dial (resolve + one-hop dial) mode:

  • RPCmacula_request/macula_response, unary call/reply.
  • Pub/Submacula_publisher/macula_subscriber, publish and per-publisher-ordered subscribe.
  • Content sharingmacula_feeder/macula_download, built on the addressable macula_content_transfer primitive: a genuinely peer-visible cancel (a real QUIC RESET_STREAM, not a local kill), pause/resume between chunks, and parallel multi-stream chunk transfer.
  • Streaming RPCmacula_streamer/macula_stream_sink, server / client / bidi modes, with an optional client_stream receive loop and terminal-reply callback, and abort-wired cancel.
  • Push-initiated content transfermacula_pusher/macula_upload push a file at a specific, already-known recipient (rather than into content-addressed storage for someone to discover and pull later), with the same chunk/hash/verify integrity guarantees, over client_stream.
  • NEW: overlay (HyParView + Plumtree) — realm-scoped bounded partial views and epidemic broadcast trees, absorbed from the standalone macula-hyparview/macula-plumtree packages. No supervised wrapper yet — see the HyParView and Plumtree guides.

All additive since 9.2.0, no breaking changes. See CHANGELOG.md for the full version-by-version history.

What is Macula?

Macula SDK Component and Feature Model

Macula is an Erlang/OTP client SDK for building applications on a mesh of stations — realm-agnostic relays that route over QUIC (HTTP/3) and form a Kademlia DHT. Your service or daemon connects outbound to one or more stations: no open ports, NAT-friendly, no VPN. It provides:

  • RPC (request/response) — discover a provider in the DHT, then dial its serving station directly (one hop), with optional realm-CA trust verification.
  • Pub/Sub — topic-based event fan-out across stations, with per-publisher ordered delivery.
  • Content — content-addressed sharing and live streaming (MCID).
  • DHT records — signed, TTL'd records (advertisements, endpoints, more).
  • Erlang distribution over meshnet_adm:ping across firewalls, no VPN.
  • Identity — Ed25519 keypairs, UCAN tokens, DID documents (NIF-accelerated).
  • MRI — typed, hierarchical resource identifiers.
  • Zero-config LAN clustering — UDP-multicast gossip.

The station (routing, DHT, SWIM, peering) is a separate repo, macula-station; this package is the client you build against.


Quick Start

Add to rebar.config:

{deps, [{macula, "~> 10.5"}]}.

Or in Elixir mix.exs:

defp deps do
  [{:macula, "~> 10.5"}]
end

SDK Connect Flow

application:ensure_all_started(macula),

%% Connect a pool to one or more stations (seed URLs). The pool owns one
%% QUIC link per seed, reconnecting and replaying subscriptions as needed.
{ok, Pool} = macula:connect([<<"quic://boot.macula.io:443">>], #{}),

%% A realm is a 32-byte tag derived from a name; it scopes every call.
%% Keep the name around too — topics are built from it, not the tag.
RealmName = <<"io.example.myapp">>,
Realm     = macula_realm:id(RealmName),

%% Topics/procedures are built via macula_topic, never hand-typed — a
%% typo becomes a wrong VALUE your own tests catch, not two strings
%% silently drifting apart. Facts (pub/sub) are past tense; hopes (RPC)
%% are present tense. See docs/guides/shared/TOPIC_NAMING_GUIDE.md.
Topic     = macula_topic:app_fact(RealmName, <<"example">>, <<"myapp">>,
                                  <<"sensors">>, <<"temperature_measured">>, 1),
Procedure = macula_topic:app_hope(RealmName, <<"example">>, <<"myapp">>,
                                  <<"math">>, <<"add">>, 1),

%% Subscribe (delivers {macula_event, Ref, Topic, Payload, Meta} to a pid),
{ok, Ref} = macula:subscribe(Pool, Realm, Topic, self()),

%% or subscribe with a callback fun(Topic, Payload, Meta):
{ok, Ref2} = macula:subscribe_callback(
    Pool, Realm, Topic,
    fun(_Topic, Payload, _Meta) -> io:format("~p~n", [Payload]) end),

%% Publish. Entity IDs go in the PAYLOAD, never in the topic.
ok = macula:publish(Pool, Realm, Topic,
                    #{sensor => <<"kitchen">>, value => 23.5}),

%% Advertise an RPC procedure (open to any identified caller here),
ok = macula:advertise(Pool, Realm, Procedure,
                      fun(#{<<"a">> := A, <<"b">> := B}) -> {ok, A + B} end,
                      #{}),

%% Call it — the SDK resolves the provider and dials its station directly.
{ok, 5} = macula:call(Pool, Realm, Procedure,
                      #{<<"a">> => 2, <<"b">> => 3}, 5_000).

Identity and Crypto (NIF-accelerated)

Identity and Crypto Stack

Rust NIFs with pure-Erlang fallbacks:

%% Ed25519 keypair (a #{public := _, private := _} map)
KP  = macula_identity:generate(),
Sig = macula_identity:sign(<<"hello">>, KP),
true = macula_identity:verify(<<"hello">>, Sig, macula_identity:public(KP)),

%% BLAKE3 hashing
Hash = macula_blake3_nif:hash(<<"hello">>),

%% UCAN capability tokens — Issuer/Audience are DIDs (binaries), not raw keys
Issuer   = <<"did:macula:io.example.myapp">>,
Audience = <<"did:macula:io.example.otherapp">>,
Caps     = [#{with => <<"mri:sensor:io.example.myapp/kitchen">>,
              can  => <<"read">>}],
{ok, Token}   = macula_ucan_nif:create(Issuer, Audience, Caps,
                                       macula_identity:private(KP)),
{ok, Payload} = macula_ucan_nif:verify(Token, macula_identity:public(KP)).

Documentation

GuideDescription
ConnectingPools, seeds, TLS policy, reconnection
PubSub GuideFan-out + per-publisher delivery ordering
PubSub ProtocolRaw subscribe/publish primitives
Topic NamingEvent-type topics, IDs in payloads
RPC GuideDirect-dial request/response
RPC ProtocolRaw advertise/call primitives, error codes
Content GuideContent-addressed blobs (MCID), push/upload
Content ProtocolRaw put_content/get_content, MCID format, discovery
Records GuideSigned, TTL'd facts in the DHT — your own record types
Streaming GuideStreaming RPC (server / client / bidi)
Streaming ProtocolRaw call_stream/advertise_stream primitives
HyParView GuideBounded partial-view realm membership
Plumtree GuideEpidemic broadcast trees, realm PubSub, OR-Set CRDT
Distribution Over MeshErlang dist through the mesh
ClusteringLAN gossip clustering
AuthorizationDID / UCAN / cert-chain trust
MRI GuideResource identifiers
DevelopmentBuilding and testing
GlossaryTerminology

The station server lives in macula-station.


ProjectDescription
macula-stationThe station: DHT, SWIM, routing, peering
macula-realmManaged-realm identity + certificate authority
macula-mri-khepriDistributed MRI persistence (Khepri/Raft)
macula-ecosystemDocumentation hub

License

Apache 2.0 — see LICENSE.


Built with the BEAM

Contributors

rgfaber

814 commits

Languages

Erlang

91.9%

Rust

5.4%

Shell

2.5%