End-to-end encrypted messaging with zero server-side state. No accounts, no logs, no persistent storage of any kind.
Tacenda — Latin, "things to be kept silent."
Live: ppb1s0n.us.kg
The server is a router, not a vault. It maintains an in-memory table mapping public keys to active WebSocket connections. When a message is sent, the server forwards an encrypted blob to the recipient's socket — nothing more. The decryption key never leaves the sender's device.
Each page load generates a fresh RSA-2048 key pair via the Web Crypto API. Your public key is your address. When you close the tab, the private key is gone.
RSA-OAEP alone cannot encrypt arbitrary-length data (~190-byte ceiling for 2048-bit keys). The system uses hybrid encryption:
- A random AES-GCM-256 key and 12-byte IV are generated per message.
- The plaintext is encrypted with AES-GCM.
- The AES key is wrapped with the recipient's RSA-2048-OAEP public key.
- Three base64 fields —
k,iv,msg— are transmitted and forwarded verbatim by the server.
For recurring contacts, generate a persistent identity key pair and exchange your public key once, offline. To start a session, broadcast a handshake: your current session public key encrypted with your contact's long-term public key. Every connected client silently attempts decryption; only the intended recipient succeeds. The server never learns who is signaling whom.
Identity key operations are CLI-only. Web clients execute JavaScript delivered by the server at runtime — a compromised server could inject code to exfiltrate a loaded private key. A compiled CLI binary is auditable, static, and immune to that class of attack.
| Path | Description |
|---|---|
/ |
Landing page — explanation and documentation |
/chat |
Chat interface |
/keygen |
Browser-based identity key generator |
Requires the Workers Paid plan ($5/month) for Durable Objects.
npm install
npx wrangler login
npm run deployThe Durable Object keeps the routing table in memory for the lifetime of the instance. Nothing is written to disk or KV storage.
npm install
node server.js # serves on http://localhost:3000For a public URL without deploying to Cloudflare, use the included tunnel config:
cloudflared tunnel runThe tunnel is configured in cloudflared.yml.
The Go CLI client supports long-term identity keys and background operation.
# build
cd cli && go build -o tacenda-cli .
# generate a long-term identity key pair (run once)
./tacenda-cli --keygen --out ~/.tacenda/identity.pem
# connect (if --key is omitted, auto-discovers ~/.tacenda/identity.pem)
./tacenda-cli --key ~/.tacenda/identity.pem \
--server wss://ppb1s0n.us.kg/ws
# commands
/find <long_term_public_key> broadcast a handshake to a contact
/accept <n> accept an incoming handshake
/chat <n> switch active contact (alias for /accept)
/contacts list known contacts
/mykey print your current session public key
/help show available commands
/quit exitThe CLI is fully interoperable with the web client — messages sent from either can be decrypted by the other.
├── public/
│ ├── index.html landing page
│ ├── chat.html chat interface
│ └── keygen.html identity key generator
├── cli/
│ ├── main.go Go CLI client
│ └── go.mod
├── worker.js Cloudflare Worker + Durable Object
├── server.js Node.js WebSocket server (local/tunnel)
├── wrangler.toml Cloudflare deployment config
└── cloudflared.yml Cloudflare Tunnel config
| Property | Status |
|---|---|
| Server reads message content | Never |
| Server stores encryption keys | Never |
| Session keys persist across reloads | Never |
| Identity private keys leave the device | Never |
| Server knows who is talking to whom (direct messages) | Routing address only (public key) |
| Server knows who is signaling whom (handshake broadcast) | No — broadcast to all |
MIT License — see LICENSE.
You are free to use, modify, distribute, and sell this software, including in commercial products. The only requirement is that you keep the copyright notice (Copyright (c) 2026 Huang) in all copies or substantial portions of the software.
This project was originally created by Huang.