Signal, Nostr & IRC
Privacy-first and decentralized channels with their auth and feature-flag specifics.
This page is the reference for Revka’s three privacy-first and decentralized chat channels: Signal (via a local signal-cli daemon bridge), Nostr (the decentralized relay protocol, with NIP-04 and NIP-17 encrypted DMs), and IRC (over TLS, with SASL and NickServ authentication). All three avoid handing your conversation to a centralized vendor API — Signal runs through encrypted infrastructure you bridge locally, Nostr is relay-based with no account server, and IRC connects directly to a server you choose. Use this page when you need exact config keys, auth flows, delivery behavior, and the build requirement for Nostr.
All three channels are configured under [channels_config] in ~/.revka/config.toml. Edit the file directly or run revka onboard --channels-only, then apply changes by restarting the daemon. For the trait model, delivery modes, and allowlist semantics shared by every channel, see Channels overview.
At a glance
Section titled “At a glance”| Channel | Receive mode | Public inbound port? | Build feature flag |
|---|---|---|---|
| Signal | signal-cli SSE bridge | No | — |
| Nostr | relay WebSocket | No | channel-nostr |
| IRC | TLS socket | No | — |
None of these channels needs a public inbound port — each one connects outward (to the local signal-cli daemon, to Nostr relays, or to the IRC server). Nostr is the only one of the three that is feature-gated at build time.
Signal
Section titled “Signal”The Signal channel does not talk to Signal’s servers directly. It bridges to a locally running signal-cli daemon over HTTP: it listens for incoming messages via Server-Sent Events (SSE) at /api/v1/events and sends replies via JSON-RPC at /api/v1/rpc. It supports both direct messages and Signal groups, and it relays typing indicators.
Prerequisites: run signal-cli in daemon mode
Section titled “Prerequisites: run signal-cli in daemon mode”Install and register signal-cli (per its own documentation), then start it in HTTP daemon mode:
signal-cli -a +1234567890 daemon --http 127.0.0.1:8686Revka connects to that HTTP endpoint. The bridge sends a JSON-RPC send request to deliver messages and sendTyping for typing indicators, and probes /api/v1/check for health.
Configure Signal
Section titled “Configure Signal”[channels_config.signal]http_url = "http://127.0.0.1:8686"account = "+1234567890"group_id = "" # "" / omit = all; "dm" = DMs only; or a base64 group idallowed_from = ["*"]ignore_attachments = falseignore_stories = trueproxy_url = ""| Field | Type | Default | Meaning |
|---|---|---|---|
http_url | string | — | Base URL of the running signal-cli HTTP daemon. Required. A trailing slash is stripped automatically. |
account | string | — | The registered Signal number in E.164 format (+1234567890). Required. Sent as the account query/RPC parameter. |
group_id | string | unset | Group filter. Omitted/unset accepts all DMs and groups; "dm" accepts direct messages only; a specific base64 group ID accepts only that group. |
allowed_from | array | — | Allowed senders. [] denies all; ["*"] allows everyone; otherwise list E.164 numbers (or UUIDs — see below). |
ignore_attachments | bool | false | When true, skip attachment-only messages (an attachment with no text body). |
ignore_stories | bool | false | Skip incoming Signal story notifications. |
proxy_url | string | — | Optional per-channel HTTP/HTTPS/SOCKS5 proxy that overrides the global [proxy] setting. |
Sender identity and the allowlist
Section titled “Sender identity and the allowlist”Signal reports a sender as a phone number (sourceNumber, E.164) or, for privacy-enabled users who have hidden their number, as a UUID. The channel prefers the E.164 number and falls back to the UUID. Match whichever form a sender presents in allowed_from. As with every channel, [] denies all and ["*"] allows all — start with ["*"] to confirm delivery, then tighten.
DM vs group routing
Section titled “DM vs group routing”The channel replies to wherever the message came from:
- A DM is answered back to the sender’s number (or UUID).
- A group message is answered to the group. Internally the reply recipient carries a
group:prefix (for examplegroup:<base64-id>), which routes the JSON-RPCsendtogroupIdinstead ofrecipient. You do not set this yourself — it is derived from the inbound message.
Use group_id = "dm" if you want the agent to ignore all group traffic and only answer direct messages.
Behavior notes
Section titled “Behavior notes”- The SSE listener reconnects automatically with exponential backoff (starting at 2s, capped at 60s).
- Typing indicators are sent via
sendTyping; there is no stop-typing call — Signal clients auto-expire the indicator after roughly 15 seconds. ignore_storiesdefaults tofalse; set it totrue(as the config example shows) to skip incoming Signal story notifications.
The Nostr channel connects to the decentralized Nostr network over relay WebSockets. It supports two private-message protocols and replies using the same protocol the sender used:
- NIP-04 — the legacy encrypted direct message (event kind 4).
- NIP-17 — modern gift-wrapped private messages (event kind 1059), which hide metadata better than NIP-04.
When Revka sends to a recipient it has not heard from, it defaults to NIP-17.
Configure Nostr
Section titled “Configure Nostr”[channels_config.nostr]private_key = "nsec1..." # hex or nsec bech32relays = ["wss://relay.damus.io", "wss://nos.lol"]allowed_pubkeys = ["npub1..."] # [] = deny all, ["*"] = allow all| Field | Type | Default | Meaning |
|---|---|---|---|
private_key | string | — | The bot identity’s secret key, in hex or nsec bech32. Required. High-value secret — see Key security below. |
relays | array | the four relays below | WebSocket relay URLs (wss://) to connect to. If omitted, Revka uses an opinionated default set. |
allowed_pubkeys | array | [] | Public keys (hex or npub) allowed to message the bot. [] denies all; ["*"] allows everyone. Invalid keys fail validation at startup. |
If you omit relays, Revka connects to these defaults:
relays = [ "wss://relay.damus.io", "wss://nos.lol", "wss://relay.primal.net", "wss://relay.snort.social",]Set relays explicitly to pin the network you trust rather than relying on the defaults.
NIP-04 vs NIP-17
Section titled “NIP-04 vs NIP-17”On startup the channel subscribes to both kind 4 (NIP-04) and kind 1059 (NIP-17 gift wrap) addressed to its public key. For each inbound message it records which protocol the sender used, so its reply matches:
- NIP-04 messages are decrypted in place; the event’s
created_atis the true timestamp. - NIP-17 messages are gift-wrapped — Revka unwraps the gift wrap and reads the inner rumor. The outer wrapper’s timestamp is deliberately jittered for privacy, so Revka uses the inner rumor’s timestamp to decide whether the message arrived after the listener started.
Messages from public keys outside allowed_pubkeys are logged and dropped before decryption is acted on. A message that fails to decrypt or unwrap is logged and skipped.
Key security
Section titled “Key security”Treat private_key as a high-value credential — it is the bot’s Nostr identity. When the encrypted secret store is enabled ([secrets] with encrypt = true), Revka decrypts private_key from the on-disk SecretStore at config load, so the key can be stored encrypted in config.toml rather than in plaintext. See Secrets, pairing & device auth.
The IRC channel connects to an IRC server over TLS, optionally authenticates with SASL and/or NickServ, joins the channels you list, and relays PRIVMSG traffic from both channels and private messages. Because IRC clients render plain text only, Revka prepends a style instruction to every inbound message telling the model to answer in plain text — no Markdown, no tables, no code fences.
Configure IRC
Section titled “Configure IRC”[channels_config.irc]server = "irc.libera.chat"port = 6697nickname = "revka-bot"username = "revka" # defaults to nicknamechannels = ["#revka", "#help"]allowed_users = ["*"]server_password = "" # PASS, e.g. for a ZNC bouncernickserv_password = "" # NickServ IDENTIFYsasl_password = "" # SASL PLAIN (IRCv3)verify_tls = true| Field | Type | Default | Meaning |
|---|---|---|---|
server | string | — | IRC server hostname. Required. |
port | number | 6697 | Server port. Defaults to the standard TLS port. |
nickname | string | — | The bot’s nick. Required. |
username | string | nickname | The IRC username (the user part of USER). Defaults to nickname. |
channels | array | [] | Channels to JOIN on connect (for example ["#revka"]). |
allowed_users | array | — | Allowed nicknames, matched case-insensitively, or ["*"]. [] denies all. |
server_password | string | — | Sent as the PASS command. Useful for bouncers like ZNC. |
nickserv_password | string | — | Password for PRIVMSG NickServ :IDENTIFY after registration. |
sasl_password | string | — | Password for SASL PLAIN authentication (IRCv3). |
verify_tls | bool | true | Verify the server’s TLS certificate. Leave on; only disable for testing against a self-signed server. |
The connection is always TLS — there is no plaintext mode. verify_tls = true validates the server certificate against the bundled root store; setting it to false disables certificate verification entirely and should be reserved for testing.
SASL authentication (IRCv3)
Section titled “SASL authentication (IRCv3)”When sasl_password is set, Revka authenticates with SASL PLAIN during the IRCv3 capability handshake, before joining channels:
- Revka sends
CAP REQ :sasl. - On
CAP * ACK :sasl, it sendsAUTHENTICATE PLAIN. - When the server replies
AUTHENTICATE +, Revka sends the base64-encodedPLAINcredentials built from the current nick andsasl_password. - On
903(RPL_SASLSUCCESS) it sendsCAP ENDand proceeds to registration.
If the server replies CAP * NAK :sasl (SASL not supported), Revka logs a warning and continues without SASL. If SASL fails (904, 905, 906, or 907), Revka logs the failure code and ends the capability negotiation rather than retrying forever.
NickServ authentication
Section titled “NickServ authentication”When nickserv_password is set, Revka identifies to NickServ after registration completes (on the 001 / RPL_WELCOME numeric):
PRIVMSG NickServ :IDENTIFY <nickserv_password>SASL and NickServ are independent — you can use either, both, or neither. SASL is preferable where the network supports it because it authenticates before you join any channel. Messages from NickServ and ChanServ are always ignored by the agent.
Connection and message behavior
Section titled “Connection and message behavior”- Channel vs DM replies. Messages to a channel (
#nameor&name) are answered in that channel and are prefixed with<sender>context for the model; private messages are answered back to the sender. - Nick in use. If the nick is taken (
433/ ERR_NICKNAMEINUSE), Revka appends_and retries (for examplerevka-bot_). - Fatal auth. A
464(ERR_PASSWDMISMATCH) ends the connection with a password-mismatch error. - Keepalive. Server
PINGs are answered withPONGautomatically. - Long messages. Replies are split into multiple
PRIVMSGlines to stay within the IRC 512-byte line limit, splitting at safe UTF-8 boundaries; embedded newlines become separate lines and blank lines are dropped. - Read timeout. If no data arrives for 300 seconds the connection is treated as dead; reconnection is handled by the channel supervisor.
The channel-nostr feature flag
Section titled “The channel-nostr feature flag”Nostr support is compiled behind the Cargo feature channel-nostr, which adds the nostr-sdk dependency. Nostr is NOT in the default feature set and is NOT compiled into the standard release binaries (default Cargo features are observability-prometheus and skill-creation; release binaries add channel-matrix, channel-lark, whatsapp-web). You must build with --features channel-nostr (e.g. cargo build --release --features channel-nostr) to get Nostr. Signal and IRC have no feature flag and are always compiled in.
Build with Nostr explicitly:
cargo build --release --features channel-nostrIf you build with --no-default-features and a hand-picked feature list, add channel-nostr back when you want Nostr. For the complete list of channel feature flags (Matrix, Lark, Nostr, WhatsApp Web, Voice Wake), see Cargo feature flags & ADRs.
Allowlist semantics
Section titled “Allowlist semantics”Each channel gates inbound senders, using a different field name per platform:
| Channel | Allowlist field | Matches on |
|---|---|---|
| Signal | allowed_from | E.164 phone number or UUID |
| Nostr | allowed_pubkeys | public key (hex or npub) |
| IRC | allowed_users | nickname (case-insensitive) |
In all three cases:
- An empty list (
[]) denies all senders. - A single
"*"entry allows all senders. - Otherwise, only listed identities are accepted.
Start with the wildcard to confirm the bot is connected and replying, then replace it with explicit identities for production.
Secrets at rest
Section titled “Secrets at rest”When the encrypted secret store is enabled ([secrets] encrypt = true), Revka decrypts these channel credentials from the SecretStore at config load, so they can live encrypted in config.toml:
- Nostr:
private_key - IRC:
server_password,nickserv_password,sasl_password
The Signal channel has no encryptable token — it performs no network authentication of its own, so its security depends on the local signal-cli daemon binding. See Secrets, pairing & device auth and the Security model.
Related pages
Section titled “Related pages”- Channels overview — the channel trait, delivery modes, and allowlist model.
- Connect a messaging channel — the general flow for enabling a channel.
- Cargo feature flags & ADRs — the
channel-nostrflag and other build options. - Secrets, pairing & device auth — encrypting channel credentials at rest.
- Config: channels, tools & integrations — the full
[channels_config]reference. - Onboarding wizard (revka onboard) — configure channels interactively.