Layr8 Elixir SDK
Elixir client for the Layr8 decentralized identity-native messaging network.
Full documentation at docs.layr8.io/build/elixir-sdk
Installation
Add to your mix.exs dependencies:
def deps do
[{:layr8, github: "layr8/elixir_sdk"}]
endRequires Elixir ~> 1.15.
Quick Start
{:ok, client} = Layr8.Client.start_link(%{
node_url: "wss://node.example.com/plugin_socket/websocket",
api_key: System.fetch_env!("LAYR8_API_KEY")
})
:ok = Layr8.Client.handle(client, "https://example.com/proto/1.0/request", fn msg ->
{:reply, %Layr8.Message{
type: "https://example.com/proto/1.0/response",
body: msg.body
}}
end)
:ok = Layr8.Client.connect(client)Configuration
Fields can be provided explicitly or resolved from environment variables:
| Field | Env Variable | Required | Description |
|---|---|---|---|
node_url | LAYR8_NODE_URL | Yes | WebSocket URL of the cloud-node |
api_key | LAYR8_API_KEY | Yes | Authentication key |
agent_did | LAYR8_AGENT_DID | No | Agent DID (ephemeral if omitted) |
HTTP(S) URLs are automatically normalized (https:// to wss://, http:// to ws://).
Message Handlers
Register handlers before calling connect/1. Each handler receives a Layr8.Message
and returns one of:
{:reply, message}— send a reply and mark as handled:noreply— mark as handled with no reply:pass— decline to handle; the cloud-node may route elsewhere{:error, reason}— signal an error to the cloud-node
:ok = Layr8.Client.handle(client, "https://example.com/proto/1.0/request", fn msg ->
{:reply, %Layr8.Message{type: "https://example.com/proto/1.0/response", body: %{"ok" => true}}}
end)
Reply messages auto-populate id, from, to, and thread_id from the inbound message.
Wildcard Handler
Register a catch-all handler for messages that don't match any specific type:
:ok = Layr8.Client.handle_all(client, fn msg ->
Logger.info("Unhandled message type: #{msg.type}")
:pass
end)Dispatch priority: specific handler > catch-all > auto-pass.
Sending Messages
# Fire-and-wait (default: waits for server ack)
:ok = Layr8.Client.send(client, %Layr8.Message{
type: "https://example.com/proto/1.0/request",
to: ["did:example:bob"],
body: %{"text" => "hello"}
})
# Fire-and-forget
:ok = Layr8.Client.send(client, msg, fire_and_forget: true)Request/Response
Send a message and block until a correlated response arrives (matched by thid):
{:ok, response} = Layr8.Client.request(client, %Layr8.Message{
type: "https://example.com/proto/1.0/request",
to: ["did:example:bob"],
body: %{"text" => "ping"}
}, timeout: 10_000)<<<<<<< HEAD
Configuration
Configuration can be provided explicitly or via environment variables:
| Field | Env Variable | Required | Description |
|---|---|---|---|
node_url | LAYR8_NODE_URL | Yes | WebSocket URL of the cloud-node |
api_key | LAYR8_API_KEY | Yes | Authentication key |
agent_did | LAYR8_AGENT_DID | No | Agent DID (ephemeral if omitted) |
HTTP(S) URLs are automatically normalized to WebSocket scheme:
https://→wss://http://→ws://
# All from environment variables
{:ok, client} = Layr8.Client.start_link(%{})
# Explicit values override env vars
{:ok, client} = Layr8.Client.start_link(%{
node_url: "wss://node.example.com/plugin_socket/websocket",
api_key: "my-api-key",
agent_did: "did:key:z6Mk..."
})Protocol Registration
The SDK automatically derives protocol base URIs from registered handler message types and sends them to the cloud-node on connect. For example, handling "https://example.com/proto/1.0/request" registers the protocol "https://example.com/proto/1.0".
Note: The cloud-node requires at least one protocol on join. Unlike the Node and Go SDKs, the Elixir SDK does not auto-add the problem report protocol. Sender-only clients that don't register any handlers will fail to connect. Register at least one handler before connecting.
Message Handlers
Handlers are registered before connect/1 and called when inbound DIDComm messages arrive.
Return Values
| Return value | Effect |
|---|---|
{:reply, message} | Send a response to the sender |
:noreply | No response; message consumed |
Manual Acknowledgment
By default messages are auto-acknowledged before the handler runs. For manual control:
Layr8.Client.handle(client, "https://example.com/proto/1.0/request", fn msg ->
# Do your work, then ack manually (coming in a future version)
:noreply
end, manual_ack: true)Request/Response Pattern
Use Layr8.Client.request/3 to send a message and wait for a correlated response.
Responses are matched by thid (thread ID).
Options: :timeout (default 30s), :parent_thread (sets pthid).
case Layr8.Client.request(client, msg, timeout: 10_000) do
{:ok, response} ->
IO.inspect(response.body)
# Raises on error:
# - Layr8.ProblemReportError — remote agent sent a problem report
# - Layr8.NotConnectedError — not connected
# - Layr8.Error — timeout or other error
endW3C Verifiable Credentials
Credential operations use the REST API (work without a WebSocket connection):
{:ok, jwt} = Layr8.Client.sign_credential(client, %{
"credentialSubject" => %{"id" => "did:example:bob", "name" => "Bob"}
}, issuer_did: "did:example:alice", format: "compact_jwt")
{:ok, verified} = Layr8.Client.verify_credential(client, jwt)
{:ok, stored} = Layr8.Client.store_credential(client, jwt)
{:ok, creds} = Layr8.Client.list_credentials(client)
{:ok, cred} = Layr8.Client.get_credential(client, stored["id"])W3C Verifiable Presentations
{:ok, vp_jwt} = Layr8.Client.sign_presentation(client, [vc_jwt],
nonce: "challenge-123", format: "compact_jwt")
{:ok, verified} = Layr8.Client.verify_presentation(client, vp_jwt)Connection Lifecycle
The cloud-node assigns an ephemeral DID on connect when no agent_did is
configured. Retrieve it with Layr8.Client.did/1.
The channel auto-reconnects with exponential backoff (1s to 30s). Subscribe to lifecycle events:
{:ok, client} = Layr8.Client.start_link(%{
on_disconnect: fn reason -> Logger.warning("Disconnected: #{inspect(reason)}") end,
on_reconnect: fn -> Logger.info("Reconnected") end
})Error Handling
All errors are exceptions under the Layr8 namespace:
| Exception | Raised when |
|---|---|
Layr8.Error | General SDK error (missing config, send failure) |
Layr8.ConnectionError | WebSocket connection fails |
Layr8.NotConnectedError | send/3 or request/3 called before connect/1 |
Layr8.AlreadyConnectedError | handle/4 called after connect/1 |
Layr8.ClientClosedError | connect/1 called after close/1 |
Layr8.ProblemReportError | Remote agent sends a DIDComm problem report |
Examples
See examples/echo_agent.ex for a standalone echo agent.
Development
mix deps.get
mix test
mix check # format + compile warnings + test
mix docs # generate ExDoc documentationArchitecture
Layr8.Client (GenServer)
Layr8.Config -- config resolution and URL normalization
Layr8.Handler -- message type -> handler registry
Layr8.Message -- DIDComm v2 message struct + marshal/parse
Layr8.Attachment -- DIDComm v2 attachment struct
Layr8.Channel -- Phoenix Channel WebSocket transport (GenServer + WebSockex)
Layr8.REST -- HTTP client for REST API (Req)
Layr8.Credentials -- W3C Verifiable Credential operations
Layr8.Presentations -- W3C Verifiable Presentation operationsLinks
License
MIT