Private. Fast. Direct.
A peer-to-peer, end-to-end encrypted chat platform. No signup, no login, no database. Messages travel directly between two browsers over a WebRTC DataChannel; a small Node.js signaling server only helps two peers find each other and is never able to read message content.
npm install
npm startThen open http://localhost:3000 in two different browser tabs (or two devices on the same network) to simulate two peers.
For auto-restart during development:
npm run dev- Create or join a room. The signaling server (
server/server.js) assigns a short room code and relays only WebRTC connection-setup data (SDP offers/answers, ICE candidates) between exactly two browsers. - Peers connect directly. Once WebRTC negotiation completes, a direct
RTCDataChannelopens between the two browsers. The signaling server is no longer part of the conversation from this point on. - Keys are exchanged over that direct channel. Each browser generates an ephemeral ECDH (P-256) key pair and exchanges public keys peer-to-peer. Both sides independently derive the same AES-256-GCM session key — it is never transmitted anywhere.
- Every message and file chunk is encrypted with that session key before it touches the data channel.
Your device → AES-256-GCM encrypt → WebRTC DataChannel → Peer decrypts
↑
Signaling server sees SDP/ICE only, never content
nt-chats/
├── server/
│ └── server.js # WebSocket signaling + Express static server + QR endpoint
├── public/
│ ├── index.html # Landing page
│ ├── chat.html # Chat screen
│ ├── manifest.json # PWA manifest
│ ├── sw.js # Service worker (offline app-shell caching)
│ ├── css/
│ │ ├── main.css # Design tokens & shared components
│ │ ├── landing.css # Landing page styles
│ │ └── chat.css # Chat screen styles
│ ├── js/
│ │ ├── app.js # Landing page logic
│ │ ├── components/
│ │ │ └── chat.js # Chat screen controller (the largest module)
│ │ ├── crypto/
│ │ │ └── encryption.js # Web Crypto API E2EE (ECDH + AES-GCM)
│ │ ├── webrtc/
│ │ │ └── peer.js # RTCPeerConnection + signaling client
│ │ └── utils/
│ │ ├── helpers.js # Sanitization, markdown, formatting
│ │ └── storage.js # IndexedDB wrapper (local-only history)
│ └── icons/ # Logo, favicon, app icons
└── package.json
Connection & security
- No signup, login, email, or phone number
- Room codes + shareable invite links + QR code (rendered server-side, encodes only the room URL)
- End-to-end encryption: ECDH key exchange, AES-256-GCM message/file encryption
- Automatic reconnect with backoff, connection status states, latency indicator
Messaging
- Real-time text messages, typing indicator, delivered/seen receipts
- Reply, forward (into composer), copy, pin, favorite, delete-locally
- Local full-text search, markdown (
**bold**,*italic*,`code`, fenced code blocks with light syntax highlighting)
Files
- Drag-and-drop or button upload; images, PDFs, text, ZIP
- Chunked, per-chunk-encrypted transfer with a live progress bar
- Image preview, download button, configurable auto-download
Interface
- Dark/light themes, 4 accent palettes, adjustable font size
- Glassmorphism surfaces, gradient accents, smooth transitions, responsive 3-pane layout
- Installable PWA with offline app-shell caching
- Keyboard shortcuts (⌘/Ctrl+K search, Esc to close panels, Enter to send)
- ARIA labels, visible focus states,
prefers-reduced-motionandprefers-contrastsupport
This build implements a genuinely working version of every major feature area in the brief (encryption, P2P messaging, files, PWA, UI polish). A few items were intentionally simplified rather than faked:
- Language selector stores a preference but the interface text itself is English-only in this build — full i18n would need a translation layer.
- File transfer is sequential (one file's chunks fully arrive before the next begins), which is simple and reliable but not maximally parallel — fine for typical chat attachments, worth revisiting for heavy concurrent transfers.
- Rooms are 1:1 by design, for the strongest peer-to-peer privacy guarantee (no relay/SFU in the middle). Group calls would need a different architecture (SFU or mesh).
- "Recent chats" in the sidebar are locally remembered room codes on this device, not a synced chat list (there's no account to sync to).
The app is a single Node process serving static files + a WebSocket endpoint, so it deploys anywhere that runs Node 18+:
npm install --omit=dev
PORT=3000 npm start- Behind a reverse proxy (Nginx, Caddy), make sure WebSocket upgrade
requests to
/wsare proxied through (Upgrade/Connectionheaders). - For HTTPS, terminate TLS at your proxy/load balancer — WebRTC requires a
secure origin (
https://) in production browsers,localhostis exempted for local development. - The server is stateless aside from in-memory room bookkeeping, so it does not need a database or persistent volume. Restarting it simply clears any rooms that were open at the time.
- Environment variables:
PORT(default3000).
MIT