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 memorymemories.search- Semantic or keyword searchmemories.list- Paginated listingmemories.count- Count with filtersconversations.get- Get conversationconversations.getHistory- Get message threadimmutable.get- Get immutable recordmutable.get- Get mutable valuecontexts.get- Get contextusers.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 memorymemories.update- Update memory (creates version)memories.delete- Delete memoryconversations.create- Create conversationconversations.addMessage- Append messageimmutable.store- Store versioned recordmutable.set- Set mutable valuemutable.update- Atomic updatecontexts.create- Create contextcontexts.update- Update context statususers.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-embeddingsa2a.publishNotification- Pub/sub integrationusers.cascadeDelete- Cloud Mode GDPR cascadegovernance.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
- Vector Embeddings - Embedding strategy and dimensions
- Search Strategy - Multi-strategy search implementation
- Performance - Optimization techniques
- Security & Privacy - Data protection
Questions? Ask in GitHub Discussions or Discord.