A multiplayer Texas Hold'em poker game you play entirely inside your terminal. No app, no browser, no account. Just SSH in and start playing.
hosting it with no guarantees on ssh tui.johnvolt.xyz -p 6357
Most likely there aren't going to be any players any time you try it out :D
gopoker is a learning project. The goal was to build something genuinely non-trivial in Go and stress-test a few concurrency ideas in a real-world setting:
- The actor pattern with channels — both the lobby and each table run as independent goroutines communicating exclusively through typed command/event channels. No shared mutable state, no mutexes.
- BubbleTea for multi-user TUI — one BubbleTea program runs per SSH connection, all sharing the same in-process domain layer. This creates interesting pressure on how you model state and propagate events to many clients at once.
- Clean session lifecycle — SSH disconnects (killed terminals, dropped connections) must be handled reliably without leaking goroutines or leaving ghost players. The approach uses a per-session disconnect watcher that fires cleanup callbacks registered as the user moves through the app.
The game itself is intentionally minimal — just enough poker to make the concurrency model interesting.
Each SSH connection spawns a BubbleTea TUI session in its own goroutine. A shared Lobby singleton persists for the lifetime of the server process; game state is entirely in-memory.
SSH session → BubbleTea model → Lobby (goroutine) → Table (goroutine) → Game (pure logic)
The Lobby and each Table follow the same concurrency pattern: a goroutine with a select loop processing two channel types:
- Commands — synchronous RPC-style requests with an embedded reply channel (e.g. join table, fold).
- Events — asynchronous broadcasts sent to every connected subscriber. Event sends are non-blocking so a slow client cannot stall the table goroutine.
When a player acts, the table goroutine processes the command, updates the pure game state, and broadcasts a snapshot to all clients. Each client's BubbleTea model applies the snapshot and re-renders independently.
- Go 1.22+
- A Linux server with a public IP or domain
The server needs a host key so clients can verify the connection.
mkdir -p .ssh
ssh-keygen -t ed25519 -f .ssh/id_ed25519 -N ""go build -o gopoker .
./gopokerThe server listens on 0.0.0.0:6357 by default.
Players join with a plain ssh <host> when the game is reachable on port 22. The cleanest way to do this without running the process as root is a firewall redirect:
# iptables
sudo iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 6357
# nftables alternative
sudo nft add rule nat prerouting tcp dport 22 redirect to :6357Before doing this: if you also manage the server over SSH, you will lock yourself out. Move OpenSSH to a different port first:
# /etc/ssh/sshd_config Port 2222Reconnect on the new port, then apply the redirect. Keep that admin port open in your firewall or you will lose access entirely.
If you use a VPS with a firewall control panel (Hetzner, DigitalOcean, etc.), make sure port 22 is allowed inbound and the redirect above handles the rest.
Once set up, anyone on the internet can join with:
ssh <your-ip-or-domain>[Unit]
Description=gopoker SSH server
After=network.target
[Service]
ExecStart=/path/to/gopoker
WorkingDirectory=/path/to/gopoker-dir
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now gopokerPick a username when you connect. You land in the lobby.
Lobby
n— create a new table (name it, pick a capacity)enter— join the selected tabletab— switch between the tables pane and the users paneh / j / k / l— navigate tables
At the table
↑ / kand↓ / j— select an actionenterorspace— confirmq— leave the table (prompts for confirmation)
When it's your turn, choose from the available actions: fold, check, call, bet, or raise. Bet and raise open a size picker with quick presets (25%, 50%, 75% of the pot) or a custom amount you type in.
Each player starts with 1200 chips. If you bust, you get a second chance — but your stack resets.
This is a learning project, not a production poker room. Several things are deliberately missing:
- Side pots — all-in situations with more than two players do not split the pot correctly. The pot goes entirely to the winner with the best hand regardless of stack sizes. This is a known gap.
- Reconnection — if you disconnect mid-hand (killed terminal, dropped connection), you are folded out and removed from the table. There is no way to rejoin the same hand.
- Persistent state — restarting the server wipes all tables and bankrolls. Nothing is saved to disk.
- Spectating — you can only see your own hole cards. Joining a table means playing; there is no observer mode.
- Player authentication — usernames are self-declared at login. Anyone can claim any name not currently in use.
- Chat — there is no in-game communication beyond the action log.
- Analytics and logging — there is no structured logging, hand history, or stats tracking. The server writes minimal output to stdout.
- Security hardening — this is not designed to run on the public internet at scale. There is no rate limiting, abuse protection, or input sanitisation beyond what the TUI enforces.