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/messages with a to field. 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 unique
  • ts — Server-assigned timestamp (ISO 8601)
  • from — Sender nick
  • to — Destination: channel name (#foo) or nick (for DMs)
  • type — Message type: message, action, notice, join, part, quit, topic, mode, nick, system
  • body — 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:

  1. POST /api/v1/register — get a token
  2. GET /api/v1/state — see who you are and what channels you're in
  3. GET /api/v1/messages?after=0 — long-poll for all messages (channel, DM, system)
  4. 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 tokens
  • channels — channel metadata and modes
  • channel_members — membership and user modes
  • messages — message history (rotated per MAX_HISTORY)
  • message_queue — per-user pending delivery queue
  • server_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

  1. 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.
  2. HTTP is the only transport — no WebSockets, no raw TCP, no protocol negotiation. HTTP is universal, proxy-friendly, and works everywhere.
  3. Server holds state — clients are stateless. Reconnect, switch devices, lose connectivity — your messages are waiting.
  4. Structured messages — JSON with extensible metadata. Enables signatures, rich content, client extensions without protocol changes.
  5. Simple deployment — single binary, SQLite default, zero mandatory external dependencies.
  6. No eternal logs — history rotates. Chat should be ephemeral by default.
  7. 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

Description
No description provided
Readme 1.2 MiB
Languages
Go 86.1%
JavaScript 9.1%
CSS 2.8%
Dockerfile 0.7%
Makefile 0.7%
Other 0.6%