IRC Client Foundations Supplement
Version: 1.0 Status: Ready for implementation Companion to: docs/pdf.md (feature PRD) Purpose: Build-from-scratch decisions for the embeddable rabbot-irc gem
How to use this document
The main PRD (pdf.md) covers mIRC/Ibis feature parity, phased deliverables, services caveats, and the Fandom companion bot. This supplement covers everything needed before rails new or gem scaffolding: packaging, auth, schema, workers, embed API, formatter spec, dialog triage, deployment, and tests.
Decisions locked in for v1:
-
Gem, not monorepo app β embeddable IRC chat usable in any Rails app
-
Optional Rails accounts β anonymous connect works; accounts unlock persistence + companion bot
-
Embeddable UI β mount into any DOM element, not only full-screen
14. Gem packaging and embed API
14.1 Gem name and repos
| Package | Role |
|---|---|
rabbot-core (extract from rabbot) |
IrcFormat, IrcReply, Normalize, CharacterProfile, CharacterProfileGenerator, CharacterTranscripts |
rabbot-irc (new repo) |
Rails engine, workers, event store, Hotwire UI |
rabbot (existing) |
Cinch bots, plugins, companion bot provisioning |
rabbot-irc depends on rabbot-core and ircinch. Host apps add gem "rabbot-irc" and mount or embed.
14.2 Engine structure
rabbot-irc/
lib/rabbot/irc/
engine.rb # Rails::Engine, isolated namespace Rabbot::Irc
version.rb
app/channels/irc_channel.rb
app/controllers/rabbot/irc/
app/javascript/controllers/irc_chat_controller.js
app/views/rabbot/irc/chat/ # partials: log, nicklist, input, status
app/services/rabbot/irc/
config/routes.rb
lib/generators/rabbot/irc/install/
14.3 Host app integration
Full-page mount (optional):
# config/routes.rb mount Rabbot::Irc::Engine => "/irc"
Embed in any view:
<div id="sidebar"
data-controller="irc-chat"
data-irc-chat-layout-value="compact"
data-irc-chat-session-url-value="<%= rabbot_irc_session_path %>">
</div>
Generator:
rails generate rabbot:irc:install rails db:migrate
Copies migrations for irc_* tables; adds importmap/Stimulus pin for irc_chat_controller.
14.4 Embed contract
| Concern | Rule |
|---|---|
| Mount target | Stimulus irc-chat scopes all UI under the host element |
| Layout modes | full, compact, log-only β CSS + partial selection via data-irc-chat-layout-value |
| ActionCable | Subscribe with signed_session_id from host session |
| Body ownership | Engine never assumes full-page layout; works in sidebar, modal, or iframe-like div |
| Theming | Host app CSS variables (--irc-bg, --irc-nick-op, etc.) optional override |
14.5 Architecture diagram
HostApp (any Rails app)
βββ mount Rabbot::Irc::Engine => "/irc" [optional full page]
βββ embed <div data-controller="irc-chat"> [any DOM element]
β
βΌ
irc_chat_controller (Stimulus)
β
βΌ
ActionCable (IrcChannel)
β
βΌ
IrcWorker process (bin/irc_worker)
β
βΌ
Cinch (ircinch) β IRC server
15. Auth and sessions
15.1 Two tiers
| Tier | Connect | Persistence | Companion bot | Saved networks |
|---|---|---|---|---|
| Anonymous | IRC connect form | Session cookie only; lost on expiry | No | No |
| Account | Optional login first | DB profiles, event cursors, replay | Yes (main PRD Β§13) | Yes |
Anonymous users get live chat while the tab is open. Creating an account (or signing in) unlocks saved network profiles, IrcEventStore replay, and companion bot settings.
15.2 Session model
-
IrcSessionβ ephemeral record tying browser tab(s) to one IRC worker connection or catch-up cursor -
Userβ host-app model (Devise, etc.); gem providesRabbot::Irc::Authenticatableconcern -
ActionCable auth β
IrcChannelrejects subscribe without validsigned_session_id(Rails.application.message_verifier(:irc_session)) -
Multi-tab β same
IrcSessionid β shared stream;last_event_idin server session +localStoragebackup per channel
15.3 Anonymous limits
-
No
IrcEventStorereplay beyond the current live connection -
No companion bot
-
No saved
IrcNetworkProfile(form fields may usesessiononly) -
Disconnect when last tab unsubscribes and session expires
15.4 Account unlocks
-
Persisted network profiles and encrypted credentials
-
Companion bot (Β§13 in main PRD)
-
Cross-device cursor sync via
user_id -
Channel/query log download (Phase 2)
16. Data model
Host app runs engine migrations via rails rabbot:irc:install:migrations.
16.1 Tables
| Table | Purpose |
|---|---|
users |
Host app (optional until account features) |
irc_sessions |
Browser session β worker; signed_token, user_id (nullable), state |
irc_network_profiles |
Per-user saved server, port, nick, SSL, profile name |
irc_credentials |
Encrypted password / SASL secret (has_encrypted per profile) |
irc_connections |
Worker-side Cinch connection state, session_id, last_ping_at |
irc_events |
Append-only log for replay (account + companion) |
irc_companion_profiles |
Fandom URL, visibility, enabled flag (Β§13) |
irc_channel_states |
Latest topic, modes, nick list snapshot per channel |
16.2 irc_events schema (key fields)
Column |
Type | Notes |
|---|---|---|
user_id |
bigint | Owner (required for replay) |
network |
string | Normalized network key |
channel |
string | chan or query nick |
event_type |
string | PRIVMSG, JOIN, PART, etc. |
payload |
jsonb | Raw parsed fields |
formatted_text |
text | Output of event_formatter |
monotonic_id |
bigint | Per-user sequence for cursor |
recorded_at |
datetime | IRC event time |
Index: (user_id, monotonic_id), (user_id, network, channel, monotonic_id).
16.3 Retention
Defaults (configurable via Rabbot::Irc.configure):
-
7 days or 50,000 events per user (whichever limit hit first)
-
Compaction job (
IrcEventCompactionJob) runs nightly; see Β§21
16.4 Database
Web platform (rabbot-irc):
-
Production: PostgreSQL required (concurrent workers, jsonb, partitioning, read replicas)
-
Development: SQLite acceptable for a single local worker
Legacy Rabbot personas:
-
Keep SQLite per bot (
data/rabbot.db,data/bots/*.db) for plugin state (games, AI, autoop, etc.) -
Do not migrate bot SQLite to Postgres unless cross-bot queries are needed
Shared boundary (Postgres only):
-
users,irc_events,irc_sessions,irc_companion_profiles, and companion sync data -
Companion bots write replay events to the web Postgres store, not their local SQLite files
17. IRC worker architecture
17.1 Library
Use Cinch (ircinch) β same as existing Rabbot. Do not use Net::IRC or EventMachine for the web client worker.
17.2 Process model
| Process | Command | Role |
|---|---|---|
Web |
bin/rails server |
HTTP, Turbo, ActionCable |
| IRC worker | bin/irc_worker |
Long-lived; holds Cinch sockets |
| Jobs | bin/jobs (Solid Queue) |
Companion lifecycle, compaction, profile generation |
IrcReadJob is for short tasks only (parse MOTD, probe capabilities) β not holding IRC sockets inside Solid Queue workers.
17.3 Worker loop
-
IrcWorkerpollsirc_connectionsforpending/stopcommands (DB row or Redis list) -
Spawns Cinch bot per active
irc_connection -
On IRC event β format β broadcast ActionCable + optionally append
irc_events -
On disconnect β update state; apply reconnect policy from
IrcReconnectGuardpatterns (lib/irc_reconnect_guard.rb): throttle floor, drone floor, connect stagger
17.4 Connection lifecycle by phase
| Phase | Default behaviour |
|---|---|
| Phase 1 | One Cinch connection per IrcSession while tab subscribed; disconnect when last tab leaves |
| Phase 2 + account | Saved profiles; optional stay-connected via companion (invisible mode) |
| Phase 3 | Visible companion = second Cinch connection mirroring channel set |
17.5 Duplicate event rule
When the userβs web session is online (active Cinch connection), log events from that connection only. Companion bot appends to irc_events only when the user connection is absent (tab closed, invisible bouncer mode). Prevents duplicate lines on reconnect.
18. Routes, UI map, and Turbo structure
Engine routes (under mount prefix, e.g. /irc):
| Method | Path | Purpose |
|---|---|---|
| GET | / |
Full-page chat shell (optional) |
| POST | /connect |
Start IrcSession, queue worker |
| DELETE | /disconnect |
End session |
| POST | /channels |
JOIN |
| DELETE | /channels/:name |
PART |
| POST | /messages |
PRIVMSG to channel or query |
| GET | /sync |
Catch-up since since=<monotonic_id> |
| GET | /settings |
Turbo Frame modal |
| GET | /health |
Worker heartbeat (Β§21) |
18.1 Turbo layout (inside mount element)
[data-controller="irc-chat"] turbo-frame#irc-channel-list turbo-frame#irc-main turbo-frame#irc-log turbo-frame#irc-nicklist turbo-frame#irc-status form#irc-input β POST /messages
18.2 Input routing
| Input | Handler |
|---|---|
/join #chan |
Forward to worker as JOIN |
/part, /msg nick text, /nick new |
IRC command dispatcher |
| Plain text | PRIVMSG to active channel or query |
| Context menu actions | Turbo POST; not raw IRC |
19. style.ibis formatter spec (Phase 1)
Map reference/stuff/style.ibis to Rabbot::Irc::EventFormatter. Colour variables from vars.ibis / identifiers1.ibis become IrcClientConfig defaults (mIRC indices 0β98).
19.1 Event templates
| Event | style.ibis | Method | Output pattern |
|---|---|---|---|
| JOIN | L56β66 | format_join |
Coloured nick + Joins #chan (host) (ppl: N) |
| PART | L68 | format_part |
Coloured nick + Parted #chan + optional (reason) |
| QUIT | L98β106 | format_quit |
Address + reason; netsplit when $1- matches split pattern |
| KICK | L77 | format_kick |
knick been kicked from #chan (reason) (ppl: N) |
| TOPIC | L78 | format_topic |
Coloured nick + New Topic Set: + topic (plain body) |
| PRIVMSG # | L52 | format_channel_msg |
Timestamp + coloured nick + plain message body |
| PRIVMSG ? | L53 | format_query_msg |
Indented PM: nick bar + plain text |
| ACTION | L54β55 | format_action |
* nick action with action colour |
| NICK | L89β96 | format_nick_change |
Changed Nick (Previously known as old) |
| NOTICE | L79β86 | format_notice |
Route to active window, comchan, or status log |
19.2 Web-specific mapping
| Ibis concept | Web implementation |
|---|---|
%echo_indent |
CSS padding-left on log rows (configurable px) |
$its timestamp |
<time datetime="..."> element |
colorNick |
Nick colour from hash or @ list; op/voice prefix in nick list |
| Plain body rule | Match lib/CONVENTIONS.md β colour on nicks/events, not message body |
19.3 Golden tests
Add test/fixtures/irc/style_ibis_events.yml with input IRC hashes + expected HTML/mIRC formatted output. Reuse existing IrcFormat test patterns from rabbot-core.
20. Dialog priority matrix
Legacy scan found 20 dialogs; most are not worth porting.
| Dialog | Phase | Action |
|---|---|---|
settings |
2 | Implement β auto-join, colours, echo indent, companion bot |
setup_autojoin |
2 | Merge into settings |
topic |
2 | Channel topic editor (Turbo Frame) |
whois / who |
2 | Context menu β Turbo partial |
startup |
2 | Connection notes panel (Β§12.3 in main PRD) |
setup_flood |
3 | Flood limits |
lock |
3 | Screen lock / away overlay |
setup_ban |
3 | Ban list viewer |
xdccleech, mp3, pc, asdf, draw, diuqil, ircspy, first, chan, tdccchangesettings, whoischan |
β | Drop v1 β document as non-goals |
21. Deployment and environment
21.1 Processes
| Process | Role |
|---|---|
web |
Puma + ActionCable |
irc_worker |
Cinch connections (scale horizontally with connection sharding) |
solid_queue |
Companion lifecycle, event compaction, fandom profile generation |
21.2 Environment variables
RABBOT_IRC_WORKER_COUNT=2 RABBOT_IRC_EVENT_RETENTION_DAYS=7 RABBOT_IRC_EVENT_RETENTION_MAX=50000 RABBOT_IRC_ACTION_CABLE_ADAPTER=redis RABBOT_IRC_COMPANION_ENABLED=true RABBOT_THROTTLE_RECONNECT_FLOOR=6 RABBOT_DRONE_RECONNECT_FLOOR=8 RABBOT_MESH_HOST=127.0.0.1
21.3 Secrets
-
IRC passwords and SASL secrets: Active Record Encryption on
irc_credentials -
Never log
PASS,IDENTIFY, or raw SASL payloads (main PRD Β§7) -
Fandom URL fetch: allowlist
*.fandom.comonly (SSRF guard for profile generator)
21.4 Health check
GET /irc/health returns JSON:
{ "workers_alive": 2, "active_connections": 14, "oldest_ping_sec": 3 }
22. Test plan
| Layer | Tool | Scope |
|---|---|---|
| Unit | Minitest | EventFormatter, CapabilityProbe, SyncCatchup, CharacterTranscripts.fandom_url? |
| Integration | Mock IRC server (e.g. Celluloid IRC test double or local ircd) |
Cinch connect, JOIN, PRIVMSG, reconnect |
| System | Capybara + ActionCable::Connection::TestCase |
Embed in <div>, send message, assert Turbo append |
| Golden | YAML fixtures | style.ibis events vs formatted output |
| Staging smoke | Manual on Undernet trout | Not in default CI |
22.1 Companion bot tests
-
Invisible mode: simulate tab close (session unsubscribe) β worker continues β
GET /sync?since=returns missed events -
Visible mode (Phase 3): bot JOIN mirrors user channel set
-
Disable companion: assert Cinch QUIT within 10s; no ghost nick after 60s
22.2 Embed tests
-
Mount
irc-chatin non-full-page div; assert channel list and log render inside host bounds -
layout=compacthides nick list until toggled
23. Scale and data plane (later phases)
Not required for MVP. Documented for growth toward large user counts.
23.1 Data stores
| Layer | Technology | Role |
|---|---|---|
| Authoritative | PostgreSQL | Users, sessions, profiles, hot irc_events (partitioned) |
| Realtime | Redis | ActionCable/pubsub, presence, rate limits, worker queue, hot log buffer |
| Ingest (at scale) | Redis Streams or Kafka | Event pipe between IRC workers and WebSocket gateways |
| Cold tier | Object storage (S3) |
Aged events beyond retention window |
| HTTP edge | Vinyl Cache | Static assets and cacheable GETs only β not IRC/WebSocket |
23.2 Application boundaries
-
Rust (
rabbot-irc-core): IRC parse, connection multiplexer, WebSocket gateway when Ruby density limits hit -
Rails: UI, auth, settings, companion profile generation
-
Sharding: by
user_id; regional worker pools; own ircd leaf at 100k+ concurrent users -
Public IRC: not 1:1 connections at millions of users β product needs own network or pooled bouncers
23.3 Deferred infrastructure
-
Proxmox / VM HA: defer until service boundaries are stable; IRC TCP sessions require app-level reconnect regardless of hypervisor failover
-
BotSQLite: remains for persona plugins; only web/shared data moves to Postgres
24. Federated capacity (mesh donate)
Optional community capacity β extends the spirit of DirectchatMesh, with a new control plane.
24.1 Model
-
Rabbot operators opt in to donate connection slots plus CPU/memory caps (absolute limits, not percentage alone)
-
Central scheduler assigns web users to volunteer workers
-
Volunteers run standard worker images; no primary Postgres on volunteer nodes
-
Trust tiers; scoped tokens; invisible bouncer default
24.2 Non-goals
-
Open P2P with long-lived IRC passwords on arbitrary nodes
-
Volunteers as source of truth for user data
-
Replacing owned Redis/Postgres infrastructure
End of foundations supplement. Feature scope remains in pdf.md. Script inventory: ibis-script-reference.md.