- go.mod with git.eeqj.de/sneak/chat module - internal packages: globals, logger, config, db, healthcheck, middleware, handlers, server - SQLite database with embedded migration system (schema_migrations tracking) - Migration 001: schema_migrations table - Migration 002: channels table - Config with chat-specific vars (MAX_HISTORY, SESSION_TIMEOUT, MAX_MESSAGE_SIZE, MOTD, SERVER_NAME, FEDERATION_KEY) - Healthcheck endpoint at /.well-known/healthcheck.json - Makefile, .gitignore - cmd/chatd/main.go entry point
chat
A modern IRC-inspired chat server written in Go. Decouples session state from transport connections, enabling mobile-friendly persistent sessions over HTTP.
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)
- Delivers messages over HTTP (JSON-RPC style)
- 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 poll for new messages via
GETwith long-polling support (server holds the connection open until messages arrive or timeout) - Client sending: Clients send messages/commands via
POST - Server federation: Servers exchange messages via HTTP to enable multi-server networks (like IRC server linking)
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.
Authentication
POST /api/v1/register — Create account (nick, password) → token
POST /api/v1/login — Authenticate → token
POST /api/v1/logout — Invalidate token
Session & Messages
GET /api/v1/messages — Retrieve queued messages (long-poll supported)
Query params: ?after=<message-id>&timeout=30
POST /api/v1/messages — Send a message or command
GET /api/v1/history — Retrieve channel/DM history
Query params: ?target=#channel&before=<id>&limit=50
Channels
GET /api/v1/channels — List joined channels
POST /api/v1/channels/join — Join a channel
POST /api/v1/channels/part — Leave a channel
GET /api/v1/channels/{name} — Channel info (topic, members, modes)
POST /api/v1/channels/{name}/topic — Set channel topic
Users
GET /api/v1/users/me — Current user info
POST /api/v1/users/nick — Change nick
GET /api/v1/users/{nick} — User info (online status, idle time)
Server Info
GET /api/v1/server — Server info (name, version, MOTD, user count)
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 |
Design Principles
- 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
Design phase. This README is the spec. Implementation has not started.
License
MIT