Skip to main content

Convex Integration

Last Updated: 2025-10-28

How Cortex leverages Convex features for persistent memory, vector search, and real-time updates.

Overview

Cortex is built natively on Convex - not as a wrapper, but as a first-class Convex application. We use Convex's features directly:

  • Queries - Read operations (reactive, cacheable)
  • Mutations - Write operations (ACID transactions)
  • Actions - External calls (embeddings, pub/sub)
  • Vector Search - Native semantic similarity
  • Search Indexes - Full-text keyword search
  • Real-time - Reactive query subscriptions
  • TypeScript - End-to-end type safety

Convex Function Types

Queries (Read Operations)

Queries are reactive, cached, and deterministic:

// convex/memories.ts
import { query } from "./_generated/server";
import { v } from "convex/values";

export const get = query({
args: { memorySpaceId: v.string(), memoryId: v.id("memories") },
handler: async (ctx, args) => {
const memory = await ctx.db.get(args.memoryId);

// Verify agent owns this memory
if (!memory || memory.agentId !== args.agentId) {
return null;
}

// Update access tracking (read-only in query, so we schedule mutation)
await ctx.scheduler.runAfter(0, "memories:updateAccessCount", {
memoryId: args.memoryId,
});

return memory;
},
});

export const search = query({
args: {
memorySpaceId: v.string(),
query: v.string(),
embedding: v.optional(v.array(v.float64())),
filters: v.any(),
},
handler: async (ctx, args) => {
let results;

if (args.embedding) {
// Semantic search
results = await ctx.db
.query("memories")
.withIndex("by_embedding", (q) =>
q
.similar("embedding", args.embedding, args.filters.limit || 20)
.eq("agentId", args.agentId),
)
.collect();
} else {
// Keyword search
results = await ctx.db
.query("memories")
.withSearchIndex("by_content", (q) =>
q.search("content", args.query).eq("agentId", args.agentId),
)
.collect();
}

// Apply filters (importance, tags, dates, etc.)
return applyFilters(results, args.filters);
},
});

Characteristics:

  • ✅ Read-only (cannot modify database)
  • ✅ Reactive (auto-update on data changes)
  • ✅ Cacheable (Convex caches results)
  • ✅ Fast (optimized by Convex)

Mutations (Write Operations)

Mutations modify data with ACID guarantees:

// convex/memories.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const store = mutation({
args: {
memorySpaceId: v.string(),
content: v.string(),
contentType: v.union(v.literal("raw"), v.literal("summarized")),
embedding: v.optional(v.array(v.float64())),
source: v.any(),
conversationRef: v.optional(v.any()),
metadata: v.any(),
},
handler: async (ctx, args) => {
// Insert into memories table
const memoryId = await ctx.db.insert("memories", {
...args,
version: 1,
previousVersions: [],
accessCount: 0,
createdAt: Date.now(),
updatedAt: Date.now(),
});

return await ctx.db.get(memoryId);
},
});

export const update = mutation({
args: {
memorySpaceId: v.string(),
memoryId: v.id("memories"),
updates: v.any(),
},
handler: async (ctx, args) => {
const memory = await ctx.db.get(args.memoryId);

if (!memory || memory.agentId !== args.agentId) {
throw new Error("MEMORY_NOT_FOUND");
}

// Create new version (preserve old)
const newVersion = memory.version + 1;
const previousVersions = [
...memory.previousVersions,
{
version: memory.version,
content: memory.content,
metadata: memory.metadata,
timestamp: memory.updatedAt,
},
];

// Apply retention (keep last 10 versions)
const retention = 10;
if (previousVersions.length > retention) {
previousVersions.shift(); // Remove oldest
}

// Update document
await ctx.db.patch(args.memoryId, {
...args.updates,
version: newVersion,
previousVersions,
updatedAt: Date.now(),
});

return await ctx.db.get(args.memoryId);
},
});

export const updateAccessCount = mutation({
args: { memoryId: v.id("memories") },
handler: async (ctx, args) => {
await ctx.db.patch(args.memoryId, {
accessCount: (await ctx.db.get(args.memoryId))!.accessCount + 1,
lastAccessed: Date.now(),
});
},
});

Characteristics:

  • ✅ Atomic (all-or-nothing)
  • ✅ Consistent (sees latest data)
  • ✅ Isolated (no race conditions)
  • ✅ Durable (persisted immediately)

Actions (External Calls)

Actions can call external APIs (embeddings, pub/sub):

// convex/actions.ts
import { action } from "./_generated/server";
import { v } from "convex/values";

export const storeWithEmbedding = action({
args: {
memorySpaceId: v.string(),
content: v.string(),
metadata: v.any(),
},
handler: async (ctx, args) => {
// 1. Call external embedding API
const response = await fetch("https://api.openai.com/v1/embeddings", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "text-embedding-3-large",
input: args.content,
}),
});

const data = await response.json();
const embedding = data.data[0].embedding;

// 2. Store in database via mutation
return await ctx.runMutation("memories:store", {
...args,
embedding,
contentType: "raw",
source: { type: "system", timestamp: Date.now() },
});
},
});

export const publishA2ANotification = action({
args: {
memorySpaceId: v.string(),
notification: v.any(),
},
handler: async (ctx, args) => {
// Call external pub/sub (Redis, RabbitMQ, etc.)
const redis = await connectRedis(process.env.REDIS_URL);

await redis.publish(
`agent:${args.agentId}:inbox`,
JSON.stringify(args.notification),
);

await redis.quit();
},
});

Characteristics:

  • ✅ Can call external APIs
  • ✅ Can call mutations/queries
  • ✅ Non-deterministic allowed
  • ✅ Used for embeddings, pub/sub, webhooks

How Cortex Uses Each Type

Queries (Read Path)

All read operations use Convex queries:

// SDK call
const memory = await cortex.memory.get("agent-1", "mem_abc");

// Becomes Convex query
await client.query(api.memories.get, {
memorySpaceId: "agent-1",
memoryId: "mem_abc",
});

// Reactive in UI
const { data: memory } = useQuery(api.memories.get, {
memorySpaceId: "agent-1",
memoryId: "mem_abc",
});
// ↑ Auto-updates when memory changes!

Cortex queries:

  • memories.get - Get single memory
  • memories.search - Semantic or keyword search
  • memories.list - Paginated listing
  • memories.count - Count with filters
  • conversations.get - Get conversation
  • conversations.getHistory - Get message thread
  • immutable.get - Get immutable record
  • mutable.get - Get mutable value
  • contexts.get - Get context
  • users.get - Get user profile (via immutable)

Mutations (Write Path)

All write operations use Convex mutations:

// SDK call
await cortex.memory.store("agent-1", data);

// Becomes Convex mutation
await client.mutation(api.memories.store, {
memorySpaceId: "agent-1",
...data,
});

Cortex mutations:

  • memories.store - Create memory
  • memories.update - Update memory (creates version)
  • memories.delete - Delete memory
  • conversations.create - Create conversation
  • conversations.addMessage - Append message
  • immutable.store - Store versioned record
  • mutable.set - Set mutable value
  • mutable.update - Atomic update
  • contexts.create - Create context
  • contexts.update - Update context status
  • users.update - Update user profile (via immutable)

Actions (External Integration)

Actions handle non-deterministic operations:

// Direct Mode: Developer calls embedding API
const embedding = await openai.embeddings.create({ ... });
await cortex.memory.store('agent-1', { content, embedding });

// Cloud Mode: Cortex action calls embedding API
await cortex.memory.store('agent-1', {
content,
autoEmbed: true, // ← Triggers action
});

// Convex action (Cloud Mode backend)
export const storeWithAutoEmbed = action({
args: { memorySpaceId: v.string(), content: v.string(), ... },
handler: async (ctx, args) => {
// Call OpenAI
const embedding = await generateEmbedding(args.content);

// Store via mutation
return await ctx.runMutation("memories:store", {
...args,
embedding,
});
},
});

Cortex actions:

  • memories.storeWithAutoEmbed - Cloud Mode auto-embeddings
  • a2a.publishNotification - Pub/sub integration
  • users.cascadeDelete - Cloud Mode GDPR cascade
  • governance.enforceRetention - Cleanup jobs

Vector Search Implementation

Vector Index Definition

// convex/schema.ts
memories: defineTable({
memorySpaceId: v.string(),
content: v.string(),
embedding: v.optional(v.array(v.float64())),
// ...
}).vectorIndex("by_embedding", {
vectorField: "embedding",
dimensions: 3072, // Configurable per deployment
filterFields: ["agentId", "userId"], // Fast pre-filtering
});

Vector Search Query

// convex/memories.ts
export const semanticSearch = query({
args: {
memorySpaceId: v.string(),
embedding: v.array(v.float64()),
userId: v.optional(v.string()),
limit: v.number(),
},
handler: async (ctx, args) => {
let q = ctx.db
.query("memories")
.withIndex("by_embedding", (q) =>
q
.similar("embedding", args.embedding, args.limit)
.eq("agentId", args.agentId),
);

// Optional user filtering (pre-filtered before similarity)
if (args.userId) {
q = q.filter((q) => q.eq(q.field("userId"), args.userId));
}

return await q.collect();
},
});

Convex vector search features:

  • Cosine similarity (built-in)
  • Pre-filtering before similarity (filterFields)
  • Multiple dimensions supported (1536, 3072, etc.)
  • Sub-100ms for millions of vectors

Full-Text Search Implementation

Search Index Definition

// convex/schema.ts
memories: defineTable({
content: v.string(),
memorySpaceId: v.string(),
userId: v.optional(v.string()),
// ...
}).searchIndex("by_content", {
searchField: "content",
filterFields: ["agentId", "userId"],
});

Keyword Search Query

// convex/memories.ts
export const keywordSearch = query({
args: {
memorySpaceId: v.string(),
keywords: v.string(),
userId: v.optional(v.string()),
limit: v.number(),
},
handler: async (ctx, args) => {
let results = await ctx.db
.query("memories")
.withSearchIndex("by_content", (q) =>
q.search("content", args.keywords).eq("agentId", args.agentId),
)
.take(args.limit);

// Additional filtering
if (args.userId) {
results = results.filter((m) => m.userId === args.userId);
}

return results;
},
});

Convex search features:

  • Tokenization (automatic)
  • Ranking by relevance
  • Prefix matching
  • Fast filtering before search

ACID Transactions

Single-Document Operations

// Automatic transaction for single doc
export const addMessage = mutation({
args: { conversationId: v.id("conversations"), message: v.any() },
handler: async (ctx, args) => {
const conversation = await ctx.db.get(args.conversationId);

// Atomic update (read + modify + write)
await ctx.db.patch(args.conversationId, {
messages: [...conversation.messages, args.message],
messageCount: conversation.messageCount + 1,
updatedAt: Date.now(),
lastMessageAt: Date.now(),
});
},
});

Multi-Document Operations

// All mutations are transactional
export const remember = mutation({
args: { memorySpaceId: v.string(), conversationId: v.id("conversations"), userMessage: v.string(), agentResponse: v.string() },
handler: async (ctx, args) => {
// Step 1: Add to conversation (ACID)
const userMsgId = await ctx.db.insert("messages", { ... });
const agentMsgId = await ctx.db.insert("messages", { ... });

// Step 2: Create vector memory (ACID)
const memoryId = await ctx.db.insert("memories", {
conversationRef: {
conversationId: args.conversationId,
messageIds: [userMsgId, agentMsgId],
},
...
});

// If any step fails, ALL steps roll back ✅
return { userMsgId, agentMsgId, memoryId };
},
});

Mutable Store Transactions

// Optimistic locking for mutable updates
export const atomicUpdate = mutation({
args: { namespace: v.string(), key: v.string(), updater: v.string() },
handler: async (ctx, args) => {
// Get current value
const record = await ctx.db
.query("mutable")
.withIndex("by_namespace_key", (q) =>
q.eq("namespace", args.namespace).eq("key", args.key),
)
.unique();

// Apply update function
const updaterFn = eval(args.updater); // Simplified
const newValue = updaterFn(record.value);

// Atomic update
await ctx.db.patch(record._id, {
value: newValue,
updatedAt: Date.now(),
});

// No race conditions - Convex handles concurrency ✅
},
});

Convex ACID benefits:

  • All mutations are transactions
  • No manual locking needed
  • Optimistic concurrency control
  • Isolation levels automatic

Reactive Queries

Client-Side Reactivity

// React component
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function AgentMemoryList({ agentId }) {
// Reactive query - auto-updates!
const memories = useQuery(api.memories.list, {
agentId,
filters: { minImportance: 50 },
});

const storeMemory = useMutation(api.memories.store);

return (
<div>
{memories?.map(m => (
<div key={m._id}>{m.content}</div>
))}

<button onClick={() => storeMemory({ agentId, content: "New memory" })}>
Add Memory
</button>
</div>
);
// ↑ List auto-updates when mutation runs!
}

Server-Side Subscriptions

// Node.js server
import { ConvexClient } from "convex/browser";

const client = new ConvexClient(process.env.CONVEX_URL);

// Subscribe to query results
client.onUpdate(api.memories.list, { memorySpaceId: "agent-1" }, (memories) => {
console.log(`Agent now has ${memories.length} memories`);
// Called every time data changes!
});

Cortex usage:

  • Real-time dashboards (Cloud Mode)
  • Live collaboration features
  • Agent activity monitoring

Compound Operations (Layer 3)

remember() - Multi-Layer Mutation

The remember() helper coordinates multiple mutations:

// SDK call
await cortex.memory.remember({
memorySpaceId: 'agent-1',
conversationId: 'conv-123',
userMessage: 'Hello',
agentResponse: 'Hi!',
userId: 'user-123',
userName: 'Alex',
});

// Becomes coordinated mutations
export const remember = mutation({
args: { ... },
handler: async (ctx, args) => {
// 1. Add user message to conversation (Layer 1a)
const userMsgId = await ctx.runMutation("conversations:addMessage", {
conversationId: args.conversationId,
message: {
id: generateId(),
role: "user",
content: args.userMessage,
userId: args.userId,
timestamp: Date.now(),
},
});

// 2. Add agent message to conversation (Layer 1a)
const agentMsgId = await ctx.runMutation("conversations:addMessage", {
conversationId: args.conversationId,
message: {
id: generateId(),
role: "agent",
content: args.agentResponse,
memorySpaceId: args.agentId,
timestamp: Date.now(),
},
});

// 3. Create vector memory (Layer 2)
const memoryId = await ctx.db.insert("memories", {
memorySpaceId: args.agentId,
userId: args.userId,
content: `${args.userName}: ${args.userMessage}\nAgent: ${args.agentResponse}`,
contentType: "summarized",
source: {
type: "conversation",
userId: args.userId,
userName: args.userName,
timestamp: Date.now(),
},
conversationRef: {
conversationId: args.conversationId,
messageIds: [userMsgId, agentMsgId], // ← Links to Layer 1
},
metadata: args.metadata,
version: 1,
previousVersions: [],
accessCount: 0,
createdAt: Date.now(),
updatedAt: Date.now(),
});

// All 3 steps are atomic (Convex transaction)
return {
conversation: { messageIds: [userMsgId, agentMsgId] },
memories: [memoryId],
};
},
});

Transaction guarantee: All 3 inserts succeed or all fail (no partial state).


Index Usage Patterns

Agent Isolation

// Compound index for agent+user queries
.index("by_agent_userId", ["agentId", "userId"])

// Efficient query
const memories = await ctx.db
.query("memories")
.withIndex("by_agent_userId", (q) =>
q.eq("agentId", agentId).eq("userId", userId)
)
.collect();
// Uses compound index ✅ O(log n)

GDPR Cascade

// userId index across all tables
.index("by_userId", ["userId"])

// Fast cascade query
const allMemories = await ctx.db
.query("memories")
.withIndex("by_userId", (q) => q.eq("userId", "user-123"))
.collect();

// Delete all (in transaction)
for (const memory of allMemories) {
await ctx.db.delete(memory._id);
}

Hierarchical Contexts

// Multiple indexes for hierarchy navigation
.index("by_parentId", ["parentId"])
.index("by_rootId", ["rootId"])
.index("by_depth", ["depth"])

// Get all children
await ctx.db
.query("contexts")
.withIndex("by_parentId", (q) => q.eq("parentId", parentId))
.collect();

// Get entire workflow
await ctx.db
.query("contexts")
.withIndex("by_rootId", (q) => q.eq("rootId", rootId))
.collect();

Versioning Implementation

Automatic Version Management

export const update = mutation({
handler: async (ctx, args) => {
const current = await ctx.db.get(args.id);

// Create version snapshot
const snapshot = {
version: current.version,
content: current.content,
metadata: current.metadata,
timestamp: current.updatedAt,
};

// Add to history (with retention)
const previousVersions = [...current.previousVersions, snapshot];

// Apply retention limit
const retention = args.retention || 10;
while (previousVersions.length > retention) {
previousVersions.shift(); // Remove oldest
}

// Update with new version
await ctx.db.patch(args.id, {
content: args.newContent,
version: current.version + 1,
previousVersions,
updatedAt: Date.now(),
});
},
});

Stored in same document - no separate versions table needed.


Real-Time Features

Live Dashboard Example

// React component with real-time updates
function AgentDashboard({ agentId }) {
// Query auto-updates when data changes
const stats = useQuery(api.agents.getStats, { agentId });
const recentMemories = useQuery(api.memories.list, {
agentId,
limit: 10,
});
const activeContexts = useQuery(api.contexts.list, {
agentId,
status: "active",
});

return (
<div>
<h2>{stats?.name}</h2>
<p>{stats?.totalMemories} memories</p>
<p>{recentMemories?.length} recent</p>
<p>{activeContexts?.length} active workflows</p>

{/* All values update in real-time! */}
</div>
);
}

Subscription-Based Triggers

// Server-side agent runner
const client = new ConvexClient(process.env.CONVEX_URL);

// Watch for new A2A messages
client.onUpdate(
api.memories.list,
{
memorySpaceId: "hr-agent",
filters: {
"source.type": "a2a",
"metadata.direction": "inbound",
"metadata.responded": false,
},
},
async (pendingRequests) => {
for (const request of pendingRequests) {
// Process request
const answer = await processRequest(request.content);

// Respond
await client.mutation(api.a2a.send, {
from: "hr-agent",
to: request.source.fromAgent,
message: answer,
});
}
},
);

Performance Optimizations

Pagination

export const list = query({
args: {
memorySpaceId: v.string(),
limit: v.number(),
offset: v.number(),
},
handler: async (ctx, args) => {
const memories = await ctx.db
.query("memories")
.withIndex("by_agent", (q) => q.eq("agentId", args.agentId))
.order("desc") // Most recent first
.skip(args.offset)
.take(args.limit)
.collect();

const total = await ctx.db
.query("memories")
.withIndex("by_agent", (q) => q.eq("agentId", args.agentId))
.count();

return {
memories,
total,
hasMore: args.offset + args.limit < total,
};
},
});

Lazy Loading

// Get conversation without messages initially
export const getConversationMeta = query({
args: { conversationId: v.id("conversations") },
handler: async (ctx, args) => {
const conversation = await ctx.db.get(args.conversationId);

// Return metadata only (no messages)
return {
conversationId: conversation._id,
type: conversation.type,
participants: conversation.participants,
messageCount: conversation.messageCount,
lastMessageAt: conversation.lastMessageAt,
};
},
});

// Load messages separately when needed
export const getMessages = query({
args: {
conversationId: v.id("conversations"),
limit: v.number(),
offset: v.number(),
},
handler: async (ctx, args) => {
const conversation = await ctx.db.get(args.conversationId);

// Paginate messages
return conversation.messages.slice(args.offset, args.offset + args.limit);
},
});

Filter Before Sort

// ✅ Efficient: Use index, then filter, then sort
const memories = await ctx.db
.query("memories")
.withIndex("by_agent", (q) => q.eq("agentId", agentId))
.filter((q) => q.gte(q.field("metadata.importance"), 70))
.order("desc") // Sort by createdAt
.take(20)
.collect();

// ❌ Inefficient: Filter without index
const all = await ctx.db.query("memories").collect();
const filtered = all.filter(
(m) => m.agentId === agentId && m.metadata.importance >= 70,
);

TypeScript Type Safety

Generated API Types

// Convex generates types from schema
import { api } from "./_generated/api";
import { Id, Doc } from "./_generated/dataModel";

// Type-safe function calls
const memory: Doc<"memories"> = await client.query(api.memories.get, {
memorySpaceId: "agent-1",
memoryId: "mem_abc" as Id<"memories">,
});

// Type errors caught at compile-time
await client.query(api.memories.get, {
memorySpaceId: 123, // ❌ TypeScript error: number not assignable to string
memoryId: "mem_abc",
});

SDK Type Wrappers

// Cortex SDK wraps Convex types
import { MemoryEntry } from "@cortex-platform/sdk";

// Convex Doc<"memories"> → Cortex MemoryEntry
class CortexSDK {
async get(
memorySpaceId: string,
memoryId: string,
): Promise<MemoryEntry | null> {
const doc = await this.client.query(api.memories.get, {
agentId,
memoryId: memoryId as Id<"memories">,
});

// Transform Convex doc to SDK type
return doc ? this.toMemoryEntry(doc) : null;
}

private toMemoryEntry(doc: Doc<"memories">): MemoryEntry {
return {
id: doc._id,
memorySpaceId: doc.agentId,
userId: doc.userId,
content: doc.content,
contentType: doc.contentType,
embedding: doc.embedding,
source: doc.source,
conversationRef: doc.conversationRef,
metadata: doc.metadata,
createdAt: new Date(doc.createdAt),
updatedAt: new Date(doc.updatedAt),
lastAccessed: doc.lastAccessed ? new Date(doc.lastAccessed) : undefined,
accessCount: doc.accessCount,
version: doc.version,
previousVersions: doc.previousVersions,
};
}
}

Scheduler for Background Tasks

Access Count Updates

// Query schedules mutation (queries are read-only)
export const get = query({
handler: async (ctx, args) => {
const memory = await ctx.db.get(args.memoryId);

// Schedule access count update (runs after query completes)
await ctx.scheduler.runAfter(0, api.memories.updateAccessCount, {
memoryId: args.memoryId,
});

return memory;
},
});

// Scheduled mutation
export const updateAccessCount = mutation({
args: { memoryId: v.id("memories") },
handler: async (ctx, args) => {
const memory = await ctx.db.get(args.memoryId);
await ctx.db.patch(args.memoryId, {
accessCount: memory.accessCount + 1,
lastAccessed: Date.now(),
});
},
});

Retention Cleanup

// Cron job for governance
export const cleanupOldVersions = mutation({
args: {},
handler: async (ctx) => {
const memories = await ctx.db.query("memories").collect();

for (const memory of memories) {
if (memory.previousVersions.length > 10) {
// Trim to retention limit
await ctx.db.patch(memory._id, {
previousVersions: memory.previousVersions.slice(-10),
});
}
}
},
});

// Schedule daily
export default {
cleanupVersions: {
schedule: "0 2 * * *", // 2 AM daily
handler: api.governance.cleanupOldVersions,
},
};

Error Handling

Convex Error Patterns

export const store = mutation({
handler: async (ctx, args) => {
// Validation
if (!args.content || args.content.length === 0) {
throw new ConvexError({
code: "INVALID_CONTENT",
message: "Content cannot be empty",
});
}

if (args.metadata.importance < 0 || args.metadata.importance > 100) {
throw new ConvexError({
code: "INVALID_IMPORTANCE",
message: "Importance must be 0-100",
});
}

// Permission check
const agent = await ctx.db
.query("agents")
.withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
.first();

if (!agent) {
throw new ConvexError({
code: "INVALID_AGENT_ID",
message: "Agent not found",
});
}

// Insert
try {
return await ctx.db.insert("memories", args);
} catch (error) {
throw new ConvexError({
code: "CONVEX_ERROR",
message: "Database insert failed",
details: error,
});
}
},
});

SDK Error Transformation

// Cortex SDK catches Convex errors
try {
await cortex.memory.store("agent-1", data);
} catch (error) {
// Transform Convex error to Cortex error
if (error.data?.code) {
throw new CortexError(
error.data.code,
error.data.message,
error.data.details,
);
}
throw error;
}

Deployment Patterns

Direct Mode (Your Convex Instance)

// Your Convex backend
// convex/memories.ts - Your functions
export const store = mutation({ ... });
export const search = query({ ... });

// Your app
import { ConvexClient } from "convex/browser";
import { Cortex } from "@cortex-platform/sdk";

const convexClient = new ConvexClient(process.env.CONVEX_URL);

// Cortex SDK uses your Convex client
const cortex = new Cortex({
convexUrl: process.env.CONVEX_URL,
// OR provide client directly
client: convexClient,
});

Cloud Mode (Cortex-Managed Functions)

// Your Convex instance (just storage)
// No custom functions needed - Cortex Cloud provides them

// Your app
const cortex = new Cortex({
mode: "cloud",
apiKey: process.env.CORTEX_CLOUD_KEY,
});

// Cortex Cloud API:
// - Deploys functions to your Convex
// - OR uses Cortex-hosted functions
// - Manages credentials securely

Next Steps


Questions? Ask in GitHub Discussions or Discord.