Terminal user interfaces (TUIs) have experienced a massive renaissance. Beyond standard CLI tools, you can build full interactive experiences—dashboards, games, portfolios, and blogs—and serve them to anyone in the world directly over SSH without requiring them to install any dependencies locally.
You can try a live example in your terminal right now:
bashssh ssh.vewake.me
(Web version: vewake.me)
Here is a practical guide on how to build and host your own SSH-accessible terminal application.
Building an SSH application requires two parts: an SSH Server Daemon to handle network sessions and an Interactive TUI Runtime to render the interface.
In the Go ecosystem, the Charm stack provides everything you need:
SIGWINCH), and user session isolation.The core of any Bubble Tea application revolves around three functions:
gotype model struct {activeTab intcursor int}func (m model) Init() tea.Cmd {return nil}func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {switch msg := msg.(type) {case tea.KeyMsg:switch msg.String() {case "q", "ctrl+c":return m, tea.Quitcase "j", "down":m.cursor++case "k", "up":if m.cursor > 0 { m.cursor-- }case "tab":m.activeTab = (m.activeTab + 1) % 4}}return m, nil}func (m model) View() string {return lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Render("Interactive SSH Application Content")}
Wish connects an incoming SSH session directly into your Bubble Tea model:
gopackage mainimport (tea "github.com/charmbracelet/bubbletea""github.com/charmbracelet/lipgloss""github.com/charmbracelet/ssh""github.com/charmbracelet/wish"wishbubbletea "github.com/charmbracelet/wish/bubbletea""github.com/muesli/termenv")func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {pty, _, active := s.Pty()if !active {wish.Fatalln(s, "no active terminal allocated")return nil, nil}// Ensure full 24-bit TrueColor profile is sent to the clientlipgloss.SetColorProfile(termenv.TrueColor)m := newAppModel()return m, []tea.ProgramOption{tea.WithAltScreen(),tea.WithMouseCellMotion(),}}func main() {s, _ := wish.NewServer(wish.WithAddress("0.0.0.0:22"),wish.WithHostKeyPath(".ssh/id_ed25519"),wish.WithMiddleware(wishbubbletea.Middleware(teaHandler)),)s.ListenAndServe()}
When applications run as background services (e.g. systemd or Docker), they often lack an attached TTY on the host process. This can cause terminal style detection to fall back to a 1-bit monochrome (black & white) mode.
To ensure your users always get rich 24-bit RGB colors, explicitly force the TrueColor profile on every session:
golipgloss.SetColorProfile(termenv.TrueColor)
To allow visitors to connect with standard ssh yourdomain.com without needing a -p port flag, your app must bind to Port 22.
On Linux servers (AWS, DigitalOcean, Hetzner, etc.), standard OpenSSH (sshd) already listens on port 22. To avoid locking yourself out:
/etc/ssh/sshd_config to Port 2222 so you can always log into the server for administration (ssh -p 2222 user@host).systemd service or Docker container binding to 0.0.0.0:22.A record in your DNS provider pointing yourdomain.com directly to your server's public IP (ensure HTTP proxying is disabled since SSH is raw TCP traffic).Building apps and portfolios over SSH provides an ultra-fast, nostalgic, and dependency-free experience for users. With Go, Wish, and Bubble Tea, you can turn any CLI idea into a globally accessible terminal experience in an afternoon.
ssh ssh.vewake.meCrafting software, ricing Linux & building local-first tools.