A React IRC client that connects to IRC servers directly over WebSocket.
- Direct WebSocket connection, no backend required
- Responsive UI (Tailwind CSS, shadcn/ui), light and dark mode
- Built-in themes (classic, modern, irc) and custom CSS themes; comfortable or compact spacing, 24 or 12-hour time, with or without seconds
- IRCv3: SASL, STS, chathistory, typing, metadata, MONITOR, read markers, pre-away, standard replies and more
- Replies, emoji reactions, multiline messages and deleting messages (IRCv3 drafts); replies and multiline messages fall back to plain lines where the server lacks them
- Spoilers (
||text||) and quotes (> text) - Bouncers: soju networks picked in the wizard or from the user menu, and the history and read state the bouncer keeps
- Older history loaded by scrolling up, and a search through the messages of the open windows
- Highlight words, an ignore list (
/ignore), and notification sounds for private messages and mentions - Joins, parts, quits and nick changes collapsed into one line, optionally hiding people who said nothing
- Keyboard shortcuts: a window switcher (Ctrl/⌘+K), message search (Ctrl/⌘+Shift+F) and the list of shortcuts (Ctrl/⌘+/)
- Auto away after a chosen time, with a message of your own
- A nick already in use: connects as
nick_, takes the nick back once it is free (MONITOR/WATCH), or asks NickServ to free it - End-to-end encrypted private messages (SIC-E2EE v1, described below)
- Through the Simple IRC Client gateway: sessions survive a backgrounded mobile tab (see below)
Built in gateway mode, the app asks the gateway for a resumable session. When the WebSocket drops (a phone puts the tab in the background), the gateway keeps the IRC connection and buffers what arrives. When the tab is visible again, the app reconnects with the session token and the number of the last line it saw, skips registration, and receives exactly the lines it missed. Channels, nick and negotiated capabilities stay as they were, and other users see no quit or rejoin.
- If the gateway no longer has the session (it expired or the gateway restarted), the same connection registers from scratch.
- After a page reload the app cannot resume, so it asks the gateway to end the old session and starts a new one.
- Disconnecting from the menu, or
/quit, sends QUIT so the gateway ends the session right away. - Encrypted conversations carry on: the peer never saw the connection end, so the session keys are kept and missed encrypted messages are decrypted from the replay.
- When some missed lines were dropped from the gateway's buffer, the app says so, refreshes the user lists of open channels and ends encrypted sessions, since a lost QUIT could mean someone else now holds a peer's nick; they are re-offered once presence reports the peer.
- It works the same on servers without IRCv3: the gateway adds the time it received each line to replayed lines that have no server-time.
The protocol is described in the gateway's README.
SIC-E2EE v1 encrypts private messages between clients without any server-side support. It uses WebCrypto only, with no third-party crypto library.
The protocol uses a Noise-like handshake with Triple Diffie-Hellman (3DH) for authentication and forward secrecy:
- Key Exchange: ECDH P-256 (chosen for broad WebCrypto compatibility across desktop webviews)
- Encryption: AES-256-GCM with 12-byte nonces
- Key Derivation: HKDF-SHA256 with a transcript hash as salt
- Message Format: CTCP-based, graceful degradation (non-SIC clients see nothing)
Alice (Initiator) Bob (Responder)
| |
|--- SIC-E2EE OFFER 1 <idKey> <ephKey> --->
| |
|<---- SIC-E2EE ACCEPT 1 <idKey> <ephKey> ---
| |
[Both derive session keys] [Both derive session keys]
| |
|--- SICE <frameId> 1/1 <ciphertext> ---->
| |
The handshake produces:
- Three DH operations mixed into IKM: DH(ephA,ephB) + DH(idA,ephB) + DH(ephA,idB)
- Transcript hash over all four public keys as HKDF salt
- Separate send/recv keys derived with different info labels (
sic-e2ee-v1 i2r/sic-e2ee-v1 r2i)
- Forward Secrecy: Per handshake, not per message (no ratchet); compromising a long-term identity key does not decrypt past sessions
- Authentication: Long-term identity keys bind the session to specific peers
- TOFU Pinning: First-seen identity keys are pinned; changed keys block the session and warn the user
- Fingerprint Verification: Users can compare 64-bit fingerprints (shown as NATO phonetic alphabet words) out-of-band to detect MITM at first contact
- No Silent Downgrade: If encryption was previously established with a peer, dropping back to plaintext shows a warning
- Each IRC network has its own identity key pair
- Identity keys are non-extractable CryptoKey objects stored in IndexedDB via structured clone
- Private keys are never exposed as raw bytes to JavaScript, so they cannot be exported or copied out
- Fingerprints are displayed as 16 NATO phonetic alphabet words, one per hex nibble (e.g.,
Zero One Two Three Four Five Six Seven Eight Nine Alpha Bravo Charlie Delta Echo Foxtrot) — internationally standard and unambiguous to read aloud
- Chunking: Large messages split into multiple IRC lines (frame size follows the server's LINELEN, 320 base64 chars by default; max 16 frames)
- Reassembly: Out-of-order chunks are buffered and reassembled within 120 seconds of the first chunk
- Message Types: Regular messages (
m) and actions/CTCP ACTION (a) are preserved through encryption - No Plaintext Leak: Even
/meactions are encrypted, not sent as cleartext ACTION
- Inbound OFFER frames throttled to 1 per peer per second
- RESET replies throttled to 1 per peer per 10 seconds
- Tracked peers and pending frames are capped, so memory use stays bounded
The encryption state is always visible:
- Lock icon in conversation header shows encryption status
- Banners appear for handshake prompts, key-mismatch warnings, and errors — an unverified-but-encrypted session is the expected TOFU default and stays quiet, with verification discoverable in the lock popover instead of nagging
- Color coding:
- Green lock = Encrypted & verified
- Yellow lock = Encrypted but unverified
- Red shield = Key mismatch / security issue
- Open lock = No encryption (with warning if previously encrypted)
Users can:
- Start encryption with any peer via the lock button menu
- Accept or decline incoming encryption requests
- View and compare fingerprints in the lock popover
- Mark a peer as verified after out-of-band fingerprint comparison
- End active encryption sessions
- Per-network identities: Prevents correlation of the same user across different IRC networks
- No history storage: Encrypted messages are not persisted to IndexedDB; only plaintext messages are stored
- Session binding to nicks: If a peer changes nick, the session is dropped (not automatically transferred)
All E2EE traffic uses CTCP (Client-To-Client Protocol):
Handshake:
SIC-E2EE OFFER 1 <identityKeyB64> <ephemeralKeyB64> (PRIVMSG)
SIC-E2EE ACCEPT 1 <identityKeyB64> <ephemeralKeyB64> (NOTICE)
SIC-E2EE DECLINE (NOTICE)
SIC-E2EE RESET (NOTICE)
Encrypted messages:
SICE <frameId> <index>/<total> <base64Chunk> (PRIVMSG)
CTCP was chosen because unknown CTCP verbs are silently ignored by other IRC clients, providing graceful degradation.
pnpm installStart the development server:
pnpm run devThe application will be available at http://localhost:5173
pnpm test # unit tests
pnpm run test:e2e # Playwright, against IRC servers in DockerEvery e2e test runs twice: against ergo, an IRCv3 server, and through the gateway against ngircd, a server without IRCv3 (the *-legacy-* projects). The tests serve the app on ports of their own (5273 and 5274), so a running pnpm run dev is left alone. Tests of IRCv3-only features are skipped on ngircd. The gateway runs from source for the *-gateway and *-legacy-* projects; it expects a checkout next to this repository (../gateway, with pnpm install done); set E2E_GATEWAY_DIR to use another location.
Run using Docker:
docker build -t simple-irc-client .
docker run -p 5173:5173 simple-irc-clientThe application will be available at http://localhost:5173
- Simple-Irc-Client - Main project organization
If you find a bug or have a feature request, please open an issue on GitHub.
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
The AGPL-3.0 license ensures that if you modify and deploy this software over a network, you must make the complete source code available to users.
Authors: