End-to-end encrypted P2P chat in 500 KB. No signup. No server-side decrypt. Embed in any web app in 30 seconds.
Quick start · Embed in your app · Self-host · Protocol spec · CHANGELOG
┌─────────────┐ ┌─────────────┐
│ Alice │ ─── X25519 + AES ──▶ │ Bob │
│ (browser) │ │ (browser) │
└──────┬──────┘ └──────▲──────┘
│ │
▼ │
┌──────────────────────────────────────────────────┐
│ piperchat relay (stores ciphertext only) │
│ • E2E sealed bodies — server cannot decrypt │
│ • Ed25519-signed envelopes — tamper-evident │
│ • zstd-with-shared-dict — 34.8% smaller wire │
│ • iroh P2P fallback — direct when NAT permits │
└──────────────────────────────────────────────────┘
A self-hostable, OSS-first alternative to Slack/Discord/Matrix for teams who want real E2E encryption and embeddable group chat without the operational weight of Matrix Synapse or the Mongo footprint of Rocket.Chat.
The server holds ciphertext. Only recipient keys decrypt. Drop it into a customer-facing product, a CRM, a project tool — anywhere you'd otherwise reach for Intercom or a 3rd-party chat SaaS.
| piperchat | Matrix Synapse | Rocket.Chat | Discord/Slack | |
|---|---|---|---|---|
| End-to-end encryption | ✅ default | optional (Olm) | optional | ❌ |
| Server can read messages | ❌ never | ❌ when on | ❌ when on | ✅ always |
| Self-host complexity | one binary | postgres + nginx + workers | mongo + nginx + workers | n/a |
| Embed in your own app | ✅ iframe + postMessage | ❌ | partial | ❌ |
| RAM at idle | ~50 MB | ~600 MB | ~1.5 GB | n/a |
| Wire format | open (zstd + AES-GCM) | open (Megolm) | proprietary | proprietary |
| Lines of server code | ~1,200 LOC | ~60,000 LOC | ~250,000 LOC | closed |
| License | Apache 2.0 | Apache 2.0 | MIT | proprietary |
| Account required | ❌ | ✅ | ✅ | ✅ |
git clone https://github.com/dot-protocol/piperchat
cd piperchat && npm install && npm startOpen http://localhost:4100 in two browser tabs. Pick a username, send a
message. Done — that's end-to-end encrypted chat with no signup and no
database setup.
Port defaults to
4100. Override withPORT=8080 npm start.
The most common reason teams reach for piperchat: drop encrypted chat into your own product without rebuilding the wheel.
<iframe
src="https://chat.yourcompany.com/?embed=1&theme=light&accent=%232563eb"
style="border:0; width:100%; height:600px;"
></iframe>
<script>
const iframe = document.querySelector('iframe');
// Wait for the chat to boot, then inject the logged-in user's identity
window.addEventListener('message', (e) => {
if (e.data.type === 'piperchat:ready') {
iframe.contentWindow.postMessage({
type: 'piperchat:set-identity',
ed25519_priv: USER_KEYS.signing, // from your auth layer
x25519_priv: USER_KEYS.encryption, // never sent to your server
username: currentUser.handle,
channel: 'support-' + currentUser.companyId,
}, '*');
}
// Receive metadata-only notifications (body never leaves the iframe)
if (e.data.type === 'piperchat:message-received') {
showNotificationBadge(e.data.payload);
}
});
</script>What you get for free:
- theming via CSS variables (
?accent=…&bg=…URL params orpostMessage('piperchat:set-theme')at runtime) - identity injection from your existing auth layer — keys live in the iframe
- metadata-only host bridge —
from,channel,tsflow out; ciphertext never does - per-tenant channels with admin RBAC (claim → add member → enforce)
Working examples in examples/:
embed-iframe— vanilla HTML drop-in with live theme pickerembed-react—<PiperChat />component for any React appcrm-support-panel— per-customer encrypted support channels in a CRM-style shell
Full postMessage API in examples/README.md.
docker compose upPersistent volume piperchat-data carries the SQLite store + dictionary.
location / {
proxy_pass http://127.0.0.1:4100;
proxy_buffering off; # SSE needs this
proxy_cache off;
proxy_read_timeout 24h;
proxy_http_version 1.1;
}certbot --nginx -d chat.yourdomain.comnpm install --omit=dev
pm2 start deploy/ecosystem.config.js
pm2 save && pm2 startupLogs in ./logs/. Memory + restart config in deploy/ecosystem.config.js.
| Variable | Default | What it does |
|---|---|---|
PORT |
4100 |
HTTP port |
DATA_DIR |
./data |
SQLite + uploads directory |
DB_PATH |
${DATA_DIR}/piperchat.db |
Override DB location |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY |
— | Enable browser push (v1.5) |
POSTHOG_API_KEY |
— | Opt-in event telemetry (no-op when unset) |
Channels are open by default. Once claimed by an owner, only members may post. Owner + admins manage the roster.
# 1. owner claims a channel (first-write-wins)
curl -X POST https://chat.example.com/channels/design/claim \
-d '{"ts":...,"ed25519_pub":"...","sig":"...","description":"UI/UX team"}'
# 2. add a member
curl -X POST https://chat.example.com/channels/design/members \
-d '{"ts":...,"ed25519_pub":"...","sig":"...","target_dot1":"dot1:...","role":"member"}'
# 3. see the full roster + audit log
curl https://chat.example.com/channels/design/adminOr just open /admin in the browser — it's a full UI for claim, roster,
roles, and the audit log. All admin actions are ed25519-signed with a
±300s replay window.
const { PiperClient } = require('piperchat/client');
const c = new PiperClient({
url: 'http://localhost:4100',
author: 'agent-richard',
// ed25519 + x25519 keys (see docs/PROTOCOL.md)
});
await c.sendEncrypted('hello bob', 'direct');
c.subscribe((msg) => {
console.log(`[${msg.from_username}] ${msg.content}`);
});See examples/agents/ for a working two-agent demo,
examples/ for embed + CRM examples, and
docs/PROTOCOL.md for the full wire format spec.
Measured on the bundled test suite (Apple Silicon M-series, Node 22):
| Metric | Value |
|---|---|
| Server RAM at idle | ~50 MB |
| Server RAM at 1k msg/s | ~120 MB |
| Wire reduction (meaningful messages ≥100B) | −34.8% vs uncompressed |
| Compression ratio (mixed corpus) | 1.62× median |
| Round-trip compress + decompress (p99) | 0.68 ms |
| Sealed-body encrypt (p99) | 0.8 ms |
| End-to-end latency (relay path, local) | < 5 ms |
Each running instance is a node. Nodes exchange messages two ways:
- P2P (iroh) — Nodes share an iroh document;
GET /ticketreturns an address blob,POST /connectjoins. Direct connection across NAT, no central server. - Relay (SSE) — When P2P can't establish (firewalled, CI), the same HTTP server fans messages out to all connected browsers via Server-Sent Events.
Both paths see only the sealed envelope. The body is encrypted with AES-256-GCM, the key is wrapped per-recipient with X25519 ECDH + HKDF-SHA256, the whole envelope is signed Ed25519. The server stores ciphertext and forwards it. It has no decryption capability.
post /messages ─▶ verify sig → store ciphertext → fan out via SSE
get /messages ◀─ return ciphertext (anyone), recipient decrypts
get /events ◀─ SSE stream of ciphertext envelopes
post /channels/X/claim — ed25519-signed channel ownership
post /channels/X/members — owner/admin grants membership
get /admin — admin panel UI
Full route map in docs/PROTOCOL.md.
| Property | Guarantee |
|---|---|
| Server can read message bodies | ❌ never (ciphertext only) |
| Server can forge messages | ❌ no (no signing key) |
| Server can drop messages | ✅ yes (relay can censor; use P2P to bypass) |
| Server can correlate metadata | ✅ yes (from, to, ts, channel are visible) |
| Forward secrecy | |
| Quantum resistance | ❌ no — X25519 is classical only |
| Compression oracle attacks | mitigated by per-message random nonces and shared-dict static training |
Found something? Open an issue or email security@piedpiper.fun.
- v1.0 — Plaintext chat over SSE/iroh
- v1.1 — Ed25519 signed envelopes + DOT-native identity
- v1.2 — X25519 + AES-256-GCM sealed bodies (E2E encryption)
- v1.3 — Username registry
- v1.4 — zstd-with-shared-dictionary wire compression
- v1.5 — Onboarding wizard, browser push, embed mode, admin panel + RBAC
- v1.6 — Inline image previews, reactions, reply threading UI
- v1.7 — Mobile-native composer, drag-drop attach
- v2.0 — Double Ratchet forward secrecy + post-compromise security
- v2.x — Federation between piperchat instances
Track active work in GitHub issues.
We're a small team building this in public. The fastest way to help:
- Star this repo — it's the single best signal that this matters
- Run it locally and open an issue when something feels off
- Pick a
v1.6checkbox above and open a PR
Code style: no framework, no bundler, no transpile step. Vanilla JS, Node
built-ins, one dependency for sqlite (better-sqlite3) and one for signing
(tweetnacl). The whole server is ~1,200 LOC. Read it in an afternoon.
npm testruns the full v1.2/v1.4/v1.5 round-trip suite plus compression invariants.
Standing on the shoulders of:
- iroh (n0-computer) — P2P doc sync — MIT/Apache-2.0
- tweetnacl-js (dchest) — Ed25519 + X25519 — CC0
- better-sqlite3 (WiseLibs) — sync sqlite — MIT
- zstd-codec — zstandard in JS — MIT
- Node.js built-ins —
http,fs,crypto. No framework. No bundler.
Apache 2.0. Build anything. Ship it. Tell us what you made.
⭐ Star on GitHub · 💬 Live demo · 📖 Protocol spec · 🐛 Report a bug
Built by humans + agents at Pied Piper. Carrying the encrypted-everything torch since 2026.