A small, modern IRC client for people who keep irssi running.
One calm reading column, your whole history, and a window happy to live in the corner of your screen.
- Your history, every time: lobby-server sits next to irssi and keeps every line, so Lobby opens with everything you missed
- Grouped, readable chat: one photo and name per person, their lines stacked under it, link previews, pictures, videos and audio inline
- Reactions and replies: sent as plain IRC (
nick: 😂,nick: text) and shown as reactions and quotes in Lobby - People: a photo, a colour and any number of nicks per person, matched by pattern or ident and host
- Who is here: the most active faces in the header open the channel's list, ops first, then by who spoke last; pick several to give op or voice in one go
- Unread that stays put: a red New line, jump buttons, and nothing marked read until you say so
- Attachments: paste, drop or pick a file, and its address goes into your message, through a file host of your own
- Notifications: desktop notifications with the sender's photo for mentions and private messages, quiet for muted channels
- irssi muscle memory:
/wc,/whois,/namesand the rest, with a/menu,Alt+1…9andAlt+A - Same everywhere: people, channel pictures, read positions and settings live on your server and follow you between devices
- Self-hosted, no bouncer: irssi stays in charge, Lobby reads through irssiproxy and talks to your own server over SSH
irssi already sees every message, but irssiproxy only hands over what happens after you connect. lobby-server fills that gap: it runs on the same machine as irssi, connects to irssiproxy like any client, and writes every line to SQLite. Lobby connects to lobby-server, loads the history, then follows the live chat.
flowchart TB
nets[(IRC networks)] --- irssi
subgraph host [Your shell host]
irssi --- proxy[irssiproxy]
proxy --> server[lobby-server<br/>SQLite history]
end
server -->|WebSocket over SSH| app[Lobby<br/>Linux and macOS]
Lobby can also connect straight to irssiproxy or any IRC server. That works without a server of your own, but only shows what arrived while Lobby was open.
| Mode | Lobby connects to | History |
|---|---|---|
| Server | lobby-server | Everything recorded, also while Lobby was closed |
| Direct | irssiproxy or an IRC server | Only what arrived while Lobby was running |
Download the latest build from Releases:
| System | File |
|---|---|
| Debian, Ubuntu | .deb, install with sudo apt install ./Lobby_*.deb |
| Other Linux | .AppImage, make it executable and run it |
| macOS (Apple silicon) | .dmg |
The macOS build is not signed. After moving Lobby to Applications, run xattr -dr com.apple.quarantine /Applications/Lobby.app once, or macOS refuses to open it.
Building from source
You need Rust from rustup.rs, plus:
| System | Command |
|---|---|
| Arch | sudo pacman -S --needed base-devel webkit2gtk-4.1 |
| Debian, Ubuntu | sudo apt install build-essential libwebkit2gtk-4.1-dev |
| Fedora | sudo dnf install webkit2gtk4.1-devel gcc |
| macOS | xcode-select --install |
git clone https://github.com/rollecode/lobby
cd lobby
cargo build --release -p lobby
./target/release/lobbyTo update: git pull and build again.
There is no App Store build yet. On a Mac with Xcode and an Apple developer account, with the iPhone connected over USB:
rustup target add aarch64-apple-ios
npm install
export APPLE_DEVELOPMENT_TEAM=YOUR_TEAM_ID
npm run tauri ios init
sh tools/ios-icons.sh
npm run tauri ios build -- --export-method debugging
xcrun devicectl device install app --device YOUR_IPHONE_UDID src-tauri/gen/apple/build/arm64/Lobby.ipaxcrun devicectl list devices shows the UDID. On first start Lobby opens its connection setup; use Through SSH and add the key line it shows on your shell host. Notifications and the badge work while Lobby is open or recently in the background; the iPhone has no background connection yet.
On the machine where irssi runs:
git clone https://github.com/rollecode/lobby
cd lobby
./server/install.shThe installer builds lobby-server, writes a config and a random token, and sets up a systemd user service. server/README.md walks through turning on irssi's proxy, the config, starting the service, updating and removing it.
Important
lobby-server only listens on 127.0.0.1. Lobby reaches it through SSH with your usual key, so nothing on the server is opened to the internet. For an address of its own, put a reverse proxy with TLS in front, as shown in the server README.
On first start Lobby opens its connection setup. You can come back to it any time from the gear, Set up the connection. The gear is in the channel list, which opens when you click the channel name in the header. The same three choices are on every device:
| Choice | What you fill in | History |
|---|---|---|
| Through SSH | you@your-shell-host, the SSH port if not 22, and the lobby-server token |
Everything lobby-server recorded |
| Server address | wss://lobby.example.org and the token |
Everything lobby-server recorded |
| Straight to IRC | Your nick and one row per IRC server or irssiproxy port | Only while Lobby is open |
The token is in ~/.config/lobby-server/token on the server.
With lobby-server, the setup also lists the server's networks. Adding or removing one there changes lobby-server's own config and takes effect right away, without restarting anything.
SSH. Lobby has SSH built in, so it also works where no ssh program exists, like the iPhone. It signs in with a key of its own, and on a computer also with your usual keys in ~/.ssh and your SSH agent. The setup shows Lobby's key as a line to add to ~/.ssh/authorized_keys on the shell host:
restrict,port-forwarding,permitopen="127.0.0.1:7667" ssh-ed25519 AAAA… lobby
That key can reach lobby-server and nothing else: no shell, no other ports. The server's host key is checked against ~/.ssh/known_hosts, or remembered on first connect.
The setup is saved in a plain file you can also edit by hand, and Lobby reconnects as soon as you save:
- Linux:
~/.config/lobby/config.toml - macOS:
~/Library/Application Support/lobby/config.toml
[server]
ssh = "you@your-shell-host"
token_file = "~/.config/lobby/server.token"| Keys | What they do |
|---|---|
Ctrl+K (Cmd+K on a Mac), or click the channel name |
Find and switch channels |
Alt+1 … Alt+9 |
Jump to a channel |
Alt+Left, Alt+Right |
Previous and next channel |
Alt+A, or the unread counter |
Next channel with unread, mentions first |
Alt+U, or the unread pill |
Jump to the first unread message in this channel |
Ctrl+Enter (Cmd+Enter on a Mac) |
Mark this channel read |
Tab |
Complete a nick |
Up in an empty input |
Bring back the last message |
Every shortcut can be changed under Settings, Keyboard shortcuts.
Reactions and replies. Point at a message for your favourite emojis, more emojis, reply and a menu. On a phone, hold a message instead: it lifts out of the chat with your favourite emojis, more emojis and reply above it. IRC has no reactions, so a reaction is sent as nick: 😂, and Lobby shows such lines from anyone as a reaction under that person's message. A reply goes out as nick: text and shows the message it answers.
Unread. A red New line marks where unread messages start. Nothing is marked read on its own: click the checkmark at the end of the New line, the Mark read button at the top, or press Ctrl+Enter (Cmd+Enter on a Mac).
People. Click a name or photo in the chat, or open the gear and People. Kaisla also covers Kaisla_ and Kaisla-, Kaisla* covers any ending, and kaisla@*.example.net matches by ident and host. The colour follows the photo unless you pick one. Click the channel icon in the header to give a channel its own picture or colour, or to mute it. A muted channel adds nothing to unread counts or jumps, except when someone mentions you. /mute and /unmute work too, and so does the bell beside each channel in the channel list.
Who is here. The faces at the right of the header are the people talking now, in the last hour. Click them for everyone in the channel: ops first, then who is talking now, earlier today and away, fading the longer they stay quiet. Voiced people have a small mint mark. The button at the top of the list keeps it beside the chat instead, and the faces then show or hide it. Click anywhere on a row to pick it, the photo turns into a checkbox. With people picked, Give voice and Give op go out as one MODE +vvv line, or a few when the server takes fewer per line. Right-click a row for the same and to message or edit the person; with one person picked, the bar under the list has Message and Edit too. Mode changes show in the chat like joins and quits.
Commands. Type / for the menu. /me, /msg, /query, /join, /part, /wc, /topic, /names, /whois, /nick, /away, /notice, /invite, /kick, /op, /deop, /voice, /devoice, /clear, /say and /quote work as in irssi. Anything else is sent as is. /quit is off, because through irssiproxy it would disconnect irssi from the network.
Emoji. :D, :), <3 and friends turn into emoji as you type. A colon at the start of a word opens the emoji picker, and :joy or a Finnish word like :nauru lists matching emoji, most used first. Both can be turned off in Settings.
Attachments. Paste an image into the message box, drop files on the window, or use the paperclip. The file goes to your file host and its address is put into your message, so everyone on IRC can open it. Set the host's address under Settings, File host for attachments.
Running a file host for attachments
Lobby uploads the way Halloy and soju-style hosts expect, so one host serves all of them:
POSTthe file as the raw request body to the host's addressContent-Typeis the file's type,Content-Dispositioncarries its name (attachment; filename*=UTF-8''name.png)- The host answers
201 Createdwith the file's address inLocation
tools/filehost/upload.php is a complete host in one PHP file. It keeps only images, PDFs and plain text, and needs a secret key in the address. Put it on any web server with PHP, set KEY, BASE_URL and STORE at the top, and allow uploads of 20 MB. With nginx:
server {
server_name files.example.org;
root /var/www/files;
client_max_body_size 20m;
location = /upload.php {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
location ^~ /u/ {
add_header X-Content-Type-Options nosniff;
}
}Then set https://files.example.org/upload.php?k=YOUR_KEY in Lobby's Settings. The address is stored on your lobby-server like other settings, so every device uses it.
Importing a folder of photos
If you already have photos named after nicks (kaisla.png, Kaisla_.png), import them all at once. Files with the same picture, or nicks that differ only by _, - or `, become one person. Nicks you have already set up are left alone. Round avatars are squared to fill the frame.
pip install Pillow websocket-client
ssh -N -L 17667:127.0.0.1:7667 you@your-shell-host &
python3 tools/import_photos.py ~/Pictures/irc ws://127.0.0.1:17667 ~/.config/lobby/server.token --dry-runDrop --dry-run to import. Add --skip file.png … at the end to leave files out.
flowchart TB
core[core<br/>IRC, history, the hub]
core --> server[server<br/>lobby-server]
core --> app[src-tauri<br/>the app]
ui[ui<br/>HTML, CSS, JS] --> app
corekeeps IRC connections running, turns lines into events and stores them. The app in direct mode and lobby-server both use it.serveris lobby-server: the hub behind a WebSocket.src-tauriis the desktop app. In server mode it talks to lobby-server, in direct mode it runs the hub itself.uiis the window. It does not know which mode is in use.
npm ci
npm test # JS and Rust unit tests
npm run check # Biome, rustfmt and clippy, as CI runs them
npm run format # Biome and rustfmt rewrite the codeEvery push runs the same checks on GitHub Actions, plus the macOS app build, the iPhone lint and the core tests on the iPhone simulator.
Changes waiting for a release collect under [Unreleased] in CHANGELOG.md. Releases are automatic: when that heading becomes a version heading on main, GitHub Actions tags it, publishes the release notes and attaches the Linux and macOS builds.
The app and lobby-server protocol
JSON over a WebSocket. The app sends hello with the token first, the server answers welcome with the current state and then streams events. Requests carry an id that the reply repeats. The types live in core/src/protocol.rs.
app {"type":"hello","token":"..."}
server {"type":"welcome","snapshot":{"networks":[...],"channels":[...],"people":[...],"settings":{...}}}
server {"type":"event","event":{"type":"line","net":"IRCnet","buffer":"#rust","nick":"kaisla","text":"hei",...}}
app {"type":"history","id":1,"net":"IRCnet","buffer":"#rust","before":null}
server {"type":"reply","id":1,"data":[...],"error":null}
app {"type":"send","id":2,"net":"IRCnet","buffer":"#rust","text":"moi"}
app {"type":"save_person","id":3,"person":{"id":0,"name":"Kaisla","nicks":["kaisla"],...}}
app {"type":"set_setting","id":4,"key":"read","value":"{...}"}

