Skip to content

dot-protocol/piperchat

piperchat

End-to-end encrypted P2P chat in 500 KB. No signup. No server-side decrypt. Embed in any web app in 30 seconds.

License: Apache 2.0 v1.5.0 node ≥22 CI dot-protocol

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.


Why piperchat

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

Quick start

git clone https://github.com/dot-protocol/piperchat
cd piperchat && npm install && npm start

Open 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 with PORT=8080 npm start.


Embed in your app

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 or postMessage('piperchat:set-theme') at runtime)
  • identity injection from your existing auth layer — keys live in the iframe
  • metadata-only host bridgefrom, channel, ts flow 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 picker
  • embed-react<PiperChat /> component for any React app
  • crm-support-panel — per-customer encrypted support channels in a CRM-style shell

Full postMessage API in examples/README.md.


Self-host

Docker

docker compose up

Persistent volume piperchat-data carries the SQLite store + dictionary.

nginx + TLS

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.com

pm2

npm install --omit=dev
pm2 start deploy/ecosystem.config.js
pm2 save && pm2 startup

Logs in ./logs/. Memory + restart config in deploy/ecosystem.config.js.

Environment

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)

Group admin (v1.5)

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/admin

Or 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.


Use it from code

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.


Performance

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

Full release notes →


Architecture

Each running instance is a node. Nodes exchange messages two ways:

  1. P2P (iroh) — Nodes share an iroh document; GET /ticket returns an address blob, POST /connect joins. Direct connection across NAT, no central server.
  2. 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.


Security model

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 ⚠️ not yet — long-lived X25519 keys (v2.0 plans Double Ratchet)
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.


Roadmap

  • 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.


Contributing

We're a small team building this in public. The fastest way to help:

  1. Star this repo — it's the single best signal that this matters
  2. Run it locally and open an issue when something feels off
  3. Pick a v1.6 checkbox 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 test

runs the full v1.2/v1.4/v1.5 round-trip suite plus compression invariants.


Credits

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-inshttp, fs, crypto. No framework. No bundler.

License

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.

About

p2p chat. two browsers, one message, no server in the middle that owns your data.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors