Ephemeral E2EE chat relay. RAM-only, zero logs, zero traces.
Ephemeral end-to-end encrypted chat relay. Everything lives in RAM — no database, no logs, no persistence. When a room is deleted or all participants leave, all data is gone forever.
E2EE by default: The relay only sees ciphertext. Encryption keys are generated in the browser (ephemeral P-256 ECDH + AES-256-GCM), exchanged peer-to-peer, and destroyed on disconnect. The relay cannot read messages.
Flow: User A → E2EE → WebSocket → GhostRAM relay (opaque ciphertext) → WebSocket → E2EE → User B
- Zero-knowledge relay — messages are end-to-end encrypted; the server only forwards ciphertext
- RAM-only — no database, no message history, no file storage
- Zero-log mode — disable all server output with
LOGGING=0 - Ephemeral keys — ECDH keypairs and AES group keys are discarded on disconnect
- File transfer — encrypted file sending with chunking and image previews
- Multi-peer — P2P rooms support up to 256 participants
- Support mode — 2-person rooms with admin dashboard for support staff
- Built-in admin UI — web-based dashboard to monitor and join support rooms
- Embeddable widget — drop-in JavaScript bundle for any website
- Cloudflare Turnstile — optional bot protection for room creation
- Rate limiting — configurable per-IP rate limits
- Tor compatible — run as an onion service for maximum privacy
- Docker ready — single-command deployment
| Mode | Participants | Join token |
|---|---|---|
p2p |
Up to maxParticipants (default 16, max 256) |
Yes — share with trusted peers only |
support |
Exactly 2. A third join attempt destroys the room. | No — use API key or open WebSocket |
When the last client leaves, the room is removed from memory.
git clone https://github.com/TheAnonymousCodingCult/ghostram.git
cd ghostram
npm install
cp .env.example .env # edit API keys
npm startOpen http://localhost:8765/admin/ (set ANOCHAT_ADMIN_KEY first).
cp .env.example .env # edit API keys
docker compose up --build -dThe container binds to 127.0.0.1:8765 by default. Put a reverse proxy (Caddy, nginx) in front for TLS.
All settings are in .env (see .env.example for all options):
| Variable | Default | Description |
|---|---|---|
PORT |
8765 |
HTTP/WS listen port |
ANOCHAT_API_KEY |
(empty) | Required for POST /rooms. Never expose in client-side code. |
ANOCHAT_ADMIN_KEY |
(empty) | Enables admin dashboard at /admin/ |
REQUIRE_WS_API_KEY |
0 |
If 1, WebSocket requires API key (P2P guests with valid joinToken are always allowed) |
MAX_ROOMS |
1000 |
Max rooms in memory (0 = unlimited) |
ROOM_MAX_IDLE_MS |
14400000 |
Auto-delete idle rooms after 4h (0 = disabled) |
TRUST_PROXY |
0 |
Set 1 behind nginx/Caddy so rate limits use X-Forwarded-For |
LOGGING |
1 |
Set 0 for zero console output (zero-knowledge mode) |
TURNSTILE |
0 |
Enable Cloudflare Turnstile for POST /rooms |
TURNSTILE_SECRET_KEY |
(empty) | Server-side Turnstile secret |
TURNSTILE_SITE_KEY |
(empty) | Published at GET /public-config for the widget |
If ANOCHAT_API_KEY is set, protected routes require: X-API-Key: <key> or Authorization: Bearer <key>.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
No | Liveness check |
POST |
/rooms |
API key | Create room. Body: { "mode": "p2p"|"support" }. Returns roomId, joinToken (P2P only). |
GET |
/rooms |
API key | List all rooms |
GET |
/rooms/:roomId |
API key | Room metadata |
DELETE |
/rooms/:roomId |
API key or joinToken | Close room, disconnect all participants |
wss://your-relay.com/ws?roomId=<uuid>&clientId=<unique-per-tab>&joinToken=<token>
roomId— must exist (create via API first)clientId— unique per browser tabjoinToken— required for P2P rooms (returned fromPOST /rooms)apiKey— optional query param ifREQUIRE_WS_API_KEY=1
All messages (text, binary) are broadcast to every other participant in the room.
The E2EE module (shared/e2ee-session.js) runs entirely in the browser:
- Each client generates an ephemeral ECDH P-256 keypair on connect
- Clients exchange public keys via
hs(handshake) frames through the relay - The room creator generates a random AES-256-GCM group key
- The group key is encrypted per-peer using ECDH-derived wrap keys and distributed via
wrapframes - All chat messages and files are encrypted with the shared group key
- On disconnect, all keys are zeroed and discarded
The relay never sees plaintext. Verify encryption integrity by comparing Safety Numbers out-of-band.
Built into the relay — no external app required.
- Set
ANOCHAT_ADMIN_KEYto a long random secret - Open
https://your-relay.com/admin/ - Enter the admin key (and optionally the relay API key for WebSocket access)
- Dashboard lists open
supportrooms. Join opens an E2EE chat panel. Close destroys the room.
| Method | Path | Header | Description |
|---|---|---|---|
GET |
/admin/api/health |
X-Admin-Key |
Sanity check |
GET |
/admin/api/support-rooms |
X-Admin-Key |
List support rooms |
GET |
/admin/api/stats |
X-Admin-Key |
Detailed server stats |
DELETE |
/admin/api/rooms/:roomId |
X-Admin-Key |
Force-close room |
Build the IIFE bundle:
npm run build:widgetOutput: widget/dist/anochat-widget.js. Host on your CDN or static server.
<div id="chat-root"></div>
<script src="https://your.cdn/anochat-widget.js"></script>
<script>
AnochatWidget.init({
apiBaseUrl: 'https://your-relay.com',
mode: 'p2p',
title: 'Encrypted Chat',
container: document.getElementById('chat-root'),
// For P2P guests joining via invite link:
// roomId: '...', joinToken: '...', isP2PGuest: true
});
</script>See widget/README.md for all options.
Security: Never put
ANOCHAT_API_KEYin public JavaScript. Create rooms on your backend and passroomId+joinTokento the frontend.
┌─────────────┐ POST /rooms ┌─────────────┐
│ Your App │ ───────────────────► │ GhostRAM │
│ Backend │ (with API key) │ relay │
│ │ ◄─────────────────── │ │
│ │ roomId + joinToken │ │
└──────┬──────┘ └──────┬──────┘
│ roomId + joinToken │
▼ │
┌─────────────┐ WebSocket (E2EE) │
│ Browser │ ◄──────────────────────────►│
│ (widget) │ ciphertext only │
└─────────────┘ │
Your backend holds the API key. The browser only gets roomId + joinToken and connects directly to the relay via WebSocket. The relay forwards encrypted frames — it cannot read them.
For maximum privacy:
# .env
LOGGING=0# docker-compose.yml
services:
ghostram:
logging:
driver: "none"Reverse proxy (Caddy example):
ghostram.example.com {
reverse_proxy localhost:8765
log {
output discard
}
}
GhostRAM can run as a Tor hidden service. Add a Tor sidecar to your Docker setup or configure torrc to forward to the relay port. See DEPLOYMENT.md for step-by-step instructions.
Users connecting via Tor Browser use the .onion address. The widget supports dual URLs (clearnet + onion) — detect the access method client-side and connect to the appropriate relay.
- Relay: Node.js + Express +
ws. Stateless message forwarding. All room state in aMapin process memory. - E2EE:
shared/e2ee-session.js— browser-only module. Ephemeral ECDH + AES-GCM. Key-setup frames serialized via lightweight mutex; chat messages decrypt in parallel. - Widget:
widget/— Vite-built IIFE bundle that imports the shared E2EE module. - Admin:
public/admin/— static HTML/JS dashboard importing the same shared E2EE module.
Each relay instance has its own memory. Clients in the same room must hit the same instance (sticky sessions). For horizontal scaling, add shared state (Redis, etc.) — not included.
MIT
