Packages
macula
12.0.0
12.1.0
12.0.0
11.5.0
11.4.0
11.3.1
11.3.0
11.2.0
11.1.0
11.0.0
10.25.0
10.24.0
10.23.0
10.22.0
10.21.0
10.20.3
10.20.2
10.20.0
10.19.2
10.19.1
10.19.0
10.18.0
10.17.0
10.16.0
10.15.0
10.14.5
10.14.4
10.14.2
10.14.1
10.14.0
10.13.2
10.13.1
10.11.0
10.10.2
10.10.1
10.10.0
10.9.1
10.9.0
10.8.0
10.7.0
10.5.8
10.5.7
10.5.6
10.5.5
10.5.4
10.5.3
10.5.2
10.5.1
10.5.0
10.4.0
10.2.0
10.1.1
10.1.0
10.0.2
10.0.1
10.0.0
9.13.8
9.8.2
9.8.1
9.8.0
9.5.0
9.4.0
9.3.1
9.3.0
9.2.0
9.1.1
9.1.0
9.0.0
8.7.0
8.6.0
8.5.0
8.4.1
8.4.0
8.3.0
8.2.0
8.1.0
8.0.2
8.0.1
8.0.0
7.1.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
README.md
# Macula SDK
[](LICENSE)
[](https://www.erlang.org)
[](https://hex.pm/packages/macula)
[](https://github.com/sponsors/rgfaber)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/macula-full-dark.svg">
<img src="assets/macula-full-light.svg" alt="Macula" width="320">
</picture>
</p>
<p align="center">
<strong>Erlang/OTP client SDK for the Macula HTTP/3 mesh</strong>
</p>
---
> **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`](https://crates.io/crates/macula-pqc) crate. Every
> signature is ML-DSA-87 on
> [`macula-mldsa`](https://crates.io/crates/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](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:
> - **RPC** — `macula_request`/`macula_response`, unary call/reply.
> - **Pub/Sub** — `macula_publisher`/`macula_subscriber`, publish and
> per-publisher-ordered subscribe.
> - **Content sharing** — `macula_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 RPC** — `macula_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 transfer** — `macula_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`.
> - **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](docs/guides/overlay/HYPARVIEW_GUIDE.md) and
> [Plumtree](docs/guides/overlay/PLUMTREE_GUIDE.md) guides.
>
> See [CHANGELOG.md](CHANGELOG.md) for the full version-by-version history.
## What is Macula?
<p align="center">
<img src="assets/sdk_architecture.svg" alt="Macula SDK Component and Feature Model" width="100%">
</p>
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 the provider's authorization
checked against the realm-signed org directory.
- **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 mesh** — `net_adm:ping` across firewalls, no VPN.
- **Identity** — ML-DSA-87 node keys (with an RSA-PSS half under `pq_hybrid`), and UCAN tokens they sign.
- **MRI** — typed, hierarchical resource identifiers.
- **Zero-config LAN clustering** — UDP-multicast gossip.
The station (routing, DHT, SWIM, peering) is a separate repo,
[macula-station](https://github.com/macula-io/macula-station); this package is
the client you build against.
---
## Quick Start
Add to `rebar.config`:
```erlang
{deps, [{macula, "~> 12.0"}]}.
```
Or in Elixir `mix.exs`:
```elixir
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.
<p align="center">
<img src="assets/connect_flow.svg" alt="SDK Connect Flow" width="100%">
</p>
```erlang
%% 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)
<p align="center">
<img src="assets/identity_crypto.svg" alt="Identity and Crypto Stack" width="100%">
</p>
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`](https://crates.io/crates/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.
```erlang
{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](docs/guides/shared/AUTHORIZATION_GUIDE.md)).
---
## Documentation
| Guide | Description |
|-------|-------------|
| [Connecting](docs/guides/shared/CONNECTING_GUIDE.md) | Pools, seeds, expected identities, reconnection |
| [PubSub Guide](docs/guides/pubsub/PUBSUB_GUIDE.md) | Fan-out + per-publisher delivery ordering |
| [PubSub Protocol](docs/guides/pubsub/PUBSUB_PROTOCOL.md) | Raw `subscribe`/`publish` primitives |
| [Topic Naming](docs/guides/shared/TOPIC_NAMING_GUIDE.md) | Event-type topics, IDs in payloads |
| [RPC Guide](docs/guides/rpc/RPC_GUIDE.md) | Direct-dial request/response |
| [RPC Protocol](docs/guides/rpc/RPC_PROTOCOL.md) | Raw `advertise`/`call` primitives, error codes |
| [Content Guide](docs/guides/content/CONTENT_GUIDE.md) | Content-addressed blobs (MCID), push/upload |
| [Content Protocol](docs/guides/content/CONTENT_PROTOCOL.md) | Raw `put_content`/`get_content`, MCID format, discovery |
| [Records Guide](docs/guides/shared/RECORDS_GUIDE.md) | Signed, TTL'd facts in the DHT — your own record types |
| [Streaming Guide](docs/guides/streaming/STREAMING_GUIDE.md) | Streaming RPC (server / client / bidi) |
| [Streaming Protocol](docs/guides/streaming/STREAMING_PROTOCOL.md) | Raw `call_stream`/`advertise_stream` primitives |
| [HyParView Guide](docs/guides/overlay/HYPARVIEW_GUIDE.md) | Bounded partial-view realm membership |
| [Plumtree Guide](docs/guides/overlay/PLUMTREE_GUIDE.md) | Epidemic broadcast trees, realm PubSub, OR-Set CRDT |
| [Distribution Over Mesh](docs/guides/DIST_OVER_MESH_GUIDE.md) | Erlang dist through the mesh |
| [Clustering](docs/guides/CLUSTERING_GUIDE.md) | LAN gossip clustering |
| [Authorization](docs/guides/shared/AUTHORIZATION_GUIDE.md) | Node keys, UCAN, provider authorization |
| [MRI Guide](docs/guides/shared/MRI_GUIDE.md) | Resource identifiers |
| [Development](docs/guides/DEVELOPMENT.md) | Building and testing |
| [Glossary](docs/GLOSSARY.md) | Terminology |
The station server lives in
[macula-station](https://github.com/macula-io/macula-station).
---
## Related Projects
| Project | Description |
|---------|-------------|
| [macula-station](https://github.com/macula-io/macula-station) | The station: DHT, SWIM, routing, peering |
| [macula-realm](https://github.com/macula-io/macula-realm) | Managed-realm identity + certificate authority |
| [macula-mri-khepri](https://github.com/macula-io/macula-mri-khepri) | Distributed MRI persistence (Khepri/Raft) |
| [macula-ecosystem](https://github.com/macula-io/macula-ecosystem) | Documentation hub |
---
## License
Apache 2.0 — see [LICENSE](LICENSE).
---
<p align="center">
<sub>Built with the BEAM</sub>
</p>