Marchat is a self-hosted terminal chat application designed to provide real-time messaging over WebSockets with optional end-to-end encryption and a flexible plugin ecosystem. It supports SQLite, PostgreSQL, or MySQL as database backends and offers a lightweight, developer-friendly solution for teams that prefer command-line interfaces.
Key Features:
Real-time messaging using WebSockets for fast communication.
Optional E2E encryption with ChaCha20-Poly1305, enabling secure file transfers and message payloads.
Extensible plugin system for custom functionality via text commands or hotkeys.
Support for SQLite (default), PostgreSQL, and MySQL to accommodate various deployment needs.
Terminal UI built with Bubble Tea, offering a modern and responsive TUI experience.
Self-hosted architecture with no external services required.
Audience & Benefit:
Ideal for developers and self-hosted teams seeking a terminal-first chat solution. Marchat provides secure communication without the need for per-user forward secrecy, making it suitable for organizations that can manage shared encryption keys. Its cross-platform support (Linux, macOS, Windows, Android/Termux) and lightweight design make it an excellent choice for resource-conscious environments.
README
marchat
A lightweight terminal chat with real-time messaging over WebSockets, optional E2E encryption, and a flexible plugin ecosystem. Built for developers who prefer the command line.
Quick start:QUICKSTART.md for a single-page walkthrough (install, server, client, and where to read next).
E2E in one sentence: Optional chat encryption uses a shared symmetric key (ChaCha20-Poly1305) distributed out of band (MARCHAT_GLOBAL_E2E_KEY, keystore, or equivalent). It is not pairwise Signal-style key exchange: anyone who holds the key can decrypt message and file payloads. See SECURITY.md and PROTOCOL.md.
Good fit: self-hosted teams, terminal-first chat, SQLite or Postgres/MySQL, and optional plugins. Poor fit: hosted SaaS you do not run, or a threat model that requires per-user forward secrecy without a shared secret.
main branch: may include changes not yet in that tag (for example items under Unreleased in CHANGELOG.md). Feature descriptions elsewhere in this README match the tree you build from source; compare your binary’s -doctor / -version output to the release page when in doubt.
Demos
Screen recordings of a current build (GIF autoplay depends on the viewer).
Server: startup banner and web admin panel
Server: diagnostics (marchat-server -doctor)
Client: reactions and help
Client: theme switching (:theme)
Client: diagnostics (marchat-client -doctor)
Features
Terminal UI - Beautiful TUI built with Bubble Tea
Real-time Chat - Fast WebSocket messaging with SQLite, PostgreSQL, or MySQL backends
Message Management - Edit, delete, pin, react to, and search messages
Direct Messages - Private DM conversations between users, persisted server-side and replayed on reconnect for sender and recipient
Channels - Multiple chat rooms with join/leave, persisted channel metadata on messages, and fast local channel switching with filtered transcript history
Typing Indicators - Channel and DM typing indicators are scoped to the active view
Read Receipts - Server stores per-user read rows; the reference client sends debounced read_receipt when the transcript is scrolled to the newest messages
Plugin System - Remote registry with text commands and Alt+key hotkeys
E2E Encryption - ChaCha20-Poly1305 with a shared global key (MARCHAT_GLOBAL_E2E_KEY), including file transfers
File Sharing - Send files up to 1MB (configurable) with interactive picker and optional E2E encryption
Admin Controls - User management, bans, kick system with ban history gaps
Smart Notifications - Bell + desktop notifications with quiet hours and focus mode (guide)
Themes - Built-in themes + custom themes via JSON (guide)
Docker Support - Containerized deployment with docker-compose.yml for local dev; optional TLS reverse proxy via Caddy (guide)
Health Monitoring - /health and /health/simple endpoints with system metrics
Structured Logging - JSON logs with component separation and user tracking
UX Enhancements - Stable status footer (connection, unread for others' new chat lines when scrolled above the tail, optional E2E and channel), banner for command feedback, tab completion for @mentions, multi-line input, chat export
Cross-Platform - Runs on Linux, macOS, Windows, and Android/Termux
Diagnostics - marchat-client -doctor and marchat-server -doctor (or -doctor-json) summarize environment, resolved paths, and configuration health
Overview
marchat started as a fun weekend project for father-son coding sessions and has evolved into a lightweight, self-hosted terminal chat application designed specifically for developers who love the command line. It supports SQLite by default and can also run against PostgreSQL or MySQL for larger deployments.
Key Benefits:
Self-hosted: No external services required
Cross-platform: Linux, macOS, Windows, and Android/Termux
Secure: Optional E2E encryption with ChaCha20-Poly1305 (global symmetric key)
Extensible: Plugin ecosystem for custom functionality
Lightweight: Minimal resource usage, perfect for servers
Quick Start
1. Generate Admin Key
openssl rand -hex 32
2. Start Server
Option A: Environment Variables (Recommended)
export MARCHAT_ADMIN_KEY="your-generated-key"
export MARCHAT_USERS="admin1,admin2"
./marchat-server
# With admin panel
./marchat-server --admin-panel
# With web panel
./marchat-server --web-panel
Runs a guided wizard only whenMARCHAT_ADMIN_KEY or MARCHAT_USERS is not set. If they are already in the environment or config/.env, the server starts normally and --interactive does nothing extra.
3. Connect Client
# Admin connection
./marchat-client --username admin1 --admin --admin-key your-key --server ws://localhost:8080/ws
# Regular user
./marchat-client --username user1 --server ws://localhost:8080/ws
# Or use interactive mode
./marchat-client
Database Schema
Tables created by the server (dialect-aware DDL for SQLite, PostgreSQL, and MySQL):
messages: Core message storage with message_id, encryption fields, edit/delete/pin flags, recipient, and persisted channel
user_message_state: Per-user message history state and last-seen timestamp
ban_history: Ban/unban event tracking for history gaps
message_reactions: Durable emoji reactions (unique per message + user + emoji)
user_channels: Last channel per user, persisted across reconnects
read_receipts: Per-user read receipt state tracking
winget:winget install Cod-e-Codes.Marchat (listed on winget). Maintainer notes and checksum alignment: PACKAGING.md.
See PACKAGING.md and packaging/ for Homebrew, Scoop, winget, Chocolatey, and AUR: canonical manifest templates here, installs available on those ecosystems, and how each release lines up with downstream publishes.
The server service loads config/.env first, then a project-root .env (both optional and gitignored). Put MARCHAT_ADMIN_KEY and MARCHAT_USERS in either file (see Essential Environment Variables). Compose also sets MARCHAT_DB_PATH=/data/marchat.db so SQLite uses the attached volume.
Example snippet for config/.env or .env (generate a strong key for anything reachable from a network):
TLS reverse proxy (Caddy, optional): To terminate TLS in front of a host-nativemarchat-server (plain HTTP on port 8080), use docker-compose.proxy.yml, deploy/caddy/Caddyfile, and deploy/caddy/proxy.env.example plus optional gitignored deploy/caddy/proxy.env for MARCHAT_CADDY_EXTRA_HOSTS (public IP/DNS on tls internal). Published port 8443 maps to HTTPS/WebSocket inside the container; clients use wss://localhost:8443/ws (with --skip-tls-verify while using Caddy’s internal CA). The proxy stack must be running whenever you use that URL. Full steps, helper scripts (scripts/build-windows.ps1 / scripts/build-linux.sh, scripts/connect-local-wss.ps1 / scripts/connect-local-wss.sh), source changes, and breaking notes: deploy/CADDY-REVERSE-PROXY.md.
From Source:
git clone https://github.com/Cod-e-Codes/marchat.git && cd marchat
go mod tidy
go build -o marchat-server ./cmd/server
go build -o marchat-client ./client
Linux clipboard support: sudo apt install xclip (Ubuntu/Debian) or sudo yum install xclip (RHEL/CentOS)
Terminal colors: The server startup banner and the client’s pre-chat output (connection, E2E status, profile picker tags such as [Admin] / [E2E], and auth prompts) use lipgloss for emphasis. Set NO_COLOR=1 (or NO_COLOR) in the environment to disable colors on plain stdout/stderr.
Configuration
Essential Environment Variables
Variable
Required
Default
Description
MARCHAT_ADMIN_KEY
Yes
-
Admin authentication key
MARCHAT_USERS
Yes
-
Comma-separated admin usernames
MARCHAT_PORT
No
8080
Server port
MARCHAT_DB_PATH
No
./config/marchat.db
Database path/DSN. Supports SQLite file path, postgres://..., or mysql:...
MARCHAT_TLS_CERT_FILE
No
-
TLS certificate (enables wss://)
MARCHAT_TLS_KEY_FILE
No
-
TLS private key
MARCHAT_GLOBAL_E2E_KEY
No
-
Base64 32-byte global E2E key (server and/or client). On the client, if set, it overrides the key from keystore.dat for that run only; the keystore file is not updated. See E2E Encryption.
PostgreSQL requires a reachable database and credentials with schema/table create permissions.
MySQL DSNs should include parseTime=true so timestamp fields decode correctly.
MariaDB generally works with the same mysql: DSN shape and parseTime=true as MySQL.
The server creates dialect-specific DDL (for example, MySQL/MariaDB use fixed-width strings where indexes, primary keys, or unique constraints apply, because full TEXT keys are rejected). Long message bodies still use a large text type.
SQLite remains the easiest local development option.
Doctor / diagnostics: Set MARCHAT_DOCTOR_NO_NETWORK to 1 to skip the GitHub latest-release check in -doctor / -doctor-json.
File Size Configuration: Use either MARCHAT_MAX_FILE_BYTES (exact bytes) or MARCHAT_MAX_FILE_MB (megabytes). If both are set, MARCHAT_MAX_FILE_BYTES takes priority.
Interactive Setup: Use --interactive flag for guided server configuration when environment variables are missing.
Server: config/.env vs process environment
The server loads {config directory}/.env (for a repo clone, usually config/.env) if the file exists, using godotenv.Overload.
Situation
Effect
A variable appears in .env
That value replaces the same name already in the process environment when the server starts.
A variable is set only in the environment (not in .env)
It is unchanged by .env loading.
No .env file
Configuration comes only from the environment, flags, and defaults.
Why: Older godotenv.Load behavior skipped keys already set in the environment, so a stale shell MARCHAT_ADMIN_KEY could override an updated config/.env. Overload makes the file authoritative for any key it defines.
Operational notes: Restart the server after editing .env. If you deploy with both injected secrets and a mounted .env, any overlapping key in the file wins at startup. See deploy/CADDY-REVERSE-PROXY.md for migration and edge cases.
Not the same as Docker Compose’s .env: Compose’s file next to docker-compose.yml is for substituting${VAR} into YAML; the table above is about marchat-server reading config/.env at runtime.
Client vs server config locations
Role
Default location
Override
Server (.env, SQLite DB, debug log)
In development from a repo clone: ./config next to go.mod. Otherwise MARCHAT_CONFIG_DIR or the user config path (see ARCHITECTURE.md).
Per-user app data (e.g. Windows %APPDATA%\marchat, Linux/macOS ~/.config/marchat). Same when developing from source.
MARCHAT_CONFIG_DIR
Keystore file: The client uses keystore.dat under MARCHAT_CONFIG_DIR or the default app data directory when that file exists; if MARCHAT_CONFIG_DIR is set and has no keystore yet, it still uses an existing keystore.dat in the standard per-user marchat folder. Only after those checks does it use legacy ./keystore.dat in the process working directory, so a stray file in a repo clone does not override your real profile keystore.
The repository’s config/ directory holds server runtime files and the Go packagegithub.com/Cod-e-Codes/marchat/config; it is not the client’s profile folder.
Diagnostics (-doctor)
Run ./marchat-client -doctor or ./marchat-server -doctor for a text report (paths, redacted MARCHAT_* secrets as length-only, other env values as shown, sanity checks). Server doctor lists MARCHAT_*after loading the resolved config directory’s .env (same as the running server), so values are not limited to what your shell exported. Client doctor only shows variables present in the client process (it does not read the server’s config/.env); it also lists experimental client hook env vars and validates receive/send hook paths when set (see CLIENT_HOOKS.md). Client doctor now also reports local DM UI state file status (dm_state.json) and whether E2E key source for this run is env or keystore. Server doctor does not list those client-only hook variables, even if they are set in the shell (for example when you run client and server from the same session). Server doctor also reports the detected DB dialect, validates the configured DB connection string format, attempts a DB ping, and reports DM history privacy behavior. On a color-capable terminal (stdout is a TTY), the text report uses ANSI colors aligned with the server pre-TUI banner; set NO_COLOR or redirect to a file/pipe for plain output. Use -doctor-json for machine-readable output (never colorized). If both flags were passed, -doctor-json wins. Exits without starting the TUI or listening on a port. See ARCHITECTURE.md for details.
Admin Commands
User Management
Command
Description
Hotkey
:ban
Permanent ban
Ctrl+B (with user selected)
:kick
24h temporary ban
Ctrl+K (with user selected)
:unban
Remove permanent ban
Ctrl+Shift+B
:allow
Override kick early
Ctrl+Shift+A
:forcedisconnect
Force disconnect user
Ctrl+F (with user selected)
:cleanup
Clean stale connections
-
Database Operations (:cleardb or Ctrl+D menu)
Clear DB - Wipe all messages
Backup DB - Create database backup
Show Stats - Display database statistics
User Commands
General
Command
Description
Hotkey
:theme
Switch theme (built-in or custom)
Ctrl+T (cycles)
:themes
List all available themes
-
:time
Toggle 12/24-hour format
Alt+T
:msginfo
Toggle message metadata (message id / encrypted) on chat lines
Alt+M
:clear
Clear chat buffer
Ctrl+L
:q
Quit application (vim-style)
-
:sendfile [path]
Send file (or open picker without path)
Alt+F
:savefile
Save received file
-
:code
Open code composer with syntax highlighting
Alt+C
:export [file]
Export chat history to a text file
-
Messaging
Command
Description
:edit
Edit your own message by ID (admins cannot edit others' messages; with E2E on, the new text is encrypted like normal chat and the server keeps is_encrypted in sync)
:delete
Delete a message by its ID
:dm [user] [msg]
Send a DM, switch to a DM conversation (:dm ), or return to global chat (:dm off)
:dms
List DM conversations currently visible in the sidebar (includes unread counts)
:dmhide [user]
Hide a DM conversation from the sidebar (or hide the active DM thread when no user is provided)
:search
Search message history on the server
:react
React to a message (supports aliases: +1, heart, fire, party, laugh, eyes, check, rocket, think, etc.)
:pin
Toggle pin on a message
:pinned
List all pinned messages
DM unread rule in the reference TUI:
Unread for a DM thread counts inbound DM messages from that user while that thread is not active.
Opening a DM thread marks that thread as read immediately.
Unread tracking and hidden thread state are saved in the client config directory (dm_state.json) and restored on reconnect.
Hidden threads reappear automatically when a new inbound DM arrives from that user.
Channels
Command
Description
:join
Join a channel (clients start in #general)
:leave
Leave current channel, return to #general
:channels
List active channels with user counts
Notifications
Command
Description
Hotkey
:notify-mode
Set notification mode (none/bell/desktop/both)
Alt+N (toggle desktop)
:bell
Toggle bell notifications
-
:bell-mention
Toggle mention-only notifications
-
:focus [duration]
Enable focus mode (mute notifications)
-
:quiet
Set quiet hours (e.g., :quiet 22 8)
-
> Note: Hotkeys work in both encrypted and unencrypted sessions since they're handled client-side.
>
> Notifications: See NOTIFICATIONS.md for full notification system documentation including desktop notifications, quiet hours, and focus mode.
Plugin Commands
Plugin management (install, uninstall, enable, disable) is admin-only. Plugin chat commands (e.g. :echo, :weather) are available to all users unless the plugin manifest sets AdminOnly: true. See Plugin Management hotkeys for keyboard shortcuts.
Command
Description
Hotkey
:store
Browse plugin store
Alt+S
:plugin list or :list
List installed plugins
Alt+P
:plugin install or :install
Install plugin
Alt+I
:plugin uninstall or :uninstall
Uninstall plugin
Alt+U
:plugin enable or :enable
Enable plugin
Alt+E
:plugin disable or :disable
Disable plugin
Alt+D
:refresh
Refresh plugin list from registry
Alt+R
> Note: Both text commands and hotkeys work in E2E encrypted sessions (sent as admin messages that bypass encryption).
File Sharing
Direct send:
:sendfile /path/to/file.txt
Interactive picker:
:sendfile
Navigate with arrow keys, Enter to select/open folders, ".. (Parent Directory)" to go up.
Public deployments: Server accessible from internet
Production environments: Enhanced security required
Corporate networks: Security policy compliance
HTTPS reverse proxies: Behind nginx, traefik, Caddy, etc.
Reverse proxy (Caddy)
The repo includes a Docker Compose-based Caddy setup for local or LAN use: docker-compose.proxy.yml, deploy/caddy/Caddyfile, deploy/caddy/proxy.env.example (and optional local deploy/caddy/proxy.env), and the walkthrough deploy/CADDY-REVERSE-PROXY.md (build flags, config/.env, firewall, wss:// client flags, E2E, and breaking change: config/.env is applied with godotenv.Overload so file values override pre-set MARCHAT_* in the process environment).
Quick reference:
Item
Role
marchat-server on the host
Listens on 8080 (ws://), reads config/.env
docker compose -f docker-compose.proxy.yml up -d
Runs Caddy; host 8443 → container 443
Client
wss://localhost:8443/ws + --skip-tls-verify until you use a public CA cert on Caddy
If 8443 is refused
Caddy is not running; start the compose stack or use ws://127.0.0.1:8080/ws
> Warning: Use --skip-tls-verify only for development. Production should use valid CA-signed certificates.
E2E Encryption
Global encryption for secure group chat using shared keys across all clients.
How It Works
Shared Key Model: All clients use the same 32-byte global key for encrypted chat (and optional encrypted files)
Simplified Management: No per-user public-key exchange on the wire; distribute the key out-of-band (env var or copy from first client)
ChaCha20-Poly1305: Authenticated encryption for payloads; see PROTOCOL.md for the on-the-wire layout
Environment Variable: MARCHAT_GLOBAL_E2E_KEY (base64) for key distribution
Auto-Generation: First client can generate a key if none is provided (then share it to peers)
Setup Options
Option 1: Shared Key (Recommended)
# Generate 32-byte key
openssl rand -base64 32
# Set on all clients
export MARCHAT_GLOBAL_E2E_KEY="your-generated-key"
# Connect with E2E
./marchat-client --e2e --keystore-passphrase your-pass --username alice --server ws://localhost:8080/ws
Option 2: Auto-Generate
# Client generates a key and saves it to the encrypted keystore (raw key is not printed)
./marchat-client --e2e --keystore-passphrase your-pass --username alice --server ws://localhost:8080/ws
# Output shows Key ID only, e.g.:
# [INFO] Generated new global E2E key (ID: RsLi9ON0...)
# [TIP] The key is not printed ... (copy keystore.dat + passphrase, or pre-share MARCHAT_GLOBAL_E2E_KEY)
Expected Output
[INFO] Using global E2E key from environment variable
E2E encryption enabled
Using global E2E key from environment variable
Global chat encryption: ENABLED (Key ID: RsLi9ON0...)
Encryption validation passed
E2E encryption enabled with keystore: config/keystore.dat
Security Features
Server Privacy: Server cannot read encrypted message bodies when E2E is used
Local Keystore: Global key stored in a passphrase-protected file (PBKDF2 + AES-GCM); see Keystore file format below
No raw key on stdout: Auto-generated global keys are not echoed in full (reduces exposure via logs, scrollback, or screen capture); only a Key ID is shown in [INFO].
Validation: Automatic encryption/decryption round-trip test on startup
Keystore file format and MARCHAT_GLOBAL_E2E_KEY
On-disk format (current): keystore.dat is a small binary file: a fixed magic and version, a random 16-byte salt stored in the file, then AES-GCM ciphertext of the JSON payload (including the global ChaCha20-Poly1305 key). The passphrase is stretched with PBKDF2 (SHA-256, 100k iterations) using that embedded salt. This means the same passphrase unlocks the file even if the absolute path to keystore.dat changes (for example after moving the file or when the client resolves a different config directory). Older files that derived PBKDF2 salt from the keystore path are still supported: on first successful unlock they are rewritten in the new format.
Environment variable vs file: If MARCHAT_GLOBAL_E2E_KEY is set in the client process, that key is used for encryption/decryption for this run. The on-disk keystore is not modified. You will see [INFO] Using global E2E key from environment variable. If you later unset the variable, the client uses the key from keystore.dat again, so the effective key can appear to “change back” even though the file was never updated. To persist a shared key in the file, run without the env var once. When the client auto-generates a key, it does not print the raw base64 material (only a Key ID); share the key with other clients by copying keystore.dat and the same passphrase, or by agreeing on MARCHAT_GLOBAL_E2E_KEY beforehand (e.g. openssl rand -base64 32 on a trusted machine).
Which file is used: Same resolution order as in Client vs server config locations above (primary config dir → per-user marchat keystore when override is empty → cwd legacy).
Legacy note: Keystore wrapping was previously upgraded to PBKDF2 (replacing an older derivation). Very old keystores from that era may still need re-initialization if they cannot be decrypted.
Plugin System
Extend functionality with remote plugins from configured registry.
Default registry source: Cod-e-Codes/marchat-plugins.
The TUI client can spawn optional external programs on send/receive and pass one JSON line on stdin per event, useful for custom logging, bridges, or local tooling. This is experimental (protocol may change); it does not replace server plugins or built-in notifications.
Protects against log injection and command injection
Troubleshooting
Issue
Solution
Wrong config folder / paths
Run ./marchat-client -doctor or ./marchat-server -doctor (add -doctor-json for scripts; set NO_COLOR for plain text). See Client vs server config locations. Server: if you set MARCHAT_CONFIG_DIR only in config/.env, restart after saving; the loader re-reads it after Overload.
Connection failed
Use ws:// or wss:// and the path your server uses (default HTTP handler is /ws, e.g. ws://host:8080/ws).
Ensure Caddy (or your proxy) is up: docker compose -f docker-compose.proxy.yml up -d, or connect directly with ws://127.0.0.1:8080/ws (reverse proxy guide).
Admin commands not working
Client must use --admin and --admin-key matching the server’s MARCHAT_ADMIN_KEY; username must be listed in MARCHAT_USERS.
Clipboard issues (Linux)
Install a clipboard tool (e.g. sudo apt install xclip or xsel).
Port in use
Set MARCHAT_PORT (e.g. 8081) in the environment or config/.env and restart the server.
Database migration fails
Check file permissions; back up the database before upgrades; run the same server binary version that created the schema.
PostgreSQL connection fails
Verify URL format: postgres://user:pass@host:5432/db?sslmode=disable; test with psql using the same credentials.
MySQL connection fails
Verify DSN prefix mysql: and body user:pass@tcp(host:3306)/db?parseTime=true; test with the mysql CLI.
SQL syntax error after backend switch
Ensure tables were created by the current server version and restart after changing MARCHAT_DB_PATH.
Message history looks incomplete
History depends on channel, per-user message state, and server filters. Ban/unban and related flows can reset stored state so scrollback differs from the raw DB.
Transcript resets after reconnect
On each successful WebSocket connect the reference client clears local messages and rebuilds from the server handshake replay (up to 50 recent lines). That avoids duplicates when the server comes back while the client stayed open. Lines older than that replay window are not shown again in that session unless you saved them with :export earlier. Third-party clients should replace or dedupe history on handshake; see PROTOCOL.md (Server Behavior).
Banner stuck on [Sending...]
Current client clears sending after each successful WebSocket write for normal chat and for admin_command, so dropped or slow server work does not leave the banner stuck. Current server sends a System line for unknown admin : commands (Unknown command: plus the token) and one System line when per-connection message rate burst is exceeded (see PROTOCOL.md Rate Limiting).
Footer unread looks wrong
The reference client increments unread only for other users' new text, dm, or file while the transcript is not at the bottom. It does not increment for typing, reactions, read receipts, edits, deletes, or your own echoed sends. See ARCHITECTURE.md (client).
Ban history gaps not working
Set MARCHAT_BAN_HISTORY_GAPS=true (default off). The server creates the ban_history table when using a database backend that runs marchat migrations.
TLS certificate errors
For dev/self-signed certs, pass --skip-tls-verify on the client (or enable Skip TLS verify in the profile / interactive setup).
Plugin installation fails
Check registry URL (MARCHAT_PLUGIN_REGISTRY_URL), network access, and JSON validity; commercial plugins need a valid license for the plugin name (see PLUGIN_ECOSYSTEM.md).
E2E encryption errors
Use --e2e and the keystore passphrase; see E2E Encryption (keystore path, MARCHAT_GLOBAL_E2E_KEY vs file). Client and server must share the same global key material.
Global E2E key errors
Key must be base64 encoding 32 raw bytes (openssl rand -base64 32). MARCHAT_GLOBAL_E2E_KEY overrides the in-memory key for that process and is not written to the keystore file.
:savefile picks the wrong payload when names collide
Received files are stored per sender internally, but :savefile matches basename only. If two users sent the same filename, which copy is saved is not deterministic; ask for distinct names or avoid duplicate basenames until disambiguation is exposed in the UI.
Send file / nothing happens
Check the footer (Connected vs Disconnected). If disconnected, :sendfile should report Not connected to server; reconnect, then retry (including Alt+F after a connection is up).
Username already taken
A live or stale session may still hold the name. Admin: :forcedisconnect . Otherwise the server’s ~5 minute WebSocket ping sweep removes broken clients (or run :cleanup).
Stale / ghost sessions
Same as above: wait for the ping sweep, run :cleanup, or :forcedisconnect.
Multi-line input not working
Use Alt+Enter or Ctrl+J in the input (plain Enter sends). Shift+Enter is unreliable on many Windows terminals.
Doctor / CI noise
For automated checks, use -doctor-json. Secret values are redacted to length only (no suffix).
Stale Connection Management
Automatic: The hub runs CleanupStaleConnections about every 5 minutes: it sends a WebSocket ping per client; failures remove the client and free the username.
Manual (admin, from the client):
:cleanup # Run stale check now for all clients
:forcedisconnect # Drop a specific connected user
Typical cases:
Abrupt client exit: may linger until the next ping sweep or :cleanup.
Half-open TCP: same; :forcedisconnect clears it immediately if the server still lists the user.
Immediate reclaim of a name: use :forcedisconnect (do not rely on the sweep if you need instant reuse).
Testing
Foundational test suite covering core functionality, cryptography, and plugins. CI (.github/workflows/go.yml) runs the full suite with the race detector and a separate database-smoke job against Postgres and MySQL (see TESTING.md).
Running Tests
go test ./... # Run all tests (main module only)
go test -cover ./... # With coverage
go test ./server -v # Specific package
go test ./... -timeout 10s # With timeout (CI recommended)
cd plugin/sdk && go test ./... # Nested SDK module (separate go.mod)
Test Scripts
Linux/macOS: ./test.sh
Windows: .\test.ps1
Coverage Summary
Percentages are statement coverage from a merged profile (go test -coverprofile=... ./... then go tool cover -func=...). Size is non-test .go lines per package (approximate). Authoritative tables and regeneration steps: TESTING.md. The nested plugin/sdk module is measured separately (58.8% statements); see TESTING.md.
Package
Coverage
Size
Status
shared
88.1%
253 LOC
High
plugin/license
87.1%
246 LOC
High
client/crypto
80.3%
387 LOC
High
config
73.2%
339 LOC
High
plugin/host
64.6%
721 LOC
Medium
client/config
58.0%
1993 LOC
Medium
internal/doctor
66.5%
826 LOC
Medium
plugin/store
47.0%
552 LOC
Medium
cmd/license
42.2%
160 LOC
Medium
server
38.5%
7309 LOC
Low
plugin/manager
32.1%
747 LOC
Low
client/exthook
24.1%
204 LOC
Low
client
25.9%
6330 LOC
Low
cmd/server
13.7%
484 LOC
Low
Overall: 39.7% (main module packages only). See TESTING.md for detailed information.
Contributing
We welcome contributions! See CONTRIBUTING.md for workflow, required checks, and a suggested reading order before your first PR.
Clone and test:
git clone https://github.com/Cod-e-Codes/marchat.git
cd marchat
go mod tidy
go test ./...