Files
patchy631--ai-engineering-hub/open-agent-builder/convex/schema.ts
T
2026-07-13 12:37:47 +08:00

208 lines
6.4 KiB
TypeScript

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
/**
* Convex Schema for Open Agent Builder
*
* Replaces Upstash Redis for workflow storage
*/
export default defineSchema({
// Users table - synced from Clerk
users: defineTable({
clerkId: v.string(),
email: v.optional(v.string()),
name: v.optional(v.string()),
imageUrl: v.optional(v.string()),
createdAt: v.string(),
})
.index("by_clerkId", ["clerkId"])
.index("by_email", ["email"]),
// Workflows table - stores complete workflow definitions
workflows: defineTable({
// User ownership
userId: v.optional(v.string()), // Clerk user ID - optional for backward compat
// Workflow identification
customId: v.optional(v.string()), // Original workflow ID (like "workflow_123" or "amazon-product-research")
name: v.string(),
description: v.optional(v.string()),
category: v.optional(v.string()),
tags: v.optional(v.array(v.string())),
difficulty: v.optional(v.string()),
estimatedTime: v.optional(v.string()),
// Workflow structure
nodes: v.array(v.any()), // Workflow nodes with flexible structure
edges: v.array(v.any()), // Workflow edges
// Timestamps
createdAt: v.string(),
updatedAt: v.string(),
// Optional metadata
version: v.optional(v.string()),
isTemplate: v.optional(v.boolean()),
isPublic: v.optional(v.boolean()), // For shared templates
})
.index("by_userId", ["userId"])
.index("by_customId", ["customId"])
.index("by_creation", ["createdAt"])
.index("by_category", ["category"])
.index("by_template", ["isTemplate"]),
// Workflow executions - track execution state
executions: defineTable({
workflowId: v.id("workflows"),
status: v.string(), // "running" | "completed" | "failed"
// Execution state
currentNodeId: v.optional(v.string()),
nodeResults: v.any(), // Flexible execution results
variables: v.any(), // State variables
// Input/Output
input: v.optional(v.any()),
output: v.optional(v.any()),
error: v.optional(v.string()),
// Timestamps
startedAt: v.string(),
completedAt: v.optional(v.string()),
// Execution metadata
threadId: v.optional(v.string()),
})
.index("by_workflow", ["workflowId"])
.index("by_status", ["status"])
.index("by_started", ["startedAt"]),
// MCP servers registry - Centralized configuration
mcpServers: defineTable({
// Ownership
userId: v.string(), // Clerk user ID who owns this MCP
// Basic info
name: v.string(),
url: v.string(),
description: v.optional(v.string()),
category: v.string(), // "web" | "ai" | "data" | "custom"
// Authentication
authType: v.string(), // "none" | "api-key" | "bearer" | "oauth-coming-soon"
accessToken: v.optional(v.string()), // Encrypted token
// Tools & Status
tools: v.optional(v.array(v.string())), // List of available tool names
connectionStatus: v.string(), // "connected" | "error" | "untested"
lastTested: v.optional(v.string()),
lastError: v.optional(v.string()),
// Configuration
enabled: v.boolean(),
isOfficial: v.boolean(), // Built-in MCPs from registry
// Headers for custom config
headers: v.optional(v.any()),
createdAt: v.string(),
updatedAt: v.string(),
})
.index("by_userId", ["userId"])
.index("by_name", ["name"])
.index("by_enabled", ["enabled"])
.index("by_category", ["category"])
.index("by_official", ["isOfficial"]),
// Arcade auth records
arcadeAuth: defineTable({
authId: v.string(),
toolName: v.string(),
authUrl: v.optional(v.string()),
userId: v.optional(v.string()),
status: v.string(), // "pending" | "completed" | "failed"
createdAt: v.string(),
completedAt: v.optional(v.string()),
})
.index("by_authId", ["authId"])
.index("by_status", ["status"]),
// User MCP Servers - Cursor-style configuration
userMCPs: defineTable({
userId: v.string(),
name: v.string(),
url: v.string(),
headers: v.optional(v.any()),
env: v.optional(v.any()),
createdAt: v.string(),
updatedAt: v.string(),
})
.index("by_userId", ["userId"])
.index("by_name", ["name"]),
// API Keys - User-generated keys for API access
apiKeys: defineTable({
key: v.string(), // Hashed key
keyPrefix: v.string(), // "sk_live_abc..." for display
userId: v.string(), // Clerk user ID who owns this key
name: v.string(), // User-given name
usageCount: v.number(),
lastUsedAt: v.optional(v.string()),
createdAt: v.string(),
expiresAt: v.optional(v.string()),
revokedAt: v.optional(v.string()),
})
.index("by_userId", ["userId"])
.index("by_key", ["key"])
.index("by_keyPrefix", ["keyPrefix"]),
// User LLM API Keys - Store user's own LLM provider keys
userLLMKeys: defineTable({
userId: v.string(), // Clerk user ID
provider: v.string(), // "anthropic" | "openai" | "groq"
encryptedKey: v.string(), // Encrypted API key
keyPrefix: v.string(), // First/last few chars for display (e.g. "sk-ant...abc")
// Metadata
label: v.optional(v.string()), // User-friendly label
isActive: v.boolean(), // Whether this key is currently active
// Timestamps
createdAt: v.string(),
updatedAt: v.string(),
lastUsedAt: v.optional(v.string()),
// Usage tracking (optional)
usageCount: v.optional(v.number()),
})
.index("by_userId", ["userId"])
.index("by_provider", ["provider"])
.index("by_userProvider", ["userId", "provider"])
.index("by_active", ["isActive"]),
// Approval records - Human-in-the-loop workflow pauses
approvals: defineTable({
approvalId: v.string(),
workflowId: v.id("workflows"),
executionId: v.optional(v.string()), // Which execution is waiting
nodeId: v.optional(v.string()), // Which node is waiting for approval
message: v.string(),
status: v.string(), // "pending" | "approved" | "rejected"
userId: v.optional(v.string()), // Who needs to approve (Clerk user ID)
createdBy: v.optional(v.string()), // Who created the approval request
createdAt: v.string(),
respondedAt: v.optional(v.string()),
respondedBy: v.optional(v.string()), // Who responded (Clerk user ID)
})
.index("by_approvalId", ["approvalId"])
.index("by_status", ["status"])
.index("by_userId", ["userId"])
.index("by_workflow", ["workflowId"])
.index("by_execution", ["executionId"]),
});