Skip to main content
The presence system tracks which Jarvis users are online, their current status, and activity. It connects to Supabase Realtime via Phoenix Channels and maintains real-time user state across all connected instances.

Architecture

PresenceClient (Rust)
  |
  +-- RealtimeClient (WebSocket)
  |     |
  |     +-- wss://<project>.supabase.co/realtime/v1/websocket
  |
  +-- event_translator task
        |
        +-- PresenceEvent stream --> UI
The Rust-side PresenceClient (jarvis-rs/crates/jarvis-social/src/presence/) handles all communication with Supabase Realtime. A background event translator task converts low-level WebSocket events into high-level presence events consumed by the UI. Source files:
  • jarvis-rs/crates/jarvis-social/src/presence/client.rs
  • jarvis-rs/crates/jarvis-social/src/presence/types.rs
  • presence/client.py (Python implementation)

Connection Flow

1

Initialize Client

Create a PresenceClient with your identity and configuration:
let config = PresenceConfig {
    project_ref: "your-project-ref".to_string(),
    api_key: "your-anon-key".to_string(),
    access_token: None,
    heartbeat_interval: 25,
    reconnect_delay: 1,
    max_reconnect_delay: 30,
};

let identity = Identity {
    user_id: uuid::Uuid::new_v4().to_string(),
    display_name: "alice".to_string(),
    access_token: None,
};

let mut client = PresenceClient::new(identity, config);
2

Connect and Subscribe

Start the connection and receive presence events:
let mut event_rx = client.start();

// Process events in your main loop
while let Some(event) = event_rx.recv().await {
    match event {
        PresenceEvent::Connected { online_count } => {
            println!("Connected! {} users online", online_count);
        }
        PresenceEvent::UserOnline(user) => {
            println!("{} is now online", user.display_name);
        }
        // ... handle other events
    }
}
3

Track Your Presence

The client automatically tracks your presence on the jarvis-presence channel with this payload:
{
  "user_id": "<uuid>",
  "display_name": "alice",
  "status": "online",
  "activity": null,
  "online_at": "2026-03-05T12:00:00Z"
}

User Status Values

The presence system supports six status values:
pub enum UserStatus {
    Online,       // Default - user is active
    Idle,         // User is away from keyboard
    InGame,       // User is playing a game
    InSkill,      // User is running a skill
    DoNotDisturb, // User doesn't want notifications
    Away,         // User is away
}

Presence Events

EventDescriptionPayload
ConnectedChannel joined successfullyonline_count: u32
DisconnectedConnection lost(none)
UserOnlineAnother user joinedOnlineUser struct
UserOfflineAnother user leftuser_id, display_name
ActivityChangedUser’s status or activity changedOnlineUser struct
GameInviteUser broadcast a game invitationuser_id, display_name, game, code
PokedSomeone poked this useruser_id, display_name
ChatMessageChat message on presence channeluser_id, display_name, channel, content
ErrorConnection or protocol errorString (error message)

Protocol Messages

Activity Update

Broadcast when a user’s status or activity changes:
{
  "type": "activity_update",
  "user_id": "<uuid>",
  "display_name": "alice",
  "status": "in_game",
  "activity": "Playing Snake"
}

Game Invite

Broadcast a game invitation to all online users:
{
  "type": "game_invite",
  "user_id": "<uuid>",
  "display_name": "alice",
  "game": "snake",
  "code": "ROOM123"
}

Poke

Send a targeted notification to a specific user:
{
  "type": "poke",
  "user_id": "<sender-uuid>",
  "display_name": "alice",
  "target_user_id": "<recipient-uuid>"
}

Python Client

Jarvis also includes a Python presence client (presence/client.py) with similar functionality:
from presence.client import PresenceClient

client = PresenceClient(
    server_url="wss://project.supabase.co/realtime/v1/websocket",
    user_id="alice-123",
    display_name="alice"
)

# Set callback for notifications
def handle_notification(event_type, data):
    if event_type == "user_online":
        print(f"{data['display_name']} is online")
    elif event_type == "poke":
        print(f"Poked by {data['display_name']}!")

client.on_notification = handle_notification

# Run the client
await client.run()

Python Protocol Messages

Connect:
await ws.send(json.dumps({
    "type": "connect",
    "user_id": "alice-123",
    "display_name": "alice",
    "version": "1"
}))
Heartbeat:
# Sent every 30 seconds
await ws.send(json.dumps({"type": "ping"}))
Response:
{"type": "pong", "online_count": 5}

Heartbeat and Reconnection

The Supabase Realtime connection uses Phoenix heartbeat messages to maintain the connection.

Configuration

ParameterDefaultDescription
heartbeat_interval25sHow often to send heartbeat messages
reconnect_delay1sBase delay before reconnecting
max_reconnect_delay30sMaximum reconnection delay

Reconnection Strategy

The client uses exponential backoff for reconnection:
delay = min(delay * 2, max_reconnect_delay)
On reconnect, the client automatically:
  1. Re-joins the jarvis-presence channel
  2. Re-tracks presence with current status
  3. Receives full presence state from the server

OnlineUser Structure

pub struct OnlineUser {
    pub user_id: String,
    pub display_name: String,
    pub status: UserStatus,
    pub activity: Option<String>,
    pub online_at: String,
}

Integration Example

use jarvis_social::presence::{PresenceClient, PresenceConfig, PresenceEvent};
use jarvis_social::identity::Identity;
use jarvis_social::protocol::UserStatus;

#[tokio::main]
async fn main() {
    // Create identity
    let identity = Identity {
        user_id: uuid::Uuid::new_v4().to_string(),
        display_name: std::env::var("USER").unwrap_or("jarvis".to_string()),
        access_token: None,
    };

    // Configure presence
    let config = PresenceConfig {
        project_ref: "your-project".to_string(),
        api_key: "your-key".to_string(),
        ..Default::default()
    };

    // Start client
    let mut client = PresenceClient::new(identity, config);
    let mut events = client.start();

    // Process events
    tokio::spawn(async move {
        while let Some(event) = events.recv().await {
            match event {
                PresenceEvent::Connected { online_count } => {
                    println!("🟢 Connected ({} online)", online_count);
                }
                PresenceEvent::UserOnline(user) => {
                    println!("👋 {} joined", user.display_name);
                }
                PresenceEvent::Poked { display_name, .. } => {
                    println!("👉 Poked by {}!", display_name);
                    // Show desktop notification
                }
                _ => {}
            }
        }
    });

    // Update activity
    client.update_activity(
        UserStatus::InSkill,
        Some("Running a terminal command".to_string())
    ).await;

    // Send a poke
    client.send_poke("another-user-id").await;
}

Configuration Reference

From jarvis-rs/crates/jarvis-config/src/schema/social.rs:
[presence]
enabled = true
server_url = ""         # Supabase project ref
heartbeat_interval = 30 # seconds

Channel Name

All presence events use the channel name jarvis-presence. This is a constant defined in client.rs:23.