Development Guide
View SourceThis guide covers setting up a development environment for contributing to Macula.
Prerequisites
- Erlang/OTP 28 (pinned in
.tool-versions) - Installation Guide - Rebar3 - Erlang build tool (Installation)
- Git - Version control
- Docker (optional) -
Dockerfile/Dockerfile.gatewaybuild deployment images; not required for local development orrebar3 eunit
Quick Setup
# Clone the repository
git clone https://github.com/macula-io/macula.git
cd macula
# Fetch dependencies
rebar3 get-deps
# Compile
rebar3 compile
# Run tests
rebar3 eunit
# Start a shell with Macula loaded
rebar3 shell
Project Structure
Macula SDK is a single Erlang/OTP library, vertical-sliced by capability
rather than by technical layer — most src/ subdirectories are one small
feature, not a horizontal services//utils/ split:
macula/
├── src/
│ ├── macula.erl # Public facade (connect, subscribe, publish, call, advertise, ...)
│ ├── macula_topic.erl # Topic/procedure naming builders (realm/org/app tiers)
│ ├── macula_realm.erl # Realm tag derivation
│ ├── macula_id.erl, macula_names.erl, macula_node.erl, macula_time.erl, macula_uri.erl, macula_cache.erl
│ │ # Small top-level utility modules
│ ├── client/ # macula_client (pool), macula_station_link (per-seed worker)
│ ├── peering/ # QUIC transport, wire frames, BOLT#4 error taxonomy, peer state machine
│ ├── pubsub/ # Pub/sub delivery ordering
│ ├── record/ # Signed DHT records (macula_record), CBOR codec
│ ├── content/ # Content chunking/manifests (macula_manifest)
│ ├── mri/ # Resource identifiers — parse, hierarchy, trie index
│ ├── identity/, auth/ # Ed25519 keys, UCAN tokens (Rust NIFs + Erlang fallback)
│ ├── macula_cert_system/ # Self-sovereign certs, trust store
│ ├── macula_dist_system/ # Erlang distribution over relay mesh (3 transports)
│ ├── macula_cluster_system/ # LAN clustering (gossip/static/libcluster) — separate from dist
│ └── (advertise_station/, host_identity/, resolve_address/, ...)
│ # Smaller single-purpose slices — one module or two each
├── native/ # Rust NIF crates (macula_quic, macula_crypto_nif, macula_ucan_nif,
│ # macula_did_nif, macula_mri_nif, macula_cbor_nif, macula_tun_nif)
├── test/ # EUnit tests, one file per module under test
├── include/ # Header files (.hrl)
├── docs/ # SDK guides
├── priv/ # build-nifs.sh, precompiled .so fallbacks
└── rebar.config # Build configurationServer modules (gateway, DHT routing, RPC/PubSub routing, SWIM, peering, etc.) live in macula-station.
Running Tests
All Tests
rebar3 eunit
Specific Module Tests
rebar3 eunit --module=macula_mri_tests
rebar3 eunit --module=macula_crypto_nif_tests
Test Coverage
rebar3 do eunit, cover
Test files live under test/, plus test/macula_dist_system/ and
test/macula_cluster_system/ — both directories are registered in
rebar.config's eunit_opts so a plain rebar3 eunit covers them too.
Code Quality Standards
Macula follows Idiomatic Erlang principles:
Core Principles
- ✅ Pattern matching on function heads - Avoid
ifandcond - ✅ Guards instead of case - Use guards for simple conditions
- ✅ Shallow nesting - Keep nesting to 1-2 levels maximum
- ✅ Let it crash - Don't catch errors unless you can handle them meaningfully
- ✅ OTP behaviors - Use gen_server, gen_statem, supervisor where appropriate
Example: Good vs. Bad
❌ Bad:
process_message(Msg, State) ->
if
is_binary(Msg) ->
case decode_message(Msg) of
{ok, Data} ->
if
Data#data.type == request ->
handle_request(Data, State);
Data#data.type == response ->
handle_response(Data, State)
end
end
end.✅ Good:
%% Guard ensures binary input
process_message(Msg, State) when is_binary(Msg) ->
case decode_message(Msg) of
{ok, Data} -> handle_decoded_message(Data, State);
{error, Reason} -> {error, Reason}
end;
process_message(_Msg, _State) ->
{error, invalid_message}.
%% Pattern match on data type
handle_decoded_message(#data{type = request} = Data, State) ->
handle_request(Data, State);
handle_decoded_message(#data{type = response} = Data, State) ->
handle_response(Data, State).See CLAUDE.md (at repository root) for complete coding guidelines.
Building Documentation
Macula uses ex_doc for documentation generation:
rebar3 ex_doc
Generated docs appear in doc/ directory. Open doc/index.html in a browser.
Rust NIFs
The SDK includes Rust NIFs for performance-critical operations. They build automatically via priv/build-nifs.sh during rebar3 compile. Requires a Rust toolchain (cargo). If Rust is not available, pure Erlang fallbacks are used.
| NIF Crate | Provides |
|---|---|
native/macula_quic/ | Quinn QUIC transport (precompiled download available) |
native/macula_crypto_nif/ | Ed25519, BLAKE3, SHA-256 |
native/macula_ucan_nif/ | UCAN token create/verify |
native/macula_did_nif/ | DID document operations |
native/macula_mri_nif/ | MRI parsing, trie index |
native/macula_cbor_nif/ | CBOR encode/decode |
native/macula_tun_nif/ | TUN device I/O |
Contributing Workflow
Read the documentation
- README.md (at repository root) - Project overview
- CLAUDE.md (at repository root) - Coding guidelines
Create a feature branch
git checkout -b feature/your-feature-nameWrite tests first (TDD approach preferred)
# Create test file touch test/macula_your_module_tests.erl # Write failing tests # Implement functionality # Verify tests pass rebar3 eunit --module=macula_your_module_testsFollow code quality standards
- Pattern matching over conditionals
- Guards instead of case where possible
- Maximum 1-2 levels of nesting
- Comprehensive tests for new functionality
Commit and push
git add . git commit -m "Add feature: your feature description" git push origin feature/your-feature-nameCreate pull request
- Describe what the PR does
- Reference any related issues
- Ensure all tests pass
- Follow PR template guidelines
Getting Help
- Issues: GitHub Issues
- Documentation:
docs/guides/— see the README's guide table for the full index