Skip to main content

Real-Time Updates with Convex

JARVIS uses Convex as its real-time database, eliminating the need for manual WebSocket setup. As agents gather intelligence, data streams instantly to the frontend—papers spawn on the corkboard, dossiers update, and connections draw in real-time.

Why Convex?

Zero WebSocket Setup

No manual connection management or reconnection logic

Automatic Reactivity

UI re-renders only when subscribed data changes

TypeScript Native

Full type safety from backend to frontend

Optimistic Updates

UI updates instantly, syncs with backend after

Convex Setup

Installation

npm install convex
npx convex dev
This creates:
  • convex/ directory with schema and functions
  • .env.local with NEXT_PUBLIC_CONVEX_URL

Provider Setup

Wrap your app with ConvexProvider:
// app/ConvexClientProvider.tsx
import { ConvexProvider, ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function ConvexClientProvider({ children }) {
  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
// app/layout.tsx
import { ConvexClientProvider } from "./ConvexClientProvider";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ConvexClientProvider>{children}</ConvexClientProvider>
      </body>
    </html>
  );
}

Using Queries (Reading Data)

Subscribe to Person List

import { useQuery } from "convex/react";
import { api } from "../../convex/_generated/api";

function PersonList() {
  // Automatically re-renders when persons table changes
  const persons = useQuery(api.persons.listAll);
  
  if (persons === undefined) return <div>Loading...</div>;
  
  return (
    <div>
      {persons.map(person => (
        <PersonCard key={person._id} person={person} />
      ))}
    </div>
  );
}

Subscribe to Intel Fragments

function IntelBoard({ personId }) {
  // Only re-renders when intel for THIS person changes
  const intel = useQuery(api.intel.byPerson, { personId });
  
  return (
    <div>
      {intel?.map(fragment => (
        <PaperDocument key={fragment._id} data={fragment} />
      ))}
    </div>
  );
}

Subscribe to Activity Log

function ActivityFeed() {
  const activity = useQuery(api.intel.recentActivity, { limit: 50 });
  
  return (
    <div>
      {activity?.map(event => (
        <ActivityItem key={event._id} event={event} />
      ))}
    </div>
  );
}

Using Mutations (Writing Data)

Create New Person

import { useMutation } from "convex/react";

function CaptureButton() {
  const createPerson = useMutation(api.persons.create);
  
  const handleCapture = async (imageUrl: string) => {
    const personId = await createPerson({
      name: "Unknown",
      photoUrl: imageUrl,
      confidence: 0,
      status: "identified",
      boardPosition: { x: 100, y: 100 },
    });
    
    console.log("Created person:", personId);
  };
  
  return <button onClick={handleCapture}>Capture</button>;
}

Update Dossier

function DossierEditor({ personId }) {
  const updateDossier = useMutation(api.persons.updateDossier);
  
  const saveDossier = async (dossier) => {
    await updateDossier({ personId, dossier });
  };
  
  return <form onSubmit={saveDossier}>...</form>;
}

Add Intel Fragment

function AgentResult({ personId, source, data }) {
  const createIntel = useMutation(api.intel.create);
  
  useEffect(() => {
    createIntel({
      personId,
      source,
      dataType: "profile",
      content: JSON.stringify(data),
      verified: false,
      timestamp: Date.now(),
    });
  }, [data]);
  
  return null;
}

Backend Integration

The Python backend sends data to Convex via HTTP:
# backend/db/convex_client.py
import httpx

class ConvexGateway:
    def __init__(self, settings):
        self.url = settings.convex_url
        self.client = httpx.AsyncClient()
    
    async def create_person(self, name, photo_url, confidence):
        response = await self.client.post(
            f"{self.url}/api/mutations/persons:create",
            json={
                "name": name,
                "photoUrl": photo_url,
                "confidence": confidence,
                "status": "identified",
                "boardPosition": {"x": 100, "y": 100},
                "createdAt": time.time() * 1000,
                "updatedAt": time.time() * 1000,
            }
        )
        return response.json()["personId"]
    
    async def add_intel_fragment(self, person_id, source, data):
        await self.client.post(
            f"{self.url}/api/mutations/intel:create",
            json={
                "personId": person_id,
                "source": source,
                "dataType": "profile",
                "content": json.dumps(data),
                "verified": False,
                "timestamp": time.time() * 1000,
            }
        )

Streaming Research Results

Combine Convex with SSE for real-time streaming:
function StreamingResearch({ personName }) {
  const createIntel = useMutation(api.intel.create);
  const intel = useQuery(api.intel.byPerson, { personId });
  
  useEffect(() => {
    // Connect to backend SSE stream
    const eventSource = new EventSource(
      `http://localhost:8000/api/research/${personName}/stream`
    );
    
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      
      // Immediately persist to Convex
      createIntel({
        personId: data.personId,
        source: data.source,
        dataType: data.type,
        content: JSON.stringify(data.content),
        verified: false,
        timestamp: Date.now(),
      });
    };
    
    return () => eventSource.close();
  }, [personName]);
  
  // UI automatically updates as intel query changes
  return (
    <div>
      {intel?.map(fragment => <IntelCard key={fragment._id} data={fragment} />)}
    </div>
  );
}

Optimistic Updates

For instant UI feedback:
function PersonCard({ person }) {
  const updatePosition = useMutation(api.persons.updatePosition);
  const [position, setPosition] = useState(person.boardPosition);
  
  const handleDragEnd = async (newX, newY) => {
    // Update UI immediately
    setPosition({ x: newX, y: newY });
    
    // Sync with backend (non-blocking)
    await updatePosition({ personId: person._id, x: newX, y: newY });
  };
  
  return <div style={{ left: position.x, top: position.y }}>...</div>;
}

Performance Characteristics

Update Latency

Backend mutation → Frontend render: 50-150ms

Subscription Overhead

Minimal - no manual state management required

Scalability

Handles 100+ concurrent subscriptions per client

Offline Support

Automatic reconnection with state sync

Debugging

View Live Queries

import { useConvex } from "convex/react";

const convex = useConvex();
console.log("Active subscriptions:", convex._subscriptions);

Monitor Updates

useEffect(() => {
  console.log("Persons updated:", persons);
}, [persons]);
Convex subscriptions run continuously. Avoid creating subscriptions inside loops or frequently re-mounting components. Move subscriptions to the highest stable component.

Next Steps

Convex Schema

Explore the complete database schema

Dossier View

See real-time updates in action

Build docs developers (and LLMs) love