Convex Backend
Overview
Convex is the sole backend — no Express, no custom server. All state, business logic, and external API calls run through Convex functions. The frontend connects via real-time subscriptions.
Schema
Four tables in convex/schema.ts:
| Table |
Purpose |
Key Fields |
investigations |
Root record for each investigation |
query, targetName, targetDescription, targetPhone, targetPhoto, knownLinks, extremeMode, status, stepCount, browserSessionId, browserLiveUrl, report, confidence, errorMessage, totalInputTokens, totalOutputTokens, estimatedCost, behavioralAnalysis, createdAt, completedAt |
findings |
Individual evidence items discovered |
investigationId (FK), source, category, platform, profileUrl, imageUrl, data, confidence, latitude, longitude, createdAt |
steps |
Activity log for real-time frontend stream |
investigationId (FK), stepNumber, action, tool, result, screenshot, createdAt |
graph_edges |
Relationship connections between entities |
investigationId (FK), sourceLabel, sourceType, targetLabel, targetType, relationship, createdAt |
Investigation Status Flow
planning → investigating → analyzing → complete
→ failed
Indexes
findings.by_investigation → ["investigationId"]
steps.by_investigation → ["investigationId"]
graph_edges.by_investigation → ["investigationId"]
Function Types
| Type |
Use For |
File Access |
Can Call APIs |
query |
Reading data, frontend subscriptions |
Read-only DB |
No |
mutation |
Writing data, state changes |
Read/write DB |
No |
action |
External API calls, orchestration |
Via runQuery/runMutation |
Yes |
internalAction |
Tool implementations (not exposed to client) |
Via runQuery/runMutation |
Yes |
Public vs Internal
- Public (
action, mutation, query): Callable from frontend via api.*
- Internal (
internalAction, internalMutation): Only callable from other Convex functions via internal.*
Most tool implementations (convex/tools/*.ts) use internalAction — they're only called by the orchestrator, never directly from the frontend.
Key Patterns
Creating an investigation
// convex/investigations.ts — create mutation
const id = await ctx.db.insert("investigations", {
query: `Investigate ${args.targetName}`,
targetName: args.targetName,
targetDescription: args.targetDescription,
targetPhone: args.targetPhone,
targetPhoto: args.targetPhoto,
knownLinks: args.knownLinks,
extremeMode: args.extremeMode ?? false,
status: "planning",
stepCount: 0,
createdAt: Date.now(),
});
Querying with indexes
// Always use .withIndex() for FK lookups
const findings = await ctx.db
.query("findings")
.withIndex("by_investigation", (q) =>
q.eq("investigationId", args.investigationId)
)
.order("desc")
.collect();
Scheduling next step (self-chaining)
// convex/orchestrator.ts — chains via scheduler
await ctx.scheduler.runAfter(0, internal.orchestrator.step, {
investigationId: args.investigationId,
conversationHistory: JSON.stringify(finalHistory),
consecutiveSaveOnlySteps: consecutiveSaveOnly,
maigretAvailable,
extremeMode,
});
This avoids the 10-minute Convex action timeout by splitting each step into its own action invocation.
Actions calling mutations
// Inside an action handler
await ctx.runMutation(api.investigations.updateStatus, {
id: args.investigationId,
status: "investigating",
});
File uploads
// Generate upload URL for photos
export const generateUploadUrl = mutation({
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
File Map
| File |
Exports |
Purpose |
convex/schema.ts |
default schema |
Table definitions + indexes |
convex/investigations.ts |
create, get, list, updateStatus, updateReport, updateBrowserSession, incrementStep, getFindings, getSteps, addStep, addSteps, addFinding, updateTokenUsage, updateBehavioralAnalysis, generateUploadUrl |
All investigation CRUD |
convex/orchestrator.ts |
startInvestigation (action), step (internalAction) |
Opus agentic loop |
convex/reports.ts |
getReport |
Assembles investigation + findings + steps |
convex/graphEdges.ts |
addEdge, addEdges, getEdges |
Relationship graph CRUD |
convex/tools/braveSearch.ts |
search (internalAction) |
Brave Search API |
convex/tools/browserUse.ts |
runTask, getSession, stopSession (internalActions) |
Browser Use Cloud v3 |
convex/tools/maigret.ts |
search, investigate, healthCheck (internalActions) |
Username OSINT via sidecar |
convex/tools/picarta.ts |
localize (internalAction) |
Picarta AI geolocation |
convex/tools/intelx.ts |
search (internalAction) |
Intelligence X dark web search |
convex/tools/reverseImageSearch.ts |
search (internalAction) |
Google Lens via SerpAPI |
Environment Variables
Set in Convex dashboard (Settings → Environment Variables), NOT in .env:
ANTHROPIC_API_KEY — Claude API for orchestrator
BROWSER_USE_API_KEY — Browser Use Cloud
BRAVE_API_KEY — Brave Search API (fast web lookups)
PICARTA_API_KEY — Picarta AI geolocation
INTELX_API_KEY — Intelligence X dark web search (extreme mode)
SERPAPI_API_KEY — SerpAPI for reverse image search
MAIGRET_SIDECAR_URL — Maigret sidecar URL (optional, defaults to http://localhost:8000)
Gotchas
- No
fetch in mutations/queries — Only actions can make HTTP requests
- 10-minute timeout — Actions auto-terminate. Use
scheduler.runAfter(0, ...) to chain long-running workflows
- Convex IDs are typed — Use
v.id("tableName"), not v.string() for foreign keys
- No raw SQL — Use
.query() builder with .withIndex(), .filter(), .order()
- Conversation history is serialized — Stored as JSON string since Convex doesn't support deeply nested dynamic objects in validators
- Batch mutations —
addSteps and addEdges accept arrays and insert in a loop with shared timestamp, reducing round trips
1---2name: convex-backend3description: Convex database schema, mutations, queries, actions, and scheduler patterns4---56# Convex Backend78## Overview910Convex is the sole backend — no Express, no custom server. All state, business logic, and external API calls run through Convex functions. The frontend connects via real-time subscriptions.1112## Schema1314Four tables in `convex/schema.ts`:1516| Table | Purpose | Key Fields |17|-------|---------|------------|18| `investigations` | Root record for each investigation | `query`, `targetName`, `targetDescription`, `targetPhone`, `targetPhoto`, `knownLinks`, `extremeMode`, `status`, `stepCount`, `browserSessionId`, `browserLiveUrl`, `report`, `confidence`, `errorMessage`, `totalInputTokens`, `totalOutputTokens`, `estimatedCost`, `behavioralAnalysis`, `createdAt`, `completedAt` |19| `findings` | Individual evidence items discovered | `investigationId` (FK), `source`, `category`, `platform`, `profileUrl`, `imageUrl`, `data`, `confidence`, `latitude`, `longitude`, `createdAt` |20| `steps` | Activity log for real-time frontend stream | `investigationId` (FK), `stepNumber`, `action`, `tool`, `result`, `screenshot`, `createdAt` |21| `graph_edges` | Relationship connections between entities | `investigationId` (FK), `sourceLabel`, `sourceType`, `targetLabel`, `targetType`, `relationship`, `createdAt` |2223### Investigation Status Flow2425```26planning → investigating → analyzing → complete27 → failed28```2930### Indexes3132- `findings.by_investigation` → `["investigationId"]`33- `steps.by_investigation` → `["investigationId"]`34- `graph_edges.by_investigation` → `["investigationId"]`3536## Function Types3738| Type | Use For | File Access | Can Call APIs |39|------|---------|-------------|--------------|40| `query` | Reading data, frontend subscriptions | Read-only DB | No |41| `mutation` | Writing data, state changes | Read/write DB | No |42| `action` | External API calls, orchestration | Via runQuery/runMutation | Yes |43| `internalAction` | Tool implementations (not exposed to client) | Via runQuery/runMutation | Yes |4445### Public vs Internal4647- **Public** (`action`, `mutation`, `query`): Callable from frontend via `api.*`48- **Internal** (`internalAction`, `internalMutation`): Only callable from other Convex functions via `internal.*`4950Most tool implementations (`convex/tools/*.ts`) use `internalAction` — they're only called by the orchestrator, never directly from the frontend.5152## Key Patterns5354### Creating an investigation5556```typescript57// convex/investigations.ts — create mutation58const id = await ctx.db.insert("investigations", {59 query: `Investigate ${args.targetName}`,60 targetName: args.targetName,61 targetDescription: args.targetDescription,62 targetPhone: args.targetPhone,63 targetPhoto: args.targetPhoto,64 knownLinks: args.knownLinks,65 extremeMode: args.extremeMode ?? false,66 status: "planning",67 stepCount: 0,68 createdAt: Date.now(),69});70```7172### Querying with indexes7374```typescript75// Always use .withIndex() for FK lookups76const findings = await ctx.db77 .query("findings")78 .withIndex("by_investigation", (q) =>79 q.eq("investigationId", args.investigationId)80 )81 .order("desc")82 .collect();83```8485### Scheduling next step (self-chaining)8687```typescript88// convex/orchestrator.ts — chains via scheduler89await ctx.scheduler.runAfter(0, internal.orchestrator.step, {90 investigationId: args.investigationId,91 conversationHistory: JSON.stringify(finalHistory),92 consecutiveSaveOnlySteps: consecutiveSaveOnly,93 maigretAvailable,94 extremeMode,95});96```9798This avoids the 10-minute Convex action timeout by splitting each step into its own action invocation.99100### Actions calling mutations101102```typescript103// Inside an action handler104await ctx.runMutation(api.investigations.updateStatus, {105 id: args.investigationId,106 status: "investigating",107});108```109110### File uploads111112```typescript113// Generate upload URL for photos114export const generateUploadUrl = mutation({115 handler: async (ctx) => {116 return await ctx.storage.generateUploadUrl();117 },118});119```120121## File Map122123| File | Exports | Purpose |124|------|---------|---------|125| `convex/schema.ts` | default schema | Table definitions + indexes |126| `convex/investigations.ts` | `create`, `get`, `list`, `updateStatus`, `updateReport`, `updateBrowserSession`, `incrementStep`, `getFindings`, `getSteps`, `addStep`, `addSteps`, `addFinding`, `updateTokenUsage`, `updateBehavioralAnalysis`, `generateUploadUrl` | All investigation CRUD |127| `convex/orchestrator.ts` | `startInvestigation` (action), `step` (internalAction) | Opus agentic loop |128| `convex/reports.ts` | `getReport` | Assembles investigation + findings + steps |129| `convex/graphEdges.ts` | `addEdge`, `addEdges`, `getEdges` | Relationship graph CRUD |130| `convex/tools/braveSearch.ts` | `search` (internalAction) | Brave Search API |131| `convex/tools/browserUse.ts` | `runTask`, `getSession`, `stopSession` (internalActions) | Browser Use Cloud v3 |132| `convex/tools/maigret.ts` | `search`, `investigate`, `healthCheck` (internalActions) | Username OSINT via sidecar |133| `convex/tools/picarta.ts` | `localize` (internalAction) | Picarta AI geolocation |134| `convex/tools/intelx.ts` | `search` (internalAction) | Intelligence X dark web search |135| `convex/tools/reverseImageSearch.ts` | `search` (internalAction) | Google Lens via SerpAPI |136137## Environment Variables138139Set in Convex dashboard (Settings → Environment Variables), NOT in `.env`:140141- `ANTHROPIC_API_KEY` — Claude API for orchestrator142- `BROWSER_USE_API_KEY` — Browser Use Cloud143- `BRAVE_API_KEY` — Brave Search API (fast web lookups)144- `PICARTA_API_KEY` — Picarta AI geolocation145- `INTELX_API_KEY` — Intelligence X dark web search (extreme mode)146- `SERPAPI_API_KEY` — SerpAPI for reverse image search147- `MAIGRET_SIDECAR_URL` — Maigret sidecar URL (optional, defaults to `http://localhost:8000`)148149## Gotchas150151- **No `fetch` in mutations/queries** — Only actions can make HTTP requests152- **10-minute timeout** — Actions auto-terminate. Use `scheduler.runAfter(0, ...)` to chain long-running workflows153- **Convex IDs are typed** — Use `v.id("tableName")`, not `v.string()` for foreign keys154- **No raw SQL** — Use `.query()` builder with `.withIndex()`, `.filter()`, `.order()`155- **Conversation history is serialized** — Stored as JSON string since Convex doesn't support deeply nested dynamic objects in validators156- **Batch mutations** — `addSteps` and `addEdges` accept arrays and insert in a loop with shared timestamp, reducing round trips