Multiplayer & Social Features
Jarvis includes built-in multiplayer capabilities: online presence tracking, real-time chat with end-to-end encryption, and collaborative terminal sessions.
Architecture
All networking features use Supabase Realtime (Phoenix Channels over WebSocket):
Jarvis Client
↓
Supabase Realtime WebSocket
↓
Phoenix Channel v1 Protocol
↓
- Presence Tracking
- Chat Broadcasts
- Room Management
Online Presence
Track who’s online with status updates and activity tracking
Live Chat
Multi-channel chat with E2E encryption and message signing
Direct Messages
End-to-end encrypted 1:1 conversations using ECDH
Poke System
Send notifications to other users with targeted broadcasts
Presence System
Online User Tracking
The presence system tracks users across the jarvis-presence channel:
use jarvis_social::PresenceClient;
let client = PresenceClient::new(config);
client.track_presence(user_id, PresenceState {
display_name: "alice".into(),
status: UserStatus::Online,
activity: Some("Editing code".into()),
online_at: SystemTime::now(),
}).await?;
User Status Values
Online
Idle
In Game
Do Not Disturb
Active and available (green dot) Away from keyboard (yellow dot) Playing arcade games (blue dot) Focused work mode (red dot)
Presence Events
pub enum PresenceEvent {
Connected { online_count: usize },
Disconnected,
UserOnline { user: UserPresence },
UserOffline { user_id: String },
ActivityChanged { user: UserPresence },
Poked { from_user_id: String, from_name: String },
// ...
}
Poke System
Send a notification to another user:
client.poke(target_user_id).await?;
In the presence panel WebView:
window.jarvis.ipc.send('presence_poke', {
target_user_id: '123e4567-e89b-12d3-a456-426614174000'
});
Recipient receives a desktop notification with the sender’s name.
Live Chat System
Multi-Channel Chat
Seven pre-configured channels:
| Channel | Purpose |
|---|
# general | Primary channel (presence tracking) |
# discord | Discord integration |
# showoff | Share projects and screenshots |
# help | Ask questions |
# random | Off-topic discussion |
# games | Gaming coordination |
# memes | Fun and memes |
Message Structure
{
"id": "uuid",
"userId": "uuid",
"nick": "alice",
"ts": 1700000000000,
"text": "hello world",
"sig": "base64-ecdsa-signature",
"pubkey": "base64-spki-der",
"fingerprint": "aa:bb:cc:dd:ee:ff:00:11"
}
Sending Messages
Flow:
- User types message and presses Enter
- AutoMod checks (keywords, spam, rate limit)
- Emoji shortcodes replaced (
:smile: → 😀)
- Message signed with ECDSA P-256 key
- Broadcast on Supabase channel
- Rendered locally with
verifyStatus: 'self'
Code example:
const message = {
id: crypto.randomUUID(),
userId: this.userId,
nick: this.nickname,
ts: Date.now(),
text: messageText,
sig: await Identity.sign(canonicalString),
pubkey: Identity.pubkeyBase64,
fingerprint: Identity.fingerprint
};
await channel.send({
type: 'broadcast',
event: 'message',
payload: message
});
Receiving Messages
Flow:
- Broadcast arrives on channel subscription
- Self-echo prevention (skip if
userId === this.userId)
- AutoMod filters (keywords, spam, rate limit)
- Signature verification via IPC
- TOFU trust check (fingerprint matches previous?)
- Verification badge attached (✓/⚠/✗/?)
- Message rendered and stored
Message Verification
Verified (✓)
Key Changed (⚠)
Invalid (✗)
Unverified (?)
Signature valid, TOFU trusted or new identity Signature valid but fingerprint differs from previousSystem message: “alice’s identity key changed (possible impersonation)” Signature verification failed No signature present (legacy message)verifyStatus: 'unverified'
Direct Messages (E2E Encrypted)
Key Exchange:
- User clicks “DM” button in user dropdown
- ECDH shared key derived:
deriveSharedKey(otherDhPubkey)
- Deterministic channel name:
jarvis-dm-{sorted-fingerprints}
- New Supabase channel subscription created
Message Encryption:
const sharedKey = await Identity.deriveSharedKey(peerDhPubkey);
const { iv, ct } = await Crypto.encrypt(plaintext, sharedKey);
const dmMessage = {
id: crypto.randomUUID(),
userId: this.userId,
nick: this.nickname,
ts: Date.now(),
iv: iv, // Instead of 'text'
ct: ct,
sig: await Identity.sign(`${id}|${userId}|${nick}|${ts}|${iv}|${ct}`),
// ...
};
Decryption:
const plaintext = await Crypto.decrypt(msg.iv, msg.ct, sharedKey);
DM signatures are computed over ciphertext components (iv, ct), not plaintext, to prevent chosen-plaintext attacks.
AutoMod System
Client-side moderation runs on both outgoing and incoming messages:
Keyword Filter:
- Banned words checked with word-boundary regex
- Case-insensitive matching
- Runtime add/remove:
AutoMod.addBanWord(word)
Rate Limiting:
| Parameter | Default |
|---|
RATE_LIMIT_COUNT | 5 messages |
RATE_LIMIT_WINDOW | 10 seconds |
Spam Detection:
- Repeated character ratio >80% → blocked
- Last 3 identical messages → blocked
- Periodic cleanup every 60 seconds
Configuration:
[livechat.automod]
enabled = true
filter_profanity = true
rate_limit = 5
max_message_length = 500
spam_detection = true
Image Messages
Paste or drag-drop images into chat:
Compression:
- Canvas resize to max 300px width
- JPEG quality 0.5
- Max size: 150,000 chars (~100 KB base64)
Rendering:
if (msg.text.startsWith('data:image/')) {
const img = document.createElement('img');
img.src = msg.text;
img.style.maxWidth = '300px';
img.onclick = () => showLightbox(msg.text);
container.appendChild(img);
}
Reactions
Emoji reactions on hover:
channel.send({
type: 'broadcast',
event: 'reaction',
payload: {
msgId: message.id,
emoji: '👍',
userId: this.userId,
action: 'add' // or 'remove'
}
});
16 emoji picker: 👍 ❤️ 😂 😮 😢 🎉 🔥 ✅ ❌ 👀 🚀 💯 ⚡ 🎯 🌟 ✨
Rooms & Channels
Channel Management
Rust-side ChannelManager tracks membership:
use jarvis_social::ChannelManager;
let mut manager = ChannelManager::new();
manager.join_channel(user_id, "general");
manager.join_channel(user_id, "games");
let user_channels = manager.user_channels(user_id);
let channel_members = manager.channel_members("general");
Chat History
Bounded in-memory ring buffer:
pub struct ChatHistory {
messages: HashMap<String, VecDeque<ChatMessage>>,
max_per_channel: usize, // Default: 500
}
WebView also caps history at 500 messages + 600 DOM nodes per channel.
Security Model
Cryptographic Identity
Each Jarvis instance generates P-256 key pairs:
- ECDSA signing key: Message signing/verification
- ECDH key: Diffie-Hellman key agreement
Keys stored in ~/.config/jarvis/identity.json (mode 0600):
{
"version": 1,
"ecdsa_pkcs8_b64": "...",
"ecdh_pkcs8_b64": "..."
}
Fingerprint
First 8 bytes of SHA-256(ECDSA SPKI DER), formatted as:
Trust On First Use (TOFU)
Nickname-to-fingerprint bindings stored in localStorage:
const tofuStore = JSON.parse(localStorage.getItem('jarvis-chat-tofu') || '{}');
if (!tofuStore[nick]) {
tofuStore[nick] = fingerprint; // First seen
verifyStatus = 'trusted';
} else if (tofuStore[nick] !== fingerprint) {
verifyStatus = 'changed'; // Key rotation or impersonation
} else {
verifyStatus = 'trusted'; // Matches previous
}
End-to-End Encryption
Three encryption contexts:
- Channel messages: AES-256-GCM with PBKDF2-derived room key (10k iterations)
- DMs: AES-256-GCM with ECDH-derived shared key
- Mobile relay: AES-256-GCM with ECDH (desktop ↔ mobile)
Key Material Isolation
Private keys never leave Rust:
// WebView only receives handles
const keyHandle = await window.jarvis.ipc.request('crypto', {
op: 'derive_shared_key',
dhPubkey: peerPublicKey
});
// Encrypt using handle (key never exposed)
const { iv, ct } = await window.jarvis.ipc.request('crypto', {
op: 'encrypt',
plaintext: message,
keyHandle: keyHandle
});
Configuration Reference
Presence Config
[presence]
enabled = true
server_url = "" # Supabase project ref
heartbeat_interval = 30 # seconds
Livechat Config
[livechat]
enabled = true
server_port = 19847
connection_timeout = 10
[livechat.nickname]
default = ""
persist = true
allow_change = true
[livechat.nickname.validation]
min_length = 1
max_length = 20
pattern = "^[a-zA-Z0-9_\\- ]+$"
[livechat.automod]
enabled = true
filter_profanity = true
rate_limit = 5
max_message_length = 500
spam_detection = true
Source files:
jarvis-rs/crates/jarvis-social/src/
jarvis-rs/assets/panels/chat/index.html
jarvis-rs/assets/panels/presence/index.html
docs/manual/09-networking.md
Supabase Realtime uses Phoenix Channels v1 protocol with 25-second heartbeat and exponential backoff reconnection (1s base, 30s max).