chat
A modern IRC-inspired chat server written in Go. Decouples session state from transport connections, enabling mobile-friendly persistent sessions over HTTP.
The HTTP API is the primary interface. It's designed to be simple enough
that writing a terminal IRC-style client against it is straightforward — just
curl and jq get you surprisingly far. The server also ships an embedded
web client as a convenience/reference implementation, but the API comes first.
Motivation
IRC is in decline because session state is tied to the TCP connection. In a mobile-first world, that's a nonstarter. Not everyone wants to run a bouncer or pay for IRCCloud.
This project builds a chat server that:
- Holds session state server-side (message queues, presence, channel membership)
- Exposes a minimal, clean HTTP+JSON API — easy to build clients against
- Supports multiple concurrent connections per user session
- Provides IRC-like semantics: channels, nicks, topics, modes
- Uses structured JSON messages with arbitrary extensibility
Architecture
Transport: HTTP only
All client↔server and server↔server communication uses HTTP/1.1+ with JSON request/response bodies. No WebSockets, no raw TCP, no gRPC — just plain HTTP.
- Client polling: Clients long-poll
GET /api/v1/messages— server holds the connection until messages arrive or timeout. One endpoint for everything. - Client sending:
POST /api/v1/messageswith atofield. That's it. - Server federation: Servers exchange messages via HTTP to enable multi-server networks (like IRC server linking)
The entire read/write loop for a client is two endpoints. Everything else is channel management and history.
Core Concepts
Users
- Identified by a unique user ID (UUID)
- Authenticate via token (issued at registration or login)
- Have a nick (changeable, unique per server at any point in time)
- Maintain a persistent message queue on the server
Sessions
- A session represents an authenticated user's connection context
- Session state is server-held, not connection-bound
- Multiple devices can share a session (messages delivered to all)
- Sessions persist across disconnects — messages queue until retrieved
- Sessions expire after a configurable idle timeout (default 24h)
Channels
- Named with
#prefix (e.g.#general) - Have a topic, mode flags, and member list
- Messages to a channel are queued for all members
- Channel history is stored server-side (configurable depth)
- No eternal logging by default — history rotates
Messages
Every message is a structured JSON object:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"ts": "2026-02-09T20:00:00.000Z",
"from": "nick",
"to": "#channel",
"type": "message",
"body": "Hello, world!",
"meta": {}
}
Fields:
id— Server-assigned UUID, globally uniquets— Server-assigned timestamp (ISO 8601)from— Sender nickto— Destination: channel name (#foo) or nick (for DMs)type— Message type:message,action,notice,join,part,quit,topic,mode,nick,systembody— Message content (UTF-8 text)meta— Arbitrary extensible metadata (JSON object). Can carry:- Cryptographic signatures
- Rich content hints (URLs, embeds)
- Client-specific extensions
- Reactions, edits, threading references
API Endpoints
All endpoints accept and return application/json. Authenticated endpoints
require Authorization: Bearer <token> header.
The API is the primary interface — designed for IRC-style clients. The entire client loop is:
POST /api/v1/register— get a tokenGET /api/v1/state— see who you are and what channels you're inGET /api/v1/messages?after=0— long-poll for all messages (channel, DM, system)POST /api/v1/messages— send to"#channel"or"nick"
That's the core. Everything else (join, part, history, members) is ancillary.
Quick example (curl)
# Register
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/register \
-d '{"nick":"alice"}' | jq -r .token)
# Join a channel
curl -s -X POST http://localhost:8080/api/v1/channels/join \
-H "Authorization: Bearer $TOKEN" \
-d '{"channel":"#general"}'
# Send a message
curl -s -X POST http://localhost:8080/api/v1/messages \
-H "Authorization: Bearer $TOKEN" \
-d '{"to":"#general","content":"hello world"}'
# Poll for messages (long-poll)
curl -s http://localhost:8080/api/v1/messages?after=0 \
-H "Authorization: Bearer $TOKEN"
Registration
POST /api/v1/register — Create account { "nick": "..." } → { id, nick, token }
State
GET /api/v1/state — User state: nick, id, and list of joined channels
Replaces separate /me and /channels endpoints
Messages (unified stream)
GET /api/v1/messages — Single message stream (long-poll supported)
All message types: channel, DM, notices, events
Query params: ?after=<message-id>&timeout=30
POST /api/v1/messages — Send a message
Body: { "to": "#channel" or "nick", "content": "..." }
History
GET /api/v1/history — Fetch history for a target (channel or DM)
Query params: ?target=#channel&before=<id>&limit=50
For DMs: ?target=nick&before=<id>&limit=50
Channels
GET /api/v1/channels/all — List all server channels
POST /api/v1/channels/join — Join a channel { "channel": "#name" }
DELETE /api/v1/channels/{name} — Part (leave) a channel
GET /api/v1/channels/{name}/members — Channel member list
Server Info
GET /api/v1/server — Server info (name, MOTD)
GET /.well-known/healthcheck.json — Health check
Federation (Server-to-Server)
Servers can link to form a network, similar to IRC server linking:
POST /api/v1/federation/link — Establish server link (mutual auth via shared key)
POST /api/v1/federation/relay — Relay messages between linked servers
GET /api/v1/federation/status — Link status
Federation uses the same HTTP+JSON transport. Messages are relayed between servers so users on different servers can share channels.
Channel Modes
Inspired by IRC but simplified:
| Mode | Meaning |
|---|---|
+i |
Invite-only |
+m |
Moderated (only voiced users can send) |
+s |
Secret (hidden from channel list) |
+t |
Topic locked (only ops can change) |
+n |
No external messages |
User channel modes: +o (operator), +v (voice)
Configuration
Via environment variables (Viper), following gohttpserver conventions:
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Listen port |
DBURL |
"" |
SQLite/Postgres connection string |
DEBUG |
false |
Debug mode |
MAX_HISTORY |
10000 |
Max messages per channel history |
SESSION_TIMEOUT |
86400 |
Session idle timeout (seconds) |
MAX_MESSAGE_SIZE |
4096 |
Max message body size (bytes) |
MOTD |
"" |
Message of the day |
SERVER_NAME |
hostname | Server display name |
FEDERATION_KEY |
"" |
Shared key for server linking |
Storage
SQLite by default (single-file, zero-config), with Postgres support for larger deployments. Tables:
users— accounts and auth tokenschannels— channel metadata and modeschannel_members— membership and user modesmessages— message history (rotated perMAX_HISTORY)message_queue— per-user pending delivery queueserver_links— federation peer configuration
Project Structure
Following gohttpserver CONVENTIONS.md:
chat/
├── cmd/
│ └── chat/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── database/
│ │ └── database.go
│ ├── globals/
│ │ └── globals.go
│ ├── handlers/
│ │ ├── handlers.go
│ │ ├── auth.go
│ │ ├── channels.go
│ │ ├── federation.go
│ │ ├── healthcheck.go
│ │ ├── messages.go
│ │ └── users.go
│ ├── healthcheck/
│ │ └── healthcheck.go
│ ├── logger/
│ │ └── logger.go
│ ├── middleware/
│ │ └── middleware.go
│ ├── models/
│ │ ├── channel.go
│ │ ├── message.go
│ │ └── user.go
│ ├── queue/
│ │ └── queue.go
│ └── server/
│ ├── server.go
│ ├── http.go
│ └── routes.go
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
├── CONVENTIONS.md → (copy from gohttpserver)
└── README.md
Required Libraries
Per gohttpserver conventions:
| Purpose | Library |
|---|---|
| DI | go.uber.org/fx |
| Router | github.com/go-chi/chi |
| Logging | log/slog (stdlib) |
| Config | github.com/spf13/viper |
| Env | github.com/joho/godotenv/autoload |
| CORS | github.com/go-chi/cors |
| Metrics | github.com/prometheus/client_golang |
| DB | modernc.org/sqlite + database/sql |
Web Client
The server embeds a single-page web client (Preact) served at /. This is a
convenience/reference implementation — not the primary interface. It
demonstrates the API and provides a quick way to test the server in a browser.
The primary intended clients are IRC-style terminal applications and bots talking directly to the HTTP API.
Design Principles
- API-first — the HTTP API is the product. Clients are thin. If you can't build a working IRC-style TUI client in an afternoon, the API is too complex.
- HTTP is the only transport — no WebSockets, no raw TCP, no protocol negotiation. HTTP is universal, proxy-friendly, and works everywhere.
- Server holds state — clients are stateless. Reconnect, switch devices, lose connectivity — your messages are waiting.
- Structured messages — JSON with extensible metadata. Enables signatures, rich content, client extensions without protocol changes.
- Simple deployment — single binary, SQLite default, zero mandatory external dependencies.
- No eternal logs — history rotates. Chat should be ephemeral by default.
- Federation optional — single server works standalone. Linking is opt-in.
Status
Implementation in progress. Core API is functional with SQLite storage and embedded web client.
License
MIT