Self-hosted terminal chat. Your server, your data, your rules. Setup in under 10 minutes.
XtermChat is a lightweight, self-hosted chat system built for the terminal. Unlike third-party platforms, XtermChat stores nothing on external servers. You deploy the server on your own VPS, connect with the CLI client or a browser, and all messages stay on infrastructure you control.
It is designed for developers and sysadmins who want something simple that just works — without the overhead of Matrix, the enterprise focus of Mattermost, or the data concerns of Slack.
| XtermChat | Matrix | Mattermost | Slack | |
|---|---|---|---|---|
| Self-hosted | ✓ Yes | ✓ Yes | ✓ Yes | ✗ No |
| Open source | ✓ MIT | ✓ Yes | ⚠ Partial | ✗ No |
| Setup time | ~5 min | ~2 hrs | ~30 min | Instant |
| RAM usage | ~30 MB | ~1–2 GB | ~300 MB | — |
| Terminal-first | ✓ Yes | ✗ No | ✗ No | ✗ No |
| Your data | ✓ Yes | ✓ Yes | ✓ Yes | ✗ No |
XtermChat has two parts: a server you deploy on a VPS, and a client your team installs on their machines. They communicate over a simple REST API with JSON payloads.
Python + Flask. Two pip packages. SQLite3 — no external database needed.
Built with prompt_toolkit. Full TUI with sidebar, emoji, link detection, and copy.
Flask serves static HTML. Same API as CLI — web and terminal users share rooms.
CLI uses hardware UUID (device-bound). Web uses a 5-digit PIN. No accounts.
make for installxclip or xsel (Linux copy)python xtc.py directlyclip built-inThe cheapest VPS tier (DigitalOcean, Hetzner, Vultr — $4–6/month, 512 MB RAM) is more than enough for a team of 2–50 people.
SSH into your VPS and follow these steps. The entire process takes about 5 minutes.
Clone the server repository
git clone https://github.com/dnysaz/xtc-server.git
cd xtc-serverInstall dependencies
pip3 install flask flask-cors werkzeugOpen port 8080
sudo ufw allow 8080
sudo ufw enable
sudo ufw status
If your VPS has a separate firewall panel (DigitalOcean, Vultr), also open TCP 8080 there.
Start the server
python3 server.py start
# [*] Server started in background (PID: 12345)Verify it's running
curl http://localhost:8080
# {"service": "XtermChat Gateway", "status": "online", "version": "1.1"}Default is 8080. Edit the bottom of server.py to change it:
elif command == "run_internal":
app.run(host='0.0.0.0', port=9000) # ← change here
else:
app.run(host='0.0.0.0', port=9000) # ← and here
Clone the client
git clone https://github.com/dnysaz/xtc-client.git
cd xtc-clientInstall — creates global xtc command
make installVerify installation
xtcInstall Python 3.10+ from python.org — check "Add to PATH" during install
Install Windows Terminal from the Microsoft Store for best Unicode and emoji support
Clone and install
git clone https://github.com/dnysaz/xtc-client.git
cd xtc-client
pip install -r requirements.txtRun
python xtc.pyConfiguration saved to ~/.xtc_config.json. Managed by xtc connect and xtc disconnect.
| Command | Args | Description |
|---|---|---|
xtc connect | @IP:PORT | Save server address to config |
xtc disconnect | @IP:PORT | Remove saved server config |
xtc status | — | Check connection and latency |
xtc list:rooms | — | List all rooms on the server |
xtc create:room | @name [pass] | Create a room. No args = interactive |
xtc delete:room | @name | Delete a room (creator only) |
xtc start:chat | @name | Open interactive chat TUI |
xtc start:web | — | Start web UI at localhost:5000 |
# Connect to server
xtc connect @103.45.67.89:8080
# Create a private room with password
xtc create:room @devteam mypassword
# Interactive room creation
xtc create:room
# Join a private room (password prompted automatically)
xtc start:chat @devteam
Running xtc start:chat opens a full terminal UI with three panels and a dual-focus mode system.
Cursor in message box. Type and send. Commands :clear, :purge, :e active.
Cursor in chat history. Scroll, select text, copy, open links. Auto-scroll pauses.
Global
INPUT MODE
:clearClear local chat display (no server effect)
:purgeDelete all messages from server (creator only)
:eToggle emoji shortcuts panel
:qQuit chat
CHAT MODE (after pressing Tab)
XtermChat includes a browser-based UI. Start it with:
xtc start:web
# URL: http://localhost:5000
Open http://localhost:5000. Enter server IP, port, username, and a 5-digit PIN to connect.
Web PIN is a 5-digit number you choose — unlike CLI which uses hardware UUID automatically. Both clients share the same rooms and messages.
| CLI | Web | |
|---|---|---|
| PIN type | Hardware UUID (auto) | 5-digit (user-chosen) |
| Device-bound | ✓ Yes | ✗ No |
| Installation | Required | None (browser only) |
| Mobile support | ✗ No | ✓ Yes |
# Public room — anyone on the server can join
xtc create:room @general
# Private room — password required
xtc create:room @team secretpassword
# Interactive mode — prompts for name, password, description
xtc create:room
Private rooms appear in xtc list:rooms with a LOCKED label. Passwords are stored as bcrypt hashes — the raw password is never saved.
When you create a room, your hardware UUID is recorded as creator_pin. Only you (from the same machine) can:
xtc delete:room @roomname:purge inside chat:purge deletes all messages permanently from the server. The room itself remains. This action cannot be undone.
Type any shortcut in a message — it auto-converts when you press Enter. Type :e to open the reference panel inside the chat.
| Shortcut | Emoji | Shortcut | Emoji | Shortcut | Emoji |
|---|---|---|---|---|---|
:fire | 🔥 | :check | ✅ | :robot | 🤖 |
:rocket | 🚀 | :warn | ⚠️ | :bug | 🪲 |
:nice | 👍 | :heart | ❤️ | :coffee | ☕ |
:cool | 😎 | :star | ⭐ | :beer | 🍺 |
:laugh | 😂 | :ghost | 👻 | :globe | 🌐 |
:smile | 😊 | :party | 🎉 | :key | 🔑 |
:pray | 🙏 | :100 | 💯 | :skull | 💀 |
:muscle | 💪 | :zap | ⚡ | :lock | 🔒 |
:cloud | ☁️ | :box | 📦 | :top | 🔝 |
| Action | Validation |
|---|---|
| Send message | Username + PIN must match registered record |
| Create room | PIN stored as creator_pin |
| Purge messages | Requester PIN must match creator_pin |
| Delete room | Username + PIN must match creator |
| Join private room | Room password (verified via bcrypt) |
| Web login | Username + PIN must match registered record |
Default setup uses HTTP, acceptable for private/internal networks. For public-facing deployments use HTTPS — see the next section.
No client code changes needed. Just point your client at an https:// URL after setup.
Automatically obtains and renews SSL certificates from Let's Encrypt.
sudo apt install caddy -y
# /etc/caddy/Caddyfile
yourdomain.com {
reverse_proxy localhost:8080
}
sudo systemctl reload caddy
sudo apt install nginx certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
After setup, update your client:
xtc connect @yourdomain.com
python3 server.py start # start in background
python3 server.py stop # stop
python3 server.py # run in foreground (debug)
cat server.pid
ps aux | grep server.py
tail -f server.log
grep "ERROR\|Exception" server.log
# /etc/systemd/system/xtermchat.service
[Unit]
Description=XtermChat Server
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/xtc-server
ExecStart=/usr/bin/python3 /home/ubuntu/xtc-server/server.py run_internal
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable xtermchat
sudo systemctl start xtermchat
# Update
cd xtc-server && python3 server.py stop && git pull && python3 server.py start
# Backup — entire state is one file
cp xtc.db xtc_backup_$(date +%Y%m%d_%H%M).db
Everything is stored in xtc.db — a single SQLite file auto-created on first run.
| Table | Columns | Purpose |
|---|---|---|
users | username PK, pin | Identity registry |
rooms | name UNIQUE, creator, password, description, created_at, creator_pin | Room config & ownership |
messages | id AI, room, sender, pin, content, timestamp | Message history |
# Inspect with check_db.py
python3 check_db.py
# Direct SQLite
sqlite3 xtc.db
.tables
SELECT * FROM rooms;
SELECT COUNT(*) FROM messages WHERE room='general';
SELECT * FROM messages ORDER BY id DESC LIMIT 20;
.quit
# Full reset
python3 server.py stop && rm xtc.db && python3 server.py start
Base URL: http://YOUR_SERVER:8080 — all endpoints return JSON.
{"status": "online", "service": "XtermChat Gateway", "version": "1.1"}
// Request
{"user": "ketut", "pin": "12345"}
// 200 — PIN matches {"status": "success", "message": "Welcome back"}
// 201 — new user {"status": "success", "message": "New identity registered"}
// 403 — PIN mismatch {"status": "failed", "message": "Identity locked to another device/PIN."}
{"status": "success", "count": 2, "rooms": [
{"name": "general", "has_password": false, "creator": "KETUT",
"description": "General chat", "created_at": 1741234567}
]}
{"room": "general", "password": "", "user": "KETUT", "content": "Hello world", "pin": "UUID"}
// 201 → ok | 403 → PIN mismatch | 400 → empty or too long (max 4000 chars)
GET /messages/general?password=
// Response: [{"sender": "KETUT", "content": "Hello", "pin": "UUID", "timestamp": "2026-03-14 08:30:00"}]
// 401 → password required
{"room": "general", "user": "ketut", "pin": "UUID"}
// 200 → ok | 403 → Unauthorized: Hardware ID mismatch
{"room": "general", "user": "ketut", "pin": "UUID"}
// 200 → ok | 403 → Unauthorized: Hardware ID mismatch
curl http://YOUR_IP:8080 # test direct access
sudo ufw status # check firewall
cat server.pid # check if server running
ps aux | grep server.py
Username registered from a different machine. Options:
# Option 1 — use a different system username
# Option 2 — admin removes the old entry
sqlite3 xtc.db "DELETE FROM users WHERE username='yourname';"
You must be on the same machine that created the room. Hardware PIN is device-specific.
sudo lsof -i :8080
sudo kill -9 <PID>
rm server.pid && python3 server.py start
sudo apt install xclip # or: sudo apt install xsel
Minimum terminal size is 80×24. Press Tab to enter CHAT MODE first, then use arrow keys or PageUp/PageDown.
tail -50 server.log
rm server.pid && python3 server.py start
Yes. Messages are polled every 2 seconds. All connected users see new messages automatically.
Until the creator runs :purge (wipes messages, keeps room) or xtc delete:room (removes everything). Delete xtc.db to reset completely.
xtc connect @localhost:8080
Hardware PIN changes. Admin can reset:
sqlite3 xtc.db "DELETE FROM users WHERE username='yourname';"
Yes. Both use the same API and see each other's messages in real time.
SQLite handles millions of rows comfortably. Fine for 2–50 people on the cheapest VPS. For very high traffic, migrate to PostgreSQL by modifying db.py.
# Server
cd xtc-server && python3 server.py stop && git pull && python3 server.py start
# Client
cd xtc-client && git pull && make install
Database and config are preserved through updates.