Logging & Search
kokoIRC stores chat messages in a local SQLite database for persistent history and full-text search. Messages survive restarts and are available across sessions — no external service needed.
Overview
When logging is enabled, every message that reaches the UI is written to a SQLite database at ~/.kokoirc/logs.db. Messages filtered by /ignore, antiflood, or script stop() propagation are never stored.
The storage system is designed for speed and reliability:
- WAL mode — SQLite Write-Ahead Logging allows concurrent reads while writing
- Batched writes — messages are buffered and flushed every 50 messages or every 1 second (whichever comes first), in a single transaction
- FTS5 index — full-text search index for instant search across all channels and queries
- Read markers — per-client unread tracking, ready for future web frontend sync
Configuration
Add a [logging] section to ~/.kokoirc/config.toml:
[logging]
enabled = true
encrypt = false # AES-256-GCM (key auto-generated in ~/.kokoirc/.env)
retention_days = 0 # 0 = keep forever
exclude_types = [] # filter: "message", "action", "event", "notice", "ctcp"
Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable or disable chat logging entirely |
encrypt |
bool | false |
Encrypt message content with AES-256-GCM |
retention_days |
int | 0 |
Auto-delete messages older than N days. 0 = keep forever |
exclude_types |
array | [] |
Message types to exclude from logging |
Exclude types
The exclude_types array accepts any combination of these message types:
"message"— regular channel and private messages"action"—/meactions"event"— join, part, quit, kick, mode changes, topic changes"notice"— server and user notices"ctcp"— CTCP requests and replies
A common setup to skip noisy join/part/quit events:
[logging]
exclude_types = ["event"]
Database
Location
All logs are stored in a single file:
~/.kokoirc/logs.db
The database uses SQLite with WAL (Write-Ahead Logging) mode and PRAGMA synchronous=NORMAL for a good balance between performance and durability.
Schema
Messages are stored in a messages table with these columns:
| Column | Type | Description |
|---|---|---|
id |
INTEGER | Auto-incrementing primary key |
msg_id |
TEXT | UUID shared across TUI and web clients |
network |
TEXT | Server/network identifier |
buffer |
TEXT | Channel or query name |
timestamp |
INTEGER | Unix milliseconds |
type |
TEXT | Message type (message, action, event, notice, ctcp) |
nick |
TEXT | Sender nick (null for system events) |
text |
TEXT | Message content (or ciphertext when encrypted) |
highlight |
INTEGER | 1 if the message triggered a highlight |
iv |
BLOB | Initialization vector (only when encrypted) |
Indexes cover (network, buffer, timestamp), (timestamp), and (msg_id) for fast lookups.
Batched writes
Messages are not written to disk immediately. Instead, they are buffered in memory and flushed to the database when either condition is met:
- The buffer reaches 50 messages
- 1 second has elapsed since the first buffered message
Each flush writes all buffered messages in a single transaction, minimizing disk I/O and SQLite lock contention. On shutdown, any remaining buffered messages are flushed before the database closes.
Full-text search
When encryption is disabled, kokoIRC creates an FTS5 (Full-Text Search 5) virtual table that indexes the nick and text columns. This enables instant full-text search across all logged messages.
Using search
/log search <query>
Search runs against the current buffer's network and channel context. Results show the 20 most recent matches with timestamps.
Examples:
/log search ssl certificate
/log search meeting tomorrow
/log search "exact phrase"
The query uses SQLite FTS5 syntax under the hood, wrapped in a phrase match by default. This means your search terms are matched as an exact phrase.
Limitations
- Search is not available in encrypted mode — ciphertext cannot be indexed
- Results are limited to 50 matches (showing the 20 most recent in the UI)
Encryption
When encrypt = true, message content is encrypted with AES-256-GCM before being written to the database.
How it works
- On first use, a random 256-bit key is generated and saved to
~/.kokoirc/.envasKOKO_LOG_KEY - The
.envfile permissions are set to0600(owner read/write only) - Each message gets a unique 12-byte random IV (initialization vector)
- Only the
textcolumn is encrypted — metadata (network, buffer, nick, timestamp, type) remains in plaintext for indexing and querying
No password prompt is needed. The key lives alongside the database, following the same trust model as irssi log files or SSH keys — if someone has access to your home directory, they have access to your data.
Trade-offs
Enabling encryption means:
- FTS5 search is disabled (you cannot search encrypted content)
- Writes are slightly slower due to per-message async encryption
- The key must be present in
.envto read old messages
Encryption config
[logging]
encrypt = true
The key is auto-generated on first write. You'll find it in ~/.kokoirc/.env:
KOKO_LOG_KEY=<64-character hex string>
Back up this key if you want to preserve access to encrypted logs.
Read markers
The database tracks per-client read positions using a read_markers table. Each marker stores:
- network and buffer — which conversation
- client — identifier (
tuifor the terminal client, or a web session ID) - last_read — timestamp of the last read message
This enables unread count tracking per buffer per client. When multiple clients (TUI + future web frontend) access the same logs, each maintains its own read position.
Retention
Set retention_days to automatically purge old messages:
[logging]
retention_days = 90 # delete messages older than 90 days
0(default) = keep messages forever- Purging runs on startup — messages older than the configured period are deleted in a single transaction
- When FTS5 is active, the search index is cleaned up before rows are deleted to maintain consistency
Commands
/log status
Show logging status including message count, database size, encryption mode, and database path.
/log status
This is the default when you run /log with no subcommand.
Output example:
───── Log Status ─────────────────────────
Messages: 12,847
Database: 4.2 MB
Mode: plain text
Path: /home/user/.kokoirc/logs.db
─────────────────────────────────────────────
/log search <query>
Full-text search across all logged messages in the current buffer context.
/log search <query>
Results show the 20 most recent matches with timestamps and nicks.