Odel
freeq

freeq

@freeq-irc83RustMITUpdated 3 days ago

Read and verify freeq conversations: channels, history, search, signatures, pins.

Server endpointStreamable HTTPNo authProbed

This is the third-party server itself — Odel doesn't run it. Hitting this URL directly talks straight to the upstream server with no auth or proxying. Connect through Odel to front it with managed auth.

freeq logo

freeq

IRC server and client with AT Protocol (Bluesky) identity authentication, end-to-end encrypted channels, iroh QUIC transport, peer-to-peer DMs, and federated server-to-server clustering.

Users authenticate with their Bluesky identity via a custom SASL mechanism (ATPROTO-CHALLENGE). Standard IRC clients connect as guests. Authenticated users get their DID bound to their connection — visible via WHOIS, enforced for nick ownership, and usable for DID-based bans, invites, and persistent ops.

Try it now: irc.freeq.at

For agents

freeq is meant to be used by software as well as people, so every surface an agent looks for exists and cross-links the others:

  • llms.txt — the documentation index, linking raw markdown (llms-full.txt for one fetch).
  • OpenAPI 3.1 — the full HTTP contract (spec/openapi.yaml; a drift test fails the build if the router and the spec disagree).
  • MCPfreeq-mcp/ (@freeq/mcp) gives any MCP client tools to read, search, verify and take part in conversations. Not on npm yet: build it (cd freeq-mcp && npm install && npm run build) and point your client at node <repo>/freeq-mcp/dist/index.js.
  • Skillsskills/: freeq (talking to other people's agents), freeq-api (REST), freeq-bots (building one).
  • Agent Assistance Interface — ask the server why a join failed or a message went missing; it answers with a conclusion plus evidence, not raw state.

Attribution is the point: every message carries a ULID msgid and a signature, and GET /api/v1/verify/{msgid} distinguishes a message signed by its author's key from one merely relayed by the server.

Web Client

The web client at irc.freeq.at provides:

  • AT Protocol OAuth login — sign in with your Bluesky identity
  • Channel policy gates — channels can require credential verification to join
  • GitHub verification — prove repo collaborator or org membership status
  • Bluesky social graph gates — prove you follow someone (no OAuth needed)
  • Moderator appointments — ops issue signed credentials for halfop (+h)
  • Automatic role escalation — credentials auto-grant IRC modes (op, halfop, voice)
  • Shareable invite linkshttps://irc.freeq.at/join/#channel
  • Message editing, deletion, reactions, threads — changing a message requires the sender's signature; current clients sign automatically
  • End-to-end encrypted channels

Demo Channels

ChannelPolicyWhat it demonstrates
#demo-followMust follow @chadfowler.com on BlueskySocial graph verification (zero OAuth)
#demo-githubOpen join, freeq-irc/freeq collaborators get auto-opLayered credentials + role escalation
#demo-moderationOpen join, moderators appointed via credentialsCredential-based moderation pipeline

Architecture

freeq-server/       IRC server with SASL, WebSocket, iroh, S2S federation
freeq-app/          React web client (Vite + Tailwind)
freeq-auth-broker/  AT Protocol OAuth broker (persistent sessions)
freeq-sdk/          Reusable client SDK (connect, auth, events, E2EE, P2P)
freeq-tui/          Terminal UI client built on the SDK
freeq-site/         Marketing site (freeq.at)

The SDK exposes a (ClientHandle, Receiver<Event>) pattern — any UI or bot can consume events and send commands.

Transport Stack

┌──────────────────────────────────────────┐
│            IRC Wire Protocol             │
├──────────┬──────────┬──────────┬─────────┤
│   TCP    │   TLS    │WebSocket │  iroh   │
│  :6667   │  :6697   │  :8080   │  QUIC   │
└──────────┴──────────┴──────────┴─────────┘

All transports feed into the same handle_generic() handler — the IRC protocol is transport-agnostic. Each transport is zero-cost when not enabled.

Quick Start

Build

cargo build --release

Run the Server

# Minimal: plain TCP only, in-memory
cargo run --release --bin freeq-server

# With persistence
cargo run --release --bin freeq-server -- --db-path data/irc.db

# With TLS
cargo run --release --bin freeq-server -- \
  --tls-cert certs/cert.pem --tls-key certs/key.pem

# With WebSocket + REST API
cargo run --release --bin freeq-server -- --web-addr 0.0.0.0:8080

# With iroh transport (QUIC, NAT-traversing)
cargo run --release --bin freeq-server -- --iroh

# Full production setup
cargo run --release --bin freeq-server -- \
  --listen-addr 0.0.0.0:6667 \
  --tls-listen-addr 0.0.0.0:6697 \
  --tls-cert /etc/letsencrypt/live/example.com/fullchain.pem \
  --tls-key /etc/letsencrypt/live/example.com/privkey.pem \
  --db-path ./irc.db \
  --web-addr 0.0.0.0:8080 \
  --iroh

Generate a self-signed cert for local development:

mkdir -p certs
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -keyout certs/key.pem -out certs/cert.pem -days 365 -nodes \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

Connect with the TUI Client

# Guest (no auth)
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick

# Bluesky OAuth (opens browser)
cargo run --release --bin freeq-tui -- 127.0.0.1:6697 mynick \
  --handle alice.bsky.social

# App password fallback
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \
  --handle alice.bsky.social --app-password xxxx-xxxx-xxxx-xxxx

# Auto-join channels
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \
  -c '#general,#random'

# Explicit iroh transport
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \
  --iroh-addr <endpoint-id>

# Vi keybindings
cargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick --vi

Iroh auto-discovery: When connecting to a server that has --iroh enabled, the TUI probes CAP LS for the iroh=<endpoint-id> capability and auto-upgrades to iroh QUIC transport. No manual endpoint ID needed.

OAuth sessions are cached to ~/.config/freeq-tui/<handle>.session.json so you don't need to re-authenticate on every launch.

Connect with a Standard IRC Client

Any IRC client works as a guest — irssi, WeeChat, HexChat, LimeChat, etc. Connect to 127.0.0.1:6667 (plain) or 127.0.0.1:6697 (TLS). No special configuration needed.

Connect via WebSocket

When --web-addr is set, the server accepts WebSocket connections at ws://<addr>/irc. A test HTML client is included at freeq-server/test-client.html.

Authentication

SASL ATPROTO-CHALLENGE

The server implements a custom SASL mechanism for AT Protocol identity:

  1. Client requests CAP sasl, then AUTHENTICATE ATPROTO-CHALLENGE
  2. Server sends a challenge: base64url(json { session_id, nonce, timestamp })
  3. Client responds with one of:
    • Crypto signature (method: "crypto"): Signs challenge bytes with a private key listed in the DID document
    • PDS session (method: "pds-session"): Sends an app-password JWT; server verifies against the PDS
    • PDS OAuth (method: "pds-oauth"): Sends a DPoP-bound access token with proof; server verifies against the PDS
  4. Server verifies, emits 903 (success) or 904 (failure)
  5. Client sends CAP END, registration completes

Security Properties

  • Each challenge contains a cryptographically random nonce
  • Challenges are invalidated after use (no replay)
  • Challenge validity window: configurable, default 60 seconds
  • Private keys never leave the client
  • PDS URL is verified against the DID document before accepting session tokens
  • Supported key types: secp256k1 (MUST), ed25519 (SHOULD)

What Authentication Gets You

  • Nick is bound to your DID — no one else can use it
  • WHOIS shows your DID and Bluesky handle
  • You can be banned or invited by DID (survives reconnect/nick changes)
  • Persistent channel ops tied to your DID (survive reconnects and work across federated servers)
  • Your identity is cryptographically verifiable

Transports

TCP / TLS (Standard)

Standard IRC on port 6667 (plain) and 6697 (TLS). TLS auto-detected by port in the client. Always available.

WebSocket

Enabled with --web-addr. Accepts WebSocket IRC at /irc. Uses the same IRC wire protocol — WebSocket is a transport, not a new protocol. Includes a read-only REST API at /api/v1/ (channels, members, topics, messages).

iroh (QUIC)

Enabled with --iroh. Provides NAT-traversing encrypted QUIC connections via iroh. The server generates a persistent secret key (iroh-key.secret) on first run — endpoint ID is stable across restarts.

The server advertises its iroh endpoint ID in CAP LS:

CAP * LS :sasl message-tags iroh=44f1415c9db30989...

Clients auto-discover and upgrade to iroh when available.

End-to-End Encryption (E2EE)

Client-side channel encryption using AES-256-GCM with HKDF-SHA256 key derivation from a shared passphrase. The server relays ciphertext unchanged.

/encrypt <passphrase>    Enable E2EE for current channel
/decrypt                 Disable E2EE for current channel

Wire format: ENC1:<nonce-b64>:<ciphertext-b64> — version-tagged, uses the message body for robustness. All channel members must use the same passphrase.

Peer-to-Peer Encrypted DMs

Direct encrypted messaging between clients via iroh QUIC, bypassing the server entirely.

/p2p start               Start your P2P endpoint
/p2p id                  Show your P2P endpoint ID
/p2p connect <id>        Connect to a peer
/p2p msg <id> <message>  Send a direct message

P2P conversations appear in dedicated p2p:<short-id> buffers. Wire format is newline-delimited JSON (not IRC protocol). ALPN: freeq/p2p-dm/1.

P2P endpoint IDs are visible in WHOIS (numeric 672).

Server-to-Server Federation (S2S)

Servers cluster over iroh QUIC connections. Each server maintains its own local state and syncs channel membership, messages, topics, and DID-based ops across the federation.

Setup

# Server A: just enable iroh (accepts incoming S2S connections)
cargo run --release --bin freeq-server -- --iroh

# Server B: enable iroh + connect to Server A
cargo run --release --bin freeq-server -- --iroh \
  --s2s-peers <server-a-endpoint-id>

Server A doesn't need --s2s-peers — it accepts incoming S2S connections automatically when --iroh is enabled.

What Syncs

FeatureSync behavior
JOIN/PART/QUITMembership tracked per origin server
PRIVMSGChannel messages relayed to all peers
TOPICTopic changes propagate
DID-based opsPersistent ops sync via CRDT
FounderFirst-write-wins CRDT resolution
NAMESIncludes both local and remote members
WHOISShows DID, handle, and origin for remote users

CRDT-Based State Convergence

Channel authority (founder, DID-based ops) uses Automerge CRDTs for conflict-free convergence. Presence is NOT in the CRDT — it's S2S event-driven to avoid ghost users when servers crash.

  • Founder resolution: Deterministic min-actor-wins — concurrent claims converge deterministically, late entrants cannot overwrite after sync
  • DID ops: Union merge — grants propagate, revocations propagate
  • Provenance tracking: All CRDT writes carry origin peer + authorizing DID
  • Authority boundaries: Soft enforcement validates who can write each key-space
  • Event dedup: S2S events carry unique IDs; bounded LRU prevents replay
  • Peer identity: CRDT sync keyed by iroh endpoint ID (cryptographic), not server name (untrusted). Hello handshake binds transport to logical identity.
  • Compaction: Periodic snapshot + reload bounds doc growth in long-lived deployments
  • Async-safe: CRDT uses tokio::sync::Mutex — no runtime thread blocking
  • No timestamps in authority decisions (spoofable by rogue servers)

S2S Acceptance Tests

# Run against two live servers
LOCAL_SERVER=localhost:6667 REMOTE_SERVER=irc.freeq.at:6667 \
  cargo test -p freeq-server --test s2s_acceptance -- --nocapture --test-threads=1

9 tests verify: connectivity, bidirectional message relay, NAMES sync, topic sync, PART/QUIT cleanup, and late-joiner state.

IRC Features

Standard IRC

Full compatibility with RFC 1459/2812 basics:

  • NICK, USER, JOIN, PART, PRIVMSG, NOTICE, QUIT
  • NAMES (query channel membership on demand)
  • PING/PONG (client and server keepalive)
  • WHOIS (shows DID, handle, iroh ID for authenticated users)
  • CTCP ACTION (/me)
  • Multiple channels, private messages

Channel Modes

ModeDescription
+o nickChannel operator
+v nickVoice
+b maskBan (hostmask *!*@host or DID did:plc:xyz)
+iInvite-only
+tTopic lock (ops only)
+k keyChannel key (password)

DID-Aware Features

  • DID bans (MODE #chan +b did:plc:xyz): Bans by identity, not just hostmask. Survives nick changes and reconnects.
  • DID invites (INVITE nick #chan): If the user is authenticated, the invite is stored by DID and survives reconnect.
  • Nick ownership: Once an authenticated user claims a nick, guests and other DIDs cannot use it. If an unauthenticated user tries to take a registered nick during SASL negotiation, they're renamed to GuestXXXX at registration time.
  • Persistent DID-based ops: When an authenticated user is opped, their DID is recorded. They're auto-opped on rejoin — even on a different server in the federation. Channel founders (first authenticated user to create a channel) can never be de-opped.

Message History

The server stores the last 100 messages per channel. When you join, recent history is replayed as standard PRIVMSG — works with any IRC client, no special protocol extension needed.

Rich Media (IRCv3 Message Tags)

Rich media is supported through IRCv3 message tags, giving multipart/alternative semantics — the same content in two representations:

  • Tags: Structured metadata (content-type, URL, dimensions, alt text)
  • Body: Plain text fallback (description + URL)
@content-type=image/jpeg;media-url=https://cdn.bsky.app/img/...;media-alt=Sunset;media-w=1200;media-h=800 :alice!a@host PRIVMSG #photos :Sunset https://cdn.bsky.app/img/...
ClientWhat they see
irssi, WeeChatSunset https://cdn.bsky.app/img/... (clickable link)
freeq-tui🖼 [image/jpeg] Sunset 1200×800 https://cdn.bsky.app/img/...

Media is hosted externally (AT Protocol PDS blob storage). The IRC server never handles media bytes — it just relays tagged messages.

Supported tag keys:

TagDescription
content-typeMIME type (e.g. image/jpeg, video/mp4)
media-urlURL where the media can be fetched
media-altAlt text / description
media-wWidth in pixels
media-hHeight in pixels
media-blurhashBlurhash placeholder
media-sizeFile size in bytes
media-filenameOriginal filename

Rate Limiting

Token bucket rate limiter (10 commands/second) kicks in after registration. The initial connection burst is not rate-limited, so clients that send many commands on connect (like LimeChat) work correctly.

TUI Client

Status Bar

The status bar shows:

  • Transport badge: Colored indicator (red=TCP, green=TLS, cyan=WS, magenta=Iroh)
  • Nick: Your current nick
  • Auth: Authenticated DID or "guest"
  • Uptime: Connection duration

Keybindings

Emacs mode (default):

KeyAction
Ctrl-A / HomeBeginning of line
Ctrl-E / EndEnd of line
Ctrl-F / RightForward char
Ctrl-B / LeftBack char
Alt-FForward word
Alt-BBack word
Ctrl-DDelete char
Ctrl-H / BackspaceDelete back
Ctrl-KKill to end of line
Ctrl-UKill to beginning
Ctrl-WKill word back
Alt-DKill word forward
Ctrl-YYank (paste kill ring)
Ctrl-TTranspose chars
Alt-UUppercase word
Alt-LLowercase word
Alt-CCapitalize word
TabNick completion
Up / DownInput history
Ctrl-N / Alt-NNext buffer
Ctrl-P / Alt-PPrevious buffer
BackTab (Shift-Tab)Previous buffer
PageUp / PageDownScroll messages
Ctrl-C / Ctrl-QQuit

Vi mode (--vi):

Normal mode: h/l move, w/b/e word motion, 0/$ line edges, i/a/I/A enter insert, x/X/D/C/S/s delete/change, p/P paste, k/j history, dd clear line. Insert mode: standard typing, Esc to exit to normal mode.

Commands

/join #channel          Join a channel
/part [#channel]        Leave current or named channel
/msg nick message       Private message
/me action              CTCP ACTION
/topic [text]           View or set channel topic
/mode +o/-o nick        Op/deop
/mode +v/-v nick        Voice/devoice
/mode +b [mask]         Ban (or list bans)
/mode +i/-i             Invite-only
/mode +t/-t             Topic lock
/mode +k/-k [key]       Channel key
/op nick                Shortcut for /mode +o
/deop nick              Shortcut for /mode -o
/voice nick             Shortcut for /mode +v
/kick nick [reason]     Kick from channel
/ban mask               Ban user
/unban mask             Remove ban
/invite nick            Invite to current channel
/whois nick             Query user info
/names [#channel]       List channel members
/raw <line>             Send raw IRC line
/encrypt <passphrase>   Enable E2EE for current channel
/decrypt                Disable E2EE for current channel
/p2p start              Start P2P endpoint
/p2p id                 Show your P2P endpoint ID
/p2p connect <id>       Connect to a peer
/p2p msg <id> <text>    Send P2P direct message
/net                    Show/hide network info popup
/debug                  Toggle raw IRC line display
/quit [message]         Disconnect
/help                   Show commands

Network Info Popup (/net)

Shows: transport type, server address, connection state, uptime, nick, authenticated DID, iroh endpoint ID, E2EE channels, P2P DM status. Close with Esc or q.

Debug Mode (/debug)

Toggles raw IRC line display in the status buffer (prefixed with ). Useful for diagnosing protocol issues.

REST API

When --web-addr is set, a read-only REST API is available:

EndpointDescription
GET /api/v1/channelsList all channels
GET /api/v1/channels/{name}Channel info (topic, modes, member count)
GET /api/v1/channels/{name}/membersChannel member list
GET /api/v1/channels/{name}/topicChannel topic
GET /api/v1/channels/{name}/messagesRecent messages (with pagination)
GET /api/v1/statsServer stats

All writes go through IRC — the REST API is strictly read-only.

Server Configuration

freeq-server [OPTIONS]

Options:
  --listen-addr <ADDR>            Plain TCP address [default: 127.0.0.1:6667]
  --tls-listen-addr <ADDR>        TLS address [default: 127.0.0.1:6697]
  --tls-cert <PATH>               TLS certificate PEM file
  --tls-key <PATH>                TLS private key PEM file
  --server-name <NAME>            Server name [default: freeq]
  --challenge-timeout-secs <N>    SASL challenge validity [default: 60]
  --config <PATH>                 Read options from a TOML file (flags override)
  --check-config                  Validate configuration and exit
  --db-path <PATH>                SQLite database path (omit for in-memory)
  --migrate-to <VERSION>          Run the schema ladder to VERSION and exit
  --web-addr <ADDR>               HTTP/WebSocket listener address
  --iroh                          Enable iroh QUIC transport
  --iroh-port <PORT>              UDP port for iroh (default: random)
  --s2s-peers <ID,ID,...>         S2S peer iroh endpoint IDs
  --s2s-peer-api <ID=URL,...>     Where each S2S peer serves its users' signing keys

Persistence

When --db-path is set, the server persists:

  • Message history — all channel messages, queryable with pagination
  • Channel state — topics, modes (+t, +i, +k), channel keys
  • Bans — hostmask and DID bans survive restarts
  • DID-nick bindings — nick ownership persists across server restarts

Without --db-path, the server runs entirely in-memory. The database uses SQLite with WAL mode for good concurrent read performance. Persistence failures are logged but do not crash the server.

Every flag can also live in a TOML file passed via --config server.toml (or FREEQ_CONFIG): keys are the flag names with underscores (listen_addr = "0.0.0.0:6667"), lists are arrays, and precedence is CLI flag > environment variable > file > default, so existing flag-based deployments work unchanged. A typo'd key is a startup error that names the key. --migrate-to is deliberately CLI-only. A full worked example ships as server.toml.example, and a test keeps it in sync with the real schema.

Schema migrations run automatically (and upward only) at startup. A server binary refuses to open a database stamped with a newer schema than it knows, so before rolling back to an older binary, downgrade the schema first: freeq-server --db-path <PATH> --migrate-to <VERSION> runs the ladder to the named version and exits. Downgrades stop with an error at any migration that is irreversible by design.

Deployment

See deploy/README.md for example VPS setup and deployment instructions.

Tests

# Unit + integration tests
cargo test

# S2S federation acceptance tests (9 tests, requires two live servers)
LOCAL_SERVER=localhost:6667 REMOTE_SERVER=irc.freeq.at:6667 \
  cargo test -p freeq-server --test s2s_acceptance -- --nocapture --test-threads=1

153 tests covering:

  • SDK (44): IRC parsing (with tag support), tag escaping roundtrip, DID document parsing, key generation/signing/verification, multibase/multicodec, challenge response encoding, SASL signer variants, media attachment roundtrip, link preview roundtrip, media type detection
  • Server unit (33 + 12 CRDT): Message parsing (with tags), tag escaping, SASL challenge store (create, take, replay, expiry, forged nonce), channel state, database roundtrips (channels, bans, messages, identities), CRDT tests (founder deterministic min-actor, founder not overwritten after sync, DID ops sync, topic provenance, authority validation, compaction, metrics, ban provenance)
  • Integration (27): Guest connection, secp256k1 auth, ed25519 auth, wrong key rejection, unknown DID rejection, expired challenge rejection, replayed nonce rejection, channel messaging, mixed auth/guest, nick collision, channel topic, topic lock, channel ops/kick, hostmask bans, DID bans, invite-only, message history replay, nick ownership, quit broadcast, channel key (+k), TLS connection, rich media tag passthrough, persistence (messages, topics, bans, nick ownership survive restart)
  • S2S acceptance (9): Connectivity, bidirectional message relay, NAMES sync, topic sync, PART/QUIT cleanup, late-joiner state

Protocol Notes

Deviations from the Spec

  • Challenge uses JSON encoding (not a binary format) for debuggability
  • PDS session verification is an additional auth method beyond the spec's crypto-only approach — it enables OAuth login without requiring users to manage raw signing keys
  • History replay uses standard PRIVMSG (no custom extension or batch)

IRCv3 Compatibility

  • CAP negotiation follows IRCv3 CAP LS 302 / CAP REQ / CAP END
  • SASL flow follows IRCv3 SASL specification with a custom mechanism name
  • message-tags capability follows the IRCv3 message tags specification
  • Media tags use vendor-prefixed names (content-type, media-url, etc.)
  • Server advertises iroh=<endpoint-id> in CAP LS for transport discovery
  • ATPROTO-CHALLENGE could be proposed as an IRCv3 WG mechanism

Plugins

Freeq supports a plugin system for custom server behavior. Plugins hook into events like authentication, message delivery, and channel joins.

# Load a plugin via CLI
freeq-server --plugin "identity-override:handle=timesync.bsky.social,display_id=3|337"

# Load plugins from a directory of TOML configs
freeq-server --plugin-dir ./examples/plugins/

See examples/plugins/ for example configurations and docs/PROTOCOL.md for the full plugin hook reference.

Documentation

License

MIT