Skip to content

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.

ChannelReceive modePublic inbound port?Build feature flag
Signalsignal-cli SSE bridgeNo
Nostrrelay WebSocketNochannel-nostr
IRCTLS socketNo

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.

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:

Terminal window
signal-cli -a +1234567890 daemon --http 127.0.0.1:8686

Revka 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.

[channels_config.signal]
http_url = "http://127.0.0.1:8686"
account = "+1234567890"
group_id = "" # "" / omit = all; "dm" = DMs only; or a base64 group id
allowed_from = ["*"]
ignore_attachments = false
ignore_stories = true
proxy_url = ""
FieldTypeDefaultMeaning
http_urlstringBase URL of the running signal-cli HTTP daemon. Required. A trailing slash is stripped automatically.
accountstringThe registered Signal number in E.164 format (+1234567890). Required. Sent as the account query/RPC parameter.
group_idstringunsetGroup filter. Omitted/unset accepts all DMs and groups; "dm" accepts direct messages only; a specific base64 group ID accepts only that group.
allowed_fromarrayAllowed senders. [] denies all; ["*"] allows everyone; otherwise list E.164 numbers (or UUIDs — see below).
ignore_attachmentsboolfalseWhen true, skip attachment-only messages (an attachment with no text body).
ignore_storiesboolfalseSkip incoming Signal story notifications.
proxy_urlstringOptional per-channel HTTP/HTTPS/SOCKS5 proxy that overrides the global [proxy] setting.

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.

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 example group:<base64-id>), which routes the JSON-RPC send to groupId instead of recipient. 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.

  • 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_stories defaults to false; set it to true (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.

[channels_config.nostr]
private_key = "nsec1..." # hex or nsec bech32
relays = ["wss://relay.damus.io", "wss://nos.lol"]
allowed_pubkeys = ["npub1..."] # [] = deny all, ["*"] = allow all
FieldTypeDefaultMeaning
private_keystringThe bot identity’s secret key, in hex or nsec bech32. Required. High-value secret — see Key security below.
relaysarraythe four relays belowWebSocket relay URLs (wss://) to connect to. If omitted, Revka uses an opinionated default set.
allowed_pubkeysarray[]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.

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_at is 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.

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.

[channels_config.irc]
server = "irc.libera.chat"
port = 6697
nickname = "revka-bot"
username = "revka" # defaults to nickname
channels = ["#revka", "#help"]
allowed_users = ["*"]
server_password = "" # PASS, e.g. for a ZNC bouncer
nickserv_password = "" # NickServ IDENTIFY
sasl_password = "" # SASL PLAIN (IRCv3)
verify_tls = true
FieldTypeDefaultMeaning
serverstringIRC server hostname. Required.
portnumber6697Server port. Defaults to the standard TLS port.
nicknamestringThe bot’s nick. Required.
usernamestringnicknameThe IRC username (the user part of USER). Defaults to nickname.
channelsarray[]Channels to JOIN on connect (for example ["#revka"]).
allowed_usersarrayAllowed nicknames, matched case-insensitively, or ["*"]. [] denies all.
server_passwordstringSent as the PASS command. Useful for bouncers like ZNC.
nickserv_passwordstringPassword for PRIVMSG NickServ :IDENTIFY after registration.
sasl_passwordstringPassword for SASL PLAIN authentication (IRCv3).
verify_tlsbooltrueVerify 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.

When sasl_password is set, Revka authenticates with SASL PLAIN during the IRCv3 capability handshake, before joining channels:

  1. Revka sends CAP REQ :sasl.
  2. On CAP * ACK :sasl, it sends AUTHENTICATE PLAIN.
  3. When the server replies AUTHENTICATE +, Revka sends the base64-encoded PLAIN credentials built from the current nick and sasl_password.
  4. On 903 (RPL_SASLSUCCESS) it sends CAP END and 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.

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.

  • Channel vs DM replies. Messages to a channel (#name or &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 example revka-bot_).
  • Fatal auth. A 464 (ERR_PASSWDMISMATCH) ends the connection with a password-mismatch error.
  • Keepalive. Server PINGs are answered with PONG automatically.
  • Long messages. Replies are split into multiple PRIVMSG lines 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.

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:

Terminal window
cargo build --release --features channel-nostr

If 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.

Each channel gates inbound senders, using a different field name per platform:

ChannelAllowlist fieldMatches on
Signalallowed_fromE.164 phone number or UUID
Nostrallowed_pubkeyspublic key (hex or npub)
IRCallowed_usersnickname (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.

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.