A minimal but fully-working Holochain “email” hApp that lets two agents exchange end-to-end encrypted messages with no central server.
It is split into four parts:
- DNA bundle (zomes + integrity)
- UI (vanilla JS / Web-Components, works in any browser once you have the Holochain Launcher)
- Packaging & running
- Next steps / hardening checklist
You can copy-paste everything into a new folder and have a chat-like “decentralised email” running on your laptop in ≈ 10 min.
---
manifest_version: "1"
name: holo_mail
integrity:
zomes:
- name: mail_integrity
bundled: ../target/wasm32-unknown-unknown/release/mail_integrity.wasm
coordinator:
zomes:
- name: mail
bundled: ../target/wasm32-unknown-unknown/release/mail.wasm
dependencies:
- name: mail_integrityuse hdi::prelude::*;
#[hdk_entry_defs]
#[unit_enum(UnitEntryTypes)]
pub enum EntryTypes {
#[entry_def(required_validations = 5)]
Message(Message),
}
#[hdk_entry_helper]
#[serde(rename_all = "camelCase")]
pub struct Message {
pub to: AgentPubKey,
pub cipher_text: String, // base64 encrypted body
pub nonce: String, // base64 nonce
pub created_at: Timestamp,
}
#[hdk_link_types]
pub enum LinkTypes {
Sent,
Inbox,
}use hdk::prelude::*;
use mail_integrity::*;
#[hdk_extern]
pub fn send_message(input: SendMessageInput) -> ExternResult<ActionHash> {
let message = Message {
to: input.to,
cipher_text: input.cipher_text,
nonce: input.nonce,
created_at: sys_time()?,
};
let ah = create_entry(&EntryTypes::Message(message.clone()))?;
// link from my “sent” list
create_link(
agent_info()?.agent_latest_pubkey,
ah.clone(),
LinkTypes::Sent,
(),
)?;
// link to recipient’s “inbox”
create_link(
input.to,
ah.clone(),
LinkTypes::Inbox,
(),
)?;
Ok(ah)
}
#[hdk_extern]
pub fn get_inbox(_: ()) -> ExternResult<Vec<Record>> {
let links = get_links(
agent_info()?.agent_latest_pubkey,
LinkTypes::Inbox,
None,
)?;
let records = links
.into_iter()
.filter_map(|l| get(ActionHash::from(l.target), GetOptions::default()).ok()?)
.collect();
Ok(records)
}
#[hdk_extern]
pub fn get_sent(_: ()) -> ExternResult<Vec<Record>> {
let links = get_links(
agent_info()?.agent_latest_pubkey,
LinkTypes::Sent,
None,
)?;
let records = links
.into_iter()
.filter_map(|l| get(ActionHash::from(l.target), GetOptions::default()).ok()?)
.collect();
Ok(records)
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SendMessageInput {
to: AgentPubKey,
cipher_text: String,
nonce: String,
}<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Holochain Mail</title>
<script type="module">
import { AppWebsocket, encodeHashToBase64 } from
'https://unpkg.com/@holochain/client@0.16.0/dist/index.es.js';
import * as nacl from
'https://unpkg.com/tweetnacl@1.0.3/nacl-fast.js';
const client = await AppWebsocket.connect();
const appInfo = await client.appInfo();
const cell = appInfo.cell_info.mails[0].provisioned;
// ----- helpers -----
const b642buf = b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const buf2b64 = buf => btoa(String.fromCharCode(...buf));
document.getElementById('sendBtn').addEventListener('click', async () => {
const toStr = document.getElementById('to').value.trim();
const plaintext = document.getElementById('body').value.trim();
const to = { type: 'AgentPubKey', data: b642buf(toStr) };
// ephemeral keypair for E2E
const boxKp = nacl.box.keyPair();
const nonce = nacl.randomBytes(24);
const cipher = nacl.box(
new TextEncoder().encode(plaintext),
nonce,
b642buf(toStr),
boxKp.secretKey
);
await client.callZome({
cell_id: cell.cell_id,
zome_name: 'mail',
fn_name: 'send_message',
payload: {
to,
cipher_text: buf2b64(cipher),
nonce: buf2b64(nonce),
}
});
alert('sent!');
});
async function refresh() {
const inbox = await client.callZome({
cell_id: cell.cell_id,
zome_name: 'mail',
fn_name: 'get_inbox',
payload: null
});
const list = document.getElementById('inbox');
list.innerHTML = '';
inbox.forEach(r => {
const li = document.createElement('li');
li.textContent = JSON.stringify(r.entry);
list.appendChild(li);
});
}
setInterval(refresh, 5000);
</script>
</head>
<body>
<h1>Holochain Mail</h1>
<label>To (AgentPubKey base64): <input id="to" size="64"/></label><br/>
<label>Message:<br/><textarea id="body" rows="4" cols="60"></textarea></label><br/>
<button id="sendBtn">Send</button>
<h2>Inbox (auto-refresh)</h2>
<ul id="inbox"></ul>
</body>
</html>UI uses TweetNaCl for fast E2E encryption but you can swap in any library.
The recipient’s browser needs the secret key corresponding to the public key used in the box.
# 1) Holochain dev environment
curl https://holochain.github.io/holochain/install.sh | sh
# 2) Rust nightly
rustup target add wasm32-unknown-unknowncd dnas/mail
cargo build --release --target wasm32-unknown-unknownnix-shell https://holochain.love # or use your cargo-installed hc
hc dna pack workdir
hc app pack workdir
hc sandbox generate workdir/holo_mail.happ --run-dnaThis spins up two agents on localhost:8888 and 8889; open two browser tabs to ui/index.html (served via any static server) and send messages between them.
| Feature | How to add |
|---|---|
| Rich metadata | Extend Message struct: subject, attachments (IPFS CIDs), threading hash |
| Multi-device identity | Use DeepKey hApp for key rotation |
| Group mail / lists | Create membranes (“private neighbourhoods”) with Neighbourhoods framework |
| Relay when offline | Publish via Holo Hosting or let friends run “mail-relay” hApp cells |
| Spam filtering | Attach web-of-trust links (Trust Graph) to rate-limit unknown senders |
| Portable UI | Wrap UI with Tauri or Electron; ship as standalone desktop app |
git clone <this-repo> && cd holo-mail && nix develop --command "hc-spin up" → two agents can now send censorship-resistant, end-to-end encrypted e-mail with no servers in under a minute.