Macula SDK

License BEAM Hex.pm GitHub Sponsors

Macula

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


12.0.0: post-quantum key exchange AND post-quantum signatures. Every QUIC link negotiates SecP384r1MLKEM1024, then SecP256r1MLKEM768, and nothing classical, from the macula-pqc crate. Every signature is ML-DSA-87 on macula-mldsa: node keys, UCAN tokens, and the self-signed certificate a listener presents, which a dial verifies and nothing classical can replace. A station dial is bound end to end: the station's identity key signs a binding over its TLS key, the client checks it against the certificate that handshake received, and the CONNECT proof covers the same certificate.

Erlang distribution over QUIC is the exception: those dials run no connection handshake yet, so they verify that the peer holds its certificate's key and nothing about who it is.

Breaking on the wire: a node on 11.5.0 or earlier cannot connect to this version, in either direction. See CHANGELOG.md.

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:

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:

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, "~> 12.0"}]}.

Or in Elixir mix.exs:

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

12.0.0 breaks on the wire: a node on 11.5.0 or earlier cannot connect to it, in either direction, and there is no classical fallback for either key exchange or authentication. Upgrade every node together.

SDK Connect Flow

%% Every node runs one post-quantum crypto profile, pq_pure
%% or pq_hybrid. The application refuses to start without one.
ok = application:set_env(macula, crypto_profile, pq_pure),
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

A node holds one key per purpose in its crypto profile. In pq_pure a key is ML-DSA-87; in pq_hybrid an identity key pairs ML-DSA-87 with RSA-PSS and signs the IETF LAMPS composite id-MLDSA87-RSA4096-PSS-SHA512. ML-DSA is macula-mldsa, verified against NIST's ACVP vectors, in a Rust NIF with no Erlang fallback, and new keys are stored as their 32-byte seed. The node_id is SHA-256 over the identity key.

{ok, Key} = macula_node_keys:generate(identity, pq_pure),
{ok, NodeId} = macula_node_keys:node_id(Key),
Sig = macula_node_keys:sign(<<"hello">>, Key),
true = macula_node_keys:verify(<<"hello">>, Sig, macula_node_keys:public_key(Key), pq_pure),
ok = macula_node_keys:save("identity.key", Key),
%% BLAKE3 hashing
Hash = macula_blake3_nif:hash(<<"hello">>).

UCAN capability tokens (macula_ucan) are signed by node keys too, with the profile's alg: ML-DSA-87 in pq_pure and ML-DSA-87-PS384, the LAMPS composite, in pq_hybrid. A token names its issuer by did:key and its audience by node_id, and an EdDSA token is refused (see the Authorization guide).


Documentation

Guide Description
Connecting Pools, seeds, expected identities, reconnection
PubSub Guide Fan-out + per-publisher delivery ordering
PubSub Protocol Raw subscribe/publish primitives
Topic Naming Event-type topics, IDs in payloads
RPC Guide Direct-dial request/response
RPC Protocol Raw advertise/call primitives, error codes
Content Guide Content-addressed blobs (MCID), push/upload
Content Protocol Raw put_content/get_content, MCID format, discovery
Records Guide Signed, TTL'd facts in the DHT — your own record types
Streaming Guide Streaming RPC (server / client / bidi)
Streaming Protocol Raw call_stream/advertise_stream primitives
HyParView Guide Bounded partial-view realm membership
Plumtree Guide Epidemic broadcast trees, realm PubSub, OR-Set CRDT
Distribution Over Mesh Erlang dist through the mesh
Clustering LAN gossip clustering
Authorization Node keys, UCAN, provider authorization
MRI Guide Resource identifiers
Development Building and testing
Glossary Terminology

The station server lives in macula-station.


Project Description
macula-station The station: DHT, SWIM, routing, peering
macula-realm Managed-realm identity + certificate authority
macula-mri-khepri Distributed MRI persistence (Khepri/Raft)
macula-ecosystem Documentation hub

License

Apache 2.0 — see LICENSE.


Built with the BEAM