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 > ;
}
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