// We need to migrate our database schema to support the domain as we understand it now // To do so requires adding a bunch of new tables, but also modiftying old ones and how // they relate to each other. This is a large change, so instead of doing in in one go, // We have created the target schema, and will migrate to it incrementally. datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-py" recursive_type_depth = 5 interface = "asyncio" } // User model to mirror Auth provider users model User { id String @id @db.Uuid // This should match the Supabase user ID email String @unique name String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt metadata String @default("") // Relations Agents Agent[] AgentExecutions AgentExecution[] AgentExecutionSchedules AgentExecutionSchedule[] AnalyticsDetails AnalyticsDetails[] AnalyticsMetrics AnalyticsMetrics[] UserBlockCredit UserBlockCredit[] AgentPresets AgentPreset[] UserAgents UserAgent[] // User Group relations UserGroupMemberships UserGroupMembership[] Profile Profile[] StoreListing StoreListing[] StoreListingSubmission StoreListingSubmission[] StoreListingReview StoreListingReview[] } model UserGroup { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt name String description String groupIconUrl String? UserGroupMemberships UserGroupMembership[] Agents Agent[] Profile Profile[] StoreListing StoreListing[] @@index([name]) } enum UserGroupRole { MEMBER OWNER } model UserGroupMembership { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt userId String @db.Uuid User User @relation(fields: [userId], references: [id], onDelete: Cascade) userGroupId String @db.Uuid UserGroup UserGroup @relation(fields: [userGroupId], references: [id], onDelete: Cascade) Role UserGroupRole @default(MEMBER) @@unique([userId, userGroupId]) @@index([userId]) @@index([userGroupId]) } // This model describes the Agent Graph/Flow (Multi Agent System). model Agent { id String @default(uuid()) @db.Uuid version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt name String? description String? // Link to User model createdByUserId String? @db.Uuid // Do not cascade delete the agent when the user is deleted // This allows us to delete user data with deleting the agent which maybe in use by other users CreatedByUser User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull) groupId String? @db.Uuid // Do not cascade delete the agent when the group is deleted // This allows us to delete user group data with deleting the agent which maybe in use by other users Group UserGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull) AgentNodes AgentNode[] AgentExecution AgentExecution[] // All sub-graphs are defined within this 1-level depth list (even if it's a nested graph). SubAgents Agent[] @relation("SubAgents") agentParentId String? @db.Uuid agentParentVersion Int? AgentParent Agent? @relation("SubAgents", fields: [agentParentId, agentParentVersion], references: [id, version]) AgentPresets AgentPreset[] WebhookTrigger WebhookTrigger[] AgentExecutionSchedule AgentExecutionSchedule[] UserAgents UserAgent[] UserBlockCredit UserBlockCredit[] StoreListing StoreListing[] StoreListingVersion StoreListingVersion[] @@id(name: "agentVersionId", [id, version]) } //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// //////////////// USER SPECIFIC DATA //////////////////// //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// // An AgentPrest is an Agent + User Configuration of that agent. // For example, if someone has created a weather agent and they want to set it up to // Inform them of extreme weather warnings in Texas, the agent with the configuration to set it to // monitor texas, along with the cron setup or webhook tiggers, is an AgentPreset model AgentPreset { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt name String description String // For agents that can be triggered by webhooks or cronjob // This bool allows us to disable a configured agent without deleting it isActive Boolean @default(true) userId String @db.Uuid User User @relation(fields: [userId], references: [id], onDelete: Cascade) agentId String @db.Uuid agentVersion Int Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade) InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData") UserAgents UserAgent[] WebhookTrigger WebhookTrigger[] AgentExecutionSchedule AgentExecutionSchedule[] AgentExecution AgentExecution[] @@index([userId]) } // For the library page // It is a user controlled list of agents, that they will see in there library model UserAgent { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt userId String @db.Uuid User User @relation(fields: [userId], references: [id], onDelete: Cascade) agentId String @db.Uuid agentVersion Int Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version]) agentPresetId String? @db.Uuid AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id]) isFavorite Boolean @default(false) isCreatedByUser Boolean @default(false) isArchived Boolean @default(false) isDeleted Boolean @default(false) @@index([userId]) } //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// //////// AGENT DEFINITION AND EXECUTION TABLES //////// //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// // This model describes a single node in the Agent Graph/Flow (Multi Agent System). model AgentNode { id String @id @default(uuid()) @db.Uuid agentBlockId String @db.Uuid AgentBlock AgentBlock @relation(fields: [agentBlockId], references: [id], onUpdate: Cascade) agentId String @db.Uuid agentVersion Int @default(1) Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade) // List of consumed input, that the parent node should provide. Input AgentNodeLink[] @relation("AgentNodeSink") // List of produced output, that the child node should be executed. Output AgentNodeLink[] @relation("AgentNodeSource") // JSON serialized dict[str, str] containing predefined input values. constantInput Json @default("{}") // JSON serialized dict[str, str] containing the node metadata. metadata Json @default("{}") ExecutionHistory AgentNodeExecution[] } // This model describes the link between two AgentNodes. model AgentNodeLink { id String @id @default(uuid()) @db.Uuid // Output of a node is connected to the source of the link. agentNodeSourceId String @db.Uuid AgentNodeSource AgentNode @relation("AgentNodeSource", fields: [agentNodeSourceId], references: [id], onDelete: Cascade) sourceName String // Input of a node is connected to the sink of the link. agentNodeSinkId String @db.Uuid AgentNodeSink AgentNode @relation("AgentNodeSink", fields: [agentNodeSinkId], references: [id], onDelete: Cascade) sinkName String // Default: the data coming from the source can only be consumed by the sink once, Static: input data will be reused. isStatic Boolean @default(false) } // This model describes a component that will be executed by the AgentNode. model AgentBlock { id String @id @default(uuid()) @db.Uuid name String @unique // We allow a block to have multiple types of input & output. // Serialized object-typed `jsonschema` with top-level properties as input/output name. inputSchema Json @default("{}") outputSchema Json @default("{}") // Prisma requires explicit back-references. ReferencedByAgentNode AgentNode[] UserBlockCredit UserBlockCredit[] } // This model describes the status of an AgentExecution or AgentNodeExecution. enum AgentExecutionStatus { INCOMPLETE QUEUED RUNNING COMPLETED FAILED } // Enum for execution trigger types enum ExecutionTriggerType { MANUAL SCHEDULE WEBHOOK } // This model describes the execution of an AgentGraph. model AgentExecution { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt startedAt DateTime? executionTriggerType ExecutionTriggerType @default(MANUAL) executionStatus AgentExecutionStatus @default(COMPLETED) agentId String @db.Uuid agentVersion Int @default(1) Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade) // we need to be able to associate an agent execution with an agent preset agentPresetId String? @db.Uuid AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id]) AgentNodeExecutions AgentNodeExecution[] // This is so we can track which user executed the agent. executedByUserId String @db.Uuid ExecutedByUser User @relation(fields: [executedByUserId], references: [id], onDelete: Cascade) stats Json @default("{}") // JSON serialized object } // This model describes the execution of an AgentNode. model AgentNodeExecution { id String @id @default(uuid()) @db.Uuid agentExecutionId String @db.Uuid AgentExecution AgentExecution @relation(fields: [agentExecutionId], references: [id], onDelete: Cascade) agentNodeId String @db.Uuid AgentNode AgentNode @relation(fields: [agentNodeId], references: [id], onDelete: Cascade) Input AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionInput") Output AgentNodeExecutionInputOutput[] @relation("AgentNodeExecutionOutput") executionStatus AgentExecutionStatus @default(COMPLETED) // Final JSON serialized input data for the node execution. executionData String? addedTime DateTime @default(now()) queuedTime DateTime? startedTime DateTime? endedTime DateTime? stats Json @default("{}") // JSON serialized object UserBlockCredit UserBlockCredit[] } // This model describes the output of an AgentNodeExecution. model AgentNodeExecutionInputOutput { id String @id @default(uuid()) @db.Uuid name String data String time DateTime @default(now()) // Prisma requires explicit back-references. referencedByInputExecId String? @db.Uuid ReferencedByInputExec AgentNodeExecution? @relation("AgentNodeExecutionInput", fields: [referencedByInputExecId], references: [id], onDelete: Cascade) referencedByOutputExecId String? @db.Uuid ReferencedByOutputExec AgentNodeExecution? @relation("AgentNodeExecutionOutput", fields: [referencedByOutputExecId], references: [id], onDelete: Cascade) agentPresetId String? @db.Uuid AgentPreset AgentPreset? @relation("AgentPresetsInputData", fields: [agentPresetId], references: [id]) // Input and Output pin names are unique for each AgentNodeExecution. @@unique([referencedByInputExecId, referencedByOutputExecId, name]) } // This model describes the recurring execution schedule of an Agent. model AgentExecutionSchedule { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt agentPresetId String @db.Uuid AgentPreset AgentPreset @relation(fields: [agentPresetId], references: [id], onDelete: Cascade) schedule String // cron expression isEnabled Boolean @default(true) // Allows triggers to be routed down different execution paths in an agent graph triggerIdentifier String // default and set the value on each update, lastUpdated field has no time zone. lastUpdated DateTime @default(now()) @updatedAt // Link to User model userId String @db.Uuid User User @relation(fields: [userId], references: [id], onDelete: Cascade) Agent Agent? @relation(fields: [agentId, agentVersion], references: [id, version]) agentId String? @db.Uuid agentVersion Int? @@index([isEnabled]) } enum HttpMethod { GET POST PUT DELETE PATCH } model WebhookTrigger { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt agentPresetId String @db.Uuid AgentPreset AgentPreset @relation(fields: [agentPresetId], references: [id]) method HttpMethod urlSlug String // Allows triggers to be routed down different execution paths in an agent graph triggerIdentifier String isActive Boolean @default(true) lastReceivedDataAt DateTime? isDeleted Boolean @default(false) Agent Agent? @relation(fields: [agentId, agentVersion], references: [id, version]) agentId String? @db.Uuid agentVersion Int? @@index([agentPresetId]) } //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// ////////////// METRICS TRACKING TABLES //////////////// //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// model AnalyticsDetails { // PK uses gen_random_uuid() to allow the db inserts to happen outside of prisma // typical uuid() inserts are handled by prisma id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt // Link to User model userId String @db.Uuid User User @relation(fields: [userId], references: [id], onDelete: Cascade) // Analytics Categorical data used for filtering (indexable w and w/o userId) type String // Analytic Specific Data. We should use a union type here, but prisma doesn't support it. data Json @default("{}") // Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc. dataIndex String? @@index([userId, type], name: "analyticsDetails") @@index([type]) } model AnalyticsMetrics { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt // Analytics Categorical data used for filtering (indexable w and w/o userId) analyticMetric String // Any numeric data that should be counted upon, summed, or otherwise aggregated. value Float // Any string data that should be used to identify the metric as distinct. // ex: '/build' vs '/market' dataString String? // Link to User model userId String @db.Uuid User User @relation(fields: [userId], references: [id], onDelete: Cascade) } //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// //////// ACCOUNTING AND CREDIT SYSTEM TABLES ////////// //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// enum UserBlockCreditType { TOP_UP USAGE } model UserBlockCredit { transactionKey String @default(uuid()) createdAt DateTime @default(now()) userId String @db.Uuid User User @relation(fields: [userId], references: [id], onDelete: Cascade) blockId String? @db.Uuid Block AgentBlock? @relation(fields: [blockId], references: [id]) // We need to be able to associate a credit transaction with an agent executedAgentId String? @db.Uuid executedAgentVersion Int? ExecutedAgent Agent? @relation(fields: [executedAgentId, executedAgentVersion], references: [id, version]) // We need to be able to associate a cost with a specific agent execution agentNodeExecutionId String? @db.Uuid AgentNodeExecution AgentNodeExecution? @relation(fields: [agentNodeExecutionId], references: [id]) amount Int type UserBlockCreditType isActive Boolean @default(true) metadata Json @default("{}") @@id(name: "creditTransactionIdentifier", [transactionKey, userId]) } //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// ////////////// Store TABLES /////////////////////////// //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// model Profile { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt // Only 1 of user or group can be set. // The user this profile belongs to, if any. userId String? @db.Uuid User User? @relation(fields: [userId], references: [id], onDelete: Cascade) // The group this profile belongs to, if any. groupId String? @db.Uuid Group UserGroup? @relation(fields: [groupId], references: [id]) username String @unique description String links String[] avatarUrl String? @@index([username]) } model StoreListing { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt isDeleted Boolean @default(false) // Not needed but makes lookups faster isApproved Boolean @default(false) // The agent link here is only so we can do lookup on agentId, for the listing the StoreListingVersion is used. agentId String @db.Uuid agentVersion Int Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade) owningUserId String @db.Uuid OwningUser User @relation(fields: [owningUserId], references: [id]) isGroupListing Boolean @default(false) owningGroupId String? @db.Uuid OwningGroup UserGroup? @relation(fields: [owningGroupId], references: [id]) StoreListingVersions StoreListingVersion[] StoreListingSubmission StoreListingSubmission[] @@index([isApproved]) @@index([agentId]) @@index([owningUserId]) @@index([owningGroupId]) } model StoreListingVersion { id String @id @default(uuid()) @db.Uuid version Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt // The agent and version to be listed on the store agentId String @db.Uuid agentVersion Int Agent Agent @relation(fields: [agentId, agentVersion], references: [id, version]) // The detials for this version of the agent, this allows the author to update the details of the agent, // But still allow using old versions of the agent with there original details. // TODO: Create a database view that shows only the latest version of each store listing. slug String name String videoUrl String? imageUrls String[] description String categories String[] isFeatured Boolean @default(false) isDeleted Boolean @default(false) // Old versions can be made unavailable by the author if desired isAvailable Boolean @default(true) // Not needed but makes lookups faster isApproved Boolean @default(false) StoreListing StoreListing? @relation(fields: [storeListingId], references: [id], onDelete: Cascade) storeListingId String? @db.Uuid StoreListingSubmission StoreListingSubmission[] // Reviews are on a specific version, but then aggregated up to the listing. // This allows us to provide a review filter to current version of the agent. StoreListingReview StoreListingReview[] @@unique([agentId, agentVersion]) @@index([agentId, agentVersion, isApproved]) } model StoreListingReview { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt storeListingVersionId String @db.Uuid StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade) reviewByUserId String @db.Uuid ReviewByUser User @relation(fields: [reviewByUserId], references: [id]) score Int comments String? } enum SubmissionStatus { DAFT PENDING APPROVED REJECTED } model StoreListingSubmission { id String @id @default(uuid()) @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt storeListingId String @db.Uuid StoreListing StoreListing @relation(fields: [storeListingId], references: [id], onDelete: Cascade) storeListingVersionId String @db.Uuid StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade) reviewerId String @db.Uuid Reviewer User @relation(fields: [reviewerId], references: [id]) Status SubmissionStatus @default(PENDING) reviewComments String? @@index([storeListingId]) @@index([Status]) }