Real-time websocket chat server with SQLite/Postgres persistence and a built-in TUI client.
- Multi-room websocket chat with join/leave events
- Message persistence in SQLite with history retrieval
- Room and user listing
- Configurable limits and timeouts
- CRDT utilities (OR-Set, LWW register)
- TUI client for local testing and demos
- Go 1.25+
CONVERGE_DB_PATH=converge.db CONVERGE_JWT_SECRET=dev-secret go run ./cmd/serverBy default, the server listens on 0.0.0.0:8080. You can change this using PORT, CONVERGE_HOST, or CONVERGE_ADDR.
On startup, the server will log its local network IP, making it easy to connect from other devices:
2026/03/04 00:58:05 Network access: http://192.168.1.5:8080
Postgres:
CONVERGE_DB_ADAPTER=postgres CONVERGE_DB_DSN="postgres://user:pass@localhost:5432/converge?sslmode=disable" CONVERGE_JWT_SECRET=dev-secret go run ./cmd/serverHealth check:
curl http://<server-ip>:8080/healthWebsocket URL:
ws://<server-ip>:8080/ws?room=lobby
For convenience, you can use the provided scripts in the scripts/ directory:
- Start Server:
./scripts/start_server.sh - Demo Client:
./scripts/demo_client.sh [user_id] [display_name] [server_url]
Example:
# In one terminal
./scripts/start_server.sh
# In another terminal
./scripts/demo_client.sh alice "Alice" ws://192.168.1.5:8080/wsAuthentication:
- Send
Authorization: Bearer <jwt> - Token must include
user_idorsub - Optional
display_nameornamefor stored display name
go run ./cmd/client -server ws://localhost:8080/ws -room lobby -token "$JWT_TOKEN"/join room/rooms/users [room]/history [limit]/quit
- OR-Set add
{ "type": "crdt_orset_add", "room": "lobby", "doc": "tags", "body": "alpha" }- OR-Set values
{ "type": "crdt_orset_values", "room": "lobby", "doc": "tags" }- LWW set/get
{ "type": "crdt_lww_set", "room": "lobby", "doc": "topic", "body": "Hello" }
{ "type": "crdt_lww_get", "room": "lobby", "doc": "topic" }- Send message
{ "type": "message", "body": "hello" }- Join room
{ "type": "join", "room": "dev" }- List rooms
{ "type": "rooms" }- List users
{ "type": "users", "room": "dev" }- Fetch history
{ "type": "history", "room": "dev", "limit": 50 }- Welcome
{ "type": "welcome", "room": "lobby", "user_id": "user-123", "display_name": "Alice", "timestamp": "..." }
"type": "welcome",
- System event
```json
{
"type": "system",
"room": "lobby",
"body": "shayy joined",
"timestamp": "..."
}- Rooms list
{ "type": "rooms", "rooms": ["lobby", "dev"], "timestamp": "..." }- Users list
{
"type": "users",
"room": "lobby",
"users": ["user-123", "user-456"],
"timestamp": "..."
}- History
{
"type": "history",
"room": "lobby",
"history": [{ "user_id": "user-123", "display_name": "Alice", "body": "hi" }],
"timestamp": "..."
}- Error
{ "type": "error", "body": "message too large", "timestamp": "..." }| Env Var | Description | Default |
|---|---|---|
| PORT | HTTP listen port | 8080 |
| CONVERGE_HOST | HTTP listen host | 0.0.0.0 |
| CONVERGE_ADDR | Full listen address (override) | empty |
| CONVERGE_DB_ADAPTER | sqlite or postgres | sqlite |
| CONVERGE_DB_PATH | SQLite file path | converge.db |
| CONVERGE_DB_DSN | Postgres connection string | empty |
| CONVERGE_ALLOWED_ORIGINS | Comma-separated origins or * |
empty (allow all) |
| CONVERGE_MAX_MESSAGE_BYTES | Max websocket frame size | 65536 |
| CONVERGE_MAX_BODY_LENGTH | Max chat message length | 2000 |
| CONVERGE_MAX_ROOM_LENGTH | Max room name length | 64 |
| CONVERGE_MAX_USER_LENGTH | Max user name length | 64 |
| CONVERGE_HISTORY_LIMIT | Max history limit | 200 |
| CONVERGE_SEND_BUFFER | Per-client send buffer size | 16 |
| CONVERGE_SAVE_BUFFER | Persist queue size | 256 |
| CONVERGE_STORE_TIMEOUT | Store operation timeout | 2s |
| CONVERGE_WRITE_WAIT | Websocket write deadline | 10s |
| CONVERGE_PONG_WAIT | Websocket pong wait | 60s |
| CONVERGE_PING_PERIOD | Websocket ping period | 54s |
| CONVERGE_READ_TIMEOUT | HTTP read timeout | 10s |
| CONVERGE_WRITE_TIMEOUT | HTTP write timeout | 10s |
| CONVERGE_IDLE_TIMEOUT | HTTP idle timeout | 60s |
| CONVERGE_JWT_SECRET | HMAC secret for JWT | empty |
| CONVERGE_JWT_ISSUER | JWT issuer | empty |
| CONVERGE_JWT_AUDIENCE | JWT audience | empty |
go test ./...store, err := chat.NewSQLiteStore("converge.db")
if err != nil {
panic(err)
}
defer store.Close()
hub := chat.NewHubWithOptions(store, chat.Options{
JWTSecret: "dev-secret",
})
go hub.Run()
http.HandleFunc("/ws", hub.HandleWS)Postgres adapter:
store, err := chat.NewPostgresStore("postgres://user:pass@localhost:5432/converge?sslmode=disable")
if err != nil {
panic(err)
}
defer store.Close()
hub := chat.NewHubWithOptions(store, chat.Options{
JWTSecret: "dev-secret",
})
go hub.Run()setA := crdt.NewORSet[string]("node-a")
setA.Add("alpha")
setB := crdt.NewORSet[string]("node-b")
setB.Add("beta")
setA.Merge(setB)
reg := crdt.NewLWWRegister("hello", time.Now().UTC(), "node-a")
reg.Set("world", time.Now().UTC(), "node-b")- Read CONTRIBUTING.md for setup and PR guidelines
- See CODE_OF_CONDUCT.md for community standards
MIT. See LICENSE.