import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; import { authTables } from "@convex-dev/auth/server"; const schema = defineSchema({ ...authTables, projects: defineTable({ userId: v.id("users"), name: v.string(), isDefault: v.boolean(), dorkingConfig: v.object({ selectedSourceIds: v.array(v.id("dataSources")), }), }).index("by_owner", ["userId"]), dataSources: defineTable({ projectId: v.id("projects"), type: v.literal("website"), url: v.string(), name: v.string(), analysisStatus: v.union( v.literal("pending"), v.literal("completed"), v.literal("failed") ), lastAnalyzedAt: v.optional(v.number()), lastError: v.optional(v.string()), analysisResults: v.optional( v.object({ features: v.array(v.string()), painPoints: v.array(v.string()), keywords: v.array(v.string()), summary: v.string(), }) ), metadata: v.optional(v.any()), }).index("by_project_url", ["projectId", "url"]), analyses: defineTable({ projectId: v.id("projects"), dataSourceId: v.id("dataSources"), createdAt: v.number(), analysisVersion: v.string(), productName: v.string(), tagline: v.string(), description: v.string(), category: v.string(), positioning: v.string(), features: v.array(v.object({ name: v.string(), description: v.string(), benefits: v.array(v.string()), useCases: v.array(v.string()), })), problemsSolved: v.array(v.object({ problem: v.string(), severity: v.union(v.literal("high"), v.literal("medium"), v.literal("low")), currentWorkarounds: v.array(v.string()), emotionalImpact: v.string(), searchTerms: v.array(v.string()), })), personas: v.array(v.object({ name: v.string(), role: v.string(), companySize: v.string(), industry: v.string(), painPoints: v.array(v.string()), goals: v.array(v.string()), techSavvy: v.union(v.literal("low"), v.literal("medium"), v.literal("high")), objections: v.array(v.string()), searchBehavior: v.array(v.string()), })), keywords: v.array(v.object({ term: v.string(), type: v.union( v.literal("product"), v.literal("problem"), v.literal("solution"), v.literal("competitor"), v.literal("feature"), v.literal("longtail"), v.literal("differentiator") ), searchVolume: v.union(v.literal("high"), v.literal("medium"), v.literal("low")), intent: v.union(v.literal("informational"), v.literal("navigational"), v.literal("transactional")), funnel: v.union(v.literal("awareness"), v.literal("consideration"), v.literal("decision")), emotionalIntensity: v.union(v.literal("frustrated"), v.literal("curious"), v.literal("ready")), })), useCases: v.array(v.object({ scenario: v.string(), trigger: v.string(), emotionalState: v.string(), currentWorkflow: v.array(v.string()), desiredOutcome: v.string(), alternativeProducts: v.array(v.string()), whyThisProduct: v.string(), churnRisk: v.array(v.string()), })), competitors: v.array(v.object({ name: v.string(), differentiator: v.string(), theirStrength: v.string(), switchTrigger: v.string(), theirWeakness: v.string(), })), dorkQueries: v.array(v.object({ query: v.string(), platform: v.union( v.literal("reddit"), v.literal("hackernews"), v.literal("indiehackers"), v.literal("twitter"), v.literal("quora"), v.literal("stackoverflow") ), intent: v.union( v.literal("looking-for"), v.literal("frustrated"), v.literal("alternative"), v.literal("comparison"), v.literal("problem-solving"), v.literal("tutorial") ), priority: v.union(v.literal("high"), v.literal("medium"), v.literal("low")), })), scrapedAt: v.string(), }) .index("by_project_createdAt", ["projectId", "createdAt"]) .index("by_dataSource_createdAt", ["dataSourceId", "createdAt"]), analysisSections: defineTable({ analysisId: v.id("analyses"), sectionKey: v.string(), items: v.any(), lastPrompt: v.optional(v.string()), source: v.union(v.literal("ai"), v.literal("manual"), v.literal("mixed")), updatedAt: v.number(), }) .index("by_analysis", ["analysisId"]) .index("by_analysis_section", ["analysisId", "sectionKey"]), opportunities: defineTable({ projectId: v.id("projects"), analysisId: v.optional(v.id("analyses")), url: v.string(), platform: v.string(), title: v.string(), snippet: v.string(), relevanceScore: v.number(), intent: v.string(), status: v.string(), suggestedApproach: v.string(), matchedKeywords: v.array(v.string()), matchedProblems: v.array(v.string()), tags: v.optional(v.array(v.string())), notes: v.optional(v.string()), softPitch: v.boolean(), createdAt: v.number(), updatedAt: v.number(), }) .index("by_project_status", ["projectId", "status"]) .index("by_project_createdAt", ["projectId", "createdAt"]) .index("by_project_url", ["projectId", "url"]), seenUrls: defineTable({ projectId: v.id("projects"), url: v.string(), firstSeenAt: v.number(), lastSeenAt: v.number(), source: v.optional(v.string()), }) .index("by_project_url", ["projectId", "url"]) .index("by_project_lastSeen", ["projectId", "lastSeenAt"]), analysisJobs: defineTable({ projectId: v.id("projects"), dataSourceId: v.optional(v.id("dataSources")), status: v.union( v.literal("pending"), v.literal("running"), v.literal("completed"), v.literal("failed") ), progress: v.optional(v.number()), stage: v.optional(v.string()), timeline: v.optional(v.array(v.object({ key: v.string(), label: v.string(), status: v.union( v.literal("pending"), v.literal("running"), v.literal("completed"), v.literal("failed") ), detail: v.optional(v.string()), }))), error: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_project_status", ["projectId", "status"]) .index("by_project_createdAt", ["projectId", "createdAt"]), searchJobs: defineTable({ projectId: v.id("projects"), status: v.union( v.literal("pending"), v.literal("running"), v.literal("completed"), v.literal("failed") ), config: v.optional(v.any()), progress: v.optional(v.number()), error: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_project_status", ["projectId", "status"]) .index("by_project_createdAt", ["projectId", "createdAt"]), }); export default schema;