Every team has this moment. Someone shares something sensitive in Slack. Someone else says "we shouldn't use this for that." Then comes the mass migration to a different app — one with a privacy policy nobody reads, on servers nobody controls, with an encryption model nobody has verified.
Your options? Signal (great, but mobile-first and phone-number-tied). Telegram (not E2EE by default). Matrix/Element (self-hostable, but the setup is an afternoon project and the UI is a lot). Discord (no). A team email thread (no).
All of them are either closed-source, require a phone number, live on someone else's server, or need a UI that feels like a full-time job to operate.
I wanted something simpler:
$ zypher work alice
That's it. Terminal. Encrypted. Your server or mine.
That's Zypher.
The 30-Second Version
Install it:
npm i -g zypher-chat
Spin up a server — one command clones, configures, and daemonizes it:
zypher server quickstart
Register on it:
zypher new
Open a conversation:
$ zypher work alice
╭─ alice ──────────────────────────────────╮
│ alice 🟢 online │
╰──────────────────────────────────────────╯
you › hey
alice › hey! how's the project going?
you › encrypting everything, obviously
No phone number. No app store. No browser extension. Just a terminal.
Why Not Just Use Signal / Telegram / Matrix?
You can. But:
- Signal needs a phone number. You can't register without a real mobile number. If your phone dies or you change numbers, you lose access. For a dev tool or internal team chat, that's an unnecessary external dependency.
- Matrix/Element is powerful but heavy. Spinning up a Synapse server is a real infrastructure project. The client is feature-rich but not CLI-native.
Discord is a product, not a protocol. Not self-hostable, not open-source, not private.
It lives on someone else's server. All of these options require trusting a third party with your data. Even if the app is open-source, the deployment isn't. Zypher is self-hosted by design — you control the infrastructure and the data.
Zypher is for when you want a terminal, a server you control, and actual end-to-end encryption — not a SaaS product that happens to call itself private.
How It Actually Works
Registration and Key Generation
When you register on a server, Zypher generates two keypairs locally — before anything touches the network:
- An Ed25519 identity key — your permanent signing key. Every message you send is signed with it. Recipients verify the signature. The server cannot forge a message from you without your private key.
- An X25519 pre-key — a Diffie-Hellman key for key exchange. Other clients use it to compute a shared secret without ever transmitting one.
The private keys never leave your machine. They're stored in ~/.zypher/config.json, mode 0600. The server only receives your public keys.
DM Encryption — per-message ephemeral ratchet
When you send a DM to Alice:
- Zypher generates a fresh ephemeral X25519 keypair for this message only.
- It performs ECDH between your ephemeral private key and Alice's public pre-key, then runs the result through HKDF-SHA256 to derive a 256-bit AES key.
- The message is encrypted with AES-256-GCM using a random IV.
- The ephemeral public key (the "ratchet key") ships alongside the ciphertext so Alice can reproduce the same ECDH and decrypt.
The ratchet key is unique per message. Every message gets a fresh keypair. The server sees only ciphertext, nonces, and public keys — never plaintext. Even if a session key were ever compromised, past messages stay safe.
Group Encryption — shared key, individually wrapped
Groups use a shared symmetric key, but that key is never sent in the clear. When you create a group or invite someone:
- A random 256-bit group key is generated on your machine.
- For each member, that group key is encrypted separately using their X25519 pre-key (same ECDH → AES-256-GCM flow as DMs).
- Each member receives their own encrypted bundle. The server stores these bundles but cannot read the group key inside any of them.
Group messages are then encrypted with the shared group key + AES-256-GCM. Admin operations — kick, promote, rename — trigger a re-key: a new group key is generated and re-encrypted for each remaining member.
What the Server Actually Stores
The server is a dumb relay with a mailbox. It stores:
- Usernames, bcrypt password hashes, and public keys (identity + pre-key)
- Encrypted ciphertext for offline delivery — it cannot read any of it
- Group membership and role metadata
It never sees plaintext. It never sees private keys. It never stores session keys.
Client A Server Client B
│ │ │
│── ECDH(ephemeral, │ │
│ B's pre-key) │ │
│── AES-256-GCM(msg) ──────▶│── store ciphertext ────▶│
│ │ │── ECDH(my pre-key,
│ │ │ ratchet_key)
│ │ │── AES-256-GCM decrypt
Things I Deliberately Didn't Build
- No phone numbers. Registration is username + password. Nothing else required.
-
No centralized server.
zypher server quickstartclones the server repo, configures it, and runs it as a local daemon. You own the infrastructure. - No cloud accounts. There's no "Zypher cloud." Every deployment is independent.
- No browser client. It's a CLI. The terminal is the UI. If you want a GUI chat app, you're looking for Signal.
- No multi-device key sync. Keys live on the device that generated them. Offline messages are queued on the server and delivered once — then gone from the queue.
- No metadata minimization theater. The server knows who is messaging whom (routing requires it). What it doesn't know is what they're saying.
Ghost Mode
Type /ghost in any chat session:
!! GHOST MODE !! This erases everything.
Type GHOST to confirm: █
Confirm with the word GHOST, and Zypher deletes ~/.zypher/ entirely — config, private keys, account data, everything. The process exits immediately. Nothing remains on disk.
One command, scorched-earth wipe. Useful when you need it.
Offline Messaging
If Alice is offline when you send her a message, the server queues the encrypted ciphertext. When Alice reconnects, all queued messages are delivered in order and the queue is cleared. The server holds them only as long as it has to — it cannot read them, and they're deleted once delivered.
The Stack
The server is ~600 lines of JavaScript:
| Component | What it does |
|---|---|
express + helmet
|
HTTP API with security headers and rate limiting |
socket.io |
Real-time message delivery |
node:sqlite |
User accounts, offline queues, group metadata |
bcrypt |
Password hashing |
jsonwebtoken |
Session authentication |
The client is a single index.js (~1,600 lines). No UI framework. One runtime dependency: socket.io-client. All crypto — X25519, Ed25519, AES-256-GCM, HKDF — is Node.js built-in (node:crypto). Nothing custom, nothing exotic.
Try It
# Install
npm i -g zypher-chat
# Quickstart: clones, configures, and launches a local server as a daemon
zypher server quickstart
# Register on it
zypher new
# Open a chat
zypher <servername> <username>
Requires Node.js 22+. Works on Linux, macOS, and Windows.
Source code: github.com/real-kijmoshi/zypher
npm: npmjs.com/package/zypher-chat
Website: zypher.kijmoshi.xyz
Top comments (0)