import { authTables } from '@convex-dev/auth/server'; import { defineSchema, defineTable } from 'convex/server'; import { v } from 'convex/values'; const applicationTables = { /* * Below is the users table definition from authTables * You can add additional fields here. You can also remove * the users table here & create a 'profiles' table if you * prefer to keep auth data separate from application data. */ users: defineTable({ name: v.optional(v.string()), image: v.optional(v.string()), email: v.optional(v.string()), emailVerificationTime: v.optional(v.number()), phone: v.optional(v.string()), phoneVerificationTime: v.optional(v.number()), isAnonymous: v.optional(v.boolean()), /* Fields below here are custom & not defined in authTables */ isAdmin: v.optional(v.boolean()), role: v.optional(v.union(v.literal('owner'), v.literal('member'))), lastSeenAt: v.optional(v.number()), themePreference: v.optional( v.union(v.literal('light'), v.literal('dark'), v.literal('system')), ), }) .index('email', ['email']) .index('phone', ['phone']) /* Indexes below here are custom & not defined in authTables */ .index('name', ['name']), gitConnections: defineTable({ userId: v.id('users'), provider: v.union( v.literal('github'), v.literal('gitea'), v.literal('gitlab'), v.literal('other'), ), providerAccountId: v.optional(v.string()), displayName: v.string(), username: v.optional(v.string()), avatarUrl: v.optional(v.string()), installationId: v.optional(v.string()), scopes: v.optional(v.array(v.string())), status: v.union( v.literal('active'), v.literal('needs_reauth'), v.literal('revoked'), ), connectedAt: v.number(), updatedAt: v.number(), }) .index('by_user', ['userId']) .index('by_user_provider', ['userId', 'provider']) .index('by_status', ['status']), spoons: defineTable({ ownerId: v.id('users'), name: v.string(), description: v.optional(v.string()), provider: v.union( v.literal('github'), v.literal('gitea'), v.literal('gitlab'), v.literal('other'), ), upstreamOwner: v.string(), upstreamRepo: v.string(), upstreamDefaultBranch: v.string(), upstreamUrl: v.string(), forkOwner: v.optional(v.string()), forkRepo: v.optional(v.string()), forkDefaultBranch: v.optional(v.string()), forkUrl: v.optional(v.string()), visibility: v.union( v.literal('public'), v.literal('private'), v.literal('internal'), v.literal('unknown'), ), maintenanceMode: v.union( v.literal('watch'), v.literal('auto_pr'), v.literal('paused'), ), syncCadence: v.union( v.literal('daily'), v.literal('weekly'), v.literal('manual'), ), productionRefStrategy: v.union( v.literal('default_branch'), v.literal('latest_release'), v.literal('tag_pattern'), ), tagPattern: v.optional(v.string()), status: v.union( v.literal('draft'), v.literal('active'), v.literal('needs_connection'), v.literal('paused'), v.literal('archived'), ), lastCheckedAt: v.optional(v.number()), lastUpstreamCommit: v.optional(v.string()), lastForkCommit: v.optional(v.string()), connectionId: v.optional(v.id('gitConnections')), githubInstallationId: v.optional(v.string()), githubRepositoryId: v.optional(v.number()), upstreamRepositoryId: v.optional(v.number()), syncStatus: v.optional( v.union( v.literal('unknown'), v.literal('up_to_date'), v.literal('behind'), v.literal('ahead'), v.literal('diverged'), v.literal('checking'), v.literal('conflict'), v.literal('error'), ), ), upstreamAheadBy: v.optional(v.number()), forkAheadBy: v.optional(v.number()), lastMergeBaseCommit: v.optional(v.string()), lastSyncRunId: v.optional(v.id('syncRuns')), lastAiReviewId: v.optional(v.id('aiReviews')), lastGithubRefreshAt: v.optional(v.number()), lastSuccessfulRefreshAt: v.optional(v.number()), lastError: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_owner', ['ownerId']) .index('by_owner_status', ['ownerId', 'status']) .index('by_owner_provider', ['ownerId', 'provider']) .index('by_upstream', ['provider', 'upstreamOwner', 'upstreamRepo']), spoonRepositoryStates: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), upstreamFullName: v.string(), forkFullName: v.string(), upstreamDefaultBranch: v.string(), forkDefaultBranch: v.string(), upstreamHeadSha: v.optional(v.string()), forkHeadSha: v.optional(v.string()), mergeBaseSha: v.optional(v.string()), upstreamAheadBy: v.number(), forkAheadBy: v.number(), status: v.union( v.literal('up_to_date'), v.literal('behind'), v.literal('ahead'), v.literal('diverged'), v.literal('unknown'), ), openForkPullRequestCount: v.number(), openUpstreamPullRequestCount: v.number(), lastCommitAt: v.optional(v.number()), rawCompareUrl: v.optional(v.string()), refreshedAt: v.number(), createdAt: v.number(), updatedAt: v.number(), }) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']) .index('by_status', ['ownerId', 'status']), spoonCommits: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), sha: v.string(), side: v.union(v.literal('upstream'), v.literal('fork')), message: v.string(), authorName: v.optional(v.string()), authorEmail: v.optional(v.string()), authorLogin: v.optional(v.string()), committedAt: v.optional(v.number()), htmlUrl: v.optional(v.string()), filesChanged: v.optional(v.number()), additions: v.optional(v.number()), deletions: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_spoon_side', ['spoonId', 'side']) .index('by_owner', ['ownerId']) .index('by_sha', ['spoonId', 'sha']) .index('by_committed', ['spoonId', 'committedAt']), spoonPullRequests: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), githubId: v.number(), number: v.number(), repoFullName: v.string(), scope: v.union( v.literal('fork'), v.literal('upstream'), v.literal('from_fork_to_upstream'), ), title: v.string(), state: v.union(v.literal('open'), v.literal('closed'), v.literal('merged')), draft: v.boolean(), authorLogin: v.optional(v.string()), baseRef: v.string(), headRef: v.string(), headRepoFullName: v.optional(v.string()), htmlUrl: v.string(), createdAtGithub: v.optional(v.number()), updatedAtGithub: v.optional(v.number()), mergedAtGithub: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_spoon', ['spoonId']) .index('by_spoon_scope', ['spoonId', 'scope']) .index('by_owner', ['ownerId']) .index('by_github_id', ['githubId']) .index('by_state', ['spoonId', 'state']), syncRuns: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), threadId: v.optional(v.id('threads')), kind: v.union( v.literal('scheduled_check'), v.literal('manual_check'), v.literal('upstream_update'), v.literal('merge_attempt'), v.literal('ai_review'), ), status: v.union( v.literal('queued'), v.literal('running'), v.literal('clean'), v.literal('conflict'), v.literal('needs_review'), v.literal('failed'), v.literal('merged'), ), upstreamFrom: v.optional(v.string()), upstreamTo: v.optional(v.string()), summary: v.optional(v.string()), aiAssessment: v.optional(v.string()), mergeRequestUrl: v.optional(v.string()), error: v.optional(v.string()), decision: v.optional( v.union( v.literal('auto_synced'), v.literal('thread_created'), v.literal('ignored'), v.literal('failed'), ), ), createdAt: v.number(), updatedAt: v.number(), }) .index('by_owner', ['ownerId']) .index('by_spoon', ['spoonId']) .index('by_owner_status', ['ownerId', 'status']) .index('by_created', ['createdAt']), aiReviews: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), syncRunId: v.optional(v.id('syncRuns')), model: v.string(), status: v.union( v.literal('queued'), v.literal('running'), v.literal('completed'), v.literal('failed'), ), reviewType: v.union( v.literal('upstream_update'), v.literal('manual_prompt'), v.literal('merge_safety'), ), inputSummary: v.string(), outputSummary: v.optional(v.string()), risk: v.union( v.literal('unknown'), v.literal('low'), v.literal('medium'), v.literal('high'), ), compatible: v.boolean(), requiresHumanReview: v.boolean(), recommendedAction: v.union( v.literal('sync'), v.literal('open_review_pr'), v.literal('manual_review'), v.literal('do_not_sync'), v.literal('unknown'), ), potentialConflicts: v.optional(v.array(v.string())), importantFiles: v.optional(v.array(v.string())), reasoningSummary: v.optional(v.string()), error: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), completedAt: v.optional(v.number()), }) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']) .index('by_status', ['ownerId', 'status']) .index('by_sync_run', ['syncRunId']) .index('by_created', ['createdAt']), spoonSettings: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), autoRefreshEnabled: v.boolean(), autoReviewEnabled: v.boolean(), autoSyncEnabled: v.boolean(), requireAiLowRiskForSync: v.boolean(), requireCleanCompareForSync: v.boolean(), ignoredFilePatterns: v.optional(v.array(v.string())), importantFilePatterns: v.optional(v.array(v.string())), createdAt: v.number(), updatedAt: v.number(), }) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']), spoonRemotes: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), label: v.string(), url: v.string(), remoteName: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']), userAiSettings: defineTable({ userId: v.id('users'), provider: v.literal('openai'), encryptedApiKey: v.optional(v.string()), apiKeyPreview: v.optional(v.string()), model: v.string(), reasoningEffort: v.union( v.literal('none'), v.literal('minimal'), v.literal('low'), v.literal('medium'), v.literal('high'), v.literal('xhigh'), ), createdAt: v.number(), updatedAt: v.number(), }) .index('by_user', ['userId']) .index('by_user_provider', ['userId', 'provider']), aiProviderProfiles: defineTable({ ownerId: v.id('users'), name: v.string(), provider: v.union( v.literal('openai'), v.literal('anthropic'), v.literal('google'), v.literal('openrouter'), v.literal('requesty'), v.literal('litellm'), v.literal('cloudflare_ai_gateway'), v.literal('custom_openai_compatible'), v.literal('opencode_openai_login'), ), authType: v.union( v.literal('api_key'), v.literal('opencode_auth_json'), v.literal('none'), ), encryptedSecret: v.optional(v.string()), secretPreview: v.optional(v.string()), baseUrl: v.optional(v.string()), defaultModel: v.string(), modelOptions: v.optional(v.array(v.string())), reasoningEffort: v.union( v.literal('none'), v.literal('minimal'), v.literal('low'), v.literal('medium'), v.literal('high'), v.literal('xhigh'), ), enabled: v.boolean(), isDefault: v.optional(v.boolean()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_owner', ['ownerId']) .index('by_owner_provider', ['ownerId', 'provider']) .index('by_owner_enabled', ['ownerId', 'enabled']), agentRequests: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), agentJobId: v.optional(v.id('agentJobs')), prompt: v.string(), requestType: v.optional( v.union( v.literal('manual_prompt'), v.literal('upstream_review'), v.literal('future_code_change'), ), ), priority: v.optional( v.union(v.literal('low'), v.literal('normal'), v.literal('high')), ), source: v.optional(v.union(v.literal('user'), v.literal('system'))), status: v.union( v.literal('draft'), v.literal('queued'), v.literal('running'), v.literal('changes_ready'), v.literal('merge_request_opened'), v.literal('failed'), v.literal('cancelled'), ), targetBranch: v.optional(v.string()), selectedSecretIds: v.optional(v.array(v.id('spoonSecrets'))), baseBranch: v.optional(v.string()), requestedBranchName: v.optional(v.string()), mergeRequestUrl: v.optional(v.string()), summary: v.optional(v.string()), error: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_owner', ['ownerId']) .index('by_spoon', ['spoonId']) .index('by_owner_status', ['ownerId', 'status']) .index('by_created', ['createdAt']), spoonSecrets: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), name: v.string(), encryptedValue: v.string(), valuePreview: v.optional(v.string()), description: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']) .index('by_name', ['spoonId', 'name']), spoonAgentSettings: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), enabled: v.boolean(), runtime: v.optional( v.union(v.literal('opencode'), v.literal('openai_direct')), ), defaultBaseBranch: v.optional(v.string()), branchPrefix: v.string(), installCommand: v.optional(v.string()), checkCommand: v.optional(v.string()), testCommand: v.optional(v.string()), agentModel: v.string(), reasoningEffort: v.union( v.literal('none'), v.literal('minimal'), v.literal('low'), v.literal('medium'), v.literal('high'), v.literal('xhigh'), ), maxJobDurationMs: v.number(), maxOutputBytes: v.number(), envFilePath: v.optional( v.union( v.literal('.env'), v.literal('.env.local'), v.literal('.env.production'), v.literal('.env.production.local'), v.literal('custom'), ), ), customEnvFilePath: v.optional(v.string()), materializeEnvFileByDefault: v.optional(v.boolean()), autoDetectCommands: v.optional(v.boolean()), allowUserFileEditing: v.optional(v.boolean()), aiProviderProfileId: v.optional(v.id('aiProviderProfiles')), createdAt: v.number(), updatedAt: v.number(), }) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']), agentJobs: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), agentRequestId: v.id('agentRequests'), threadId: v.optional(v.id('threads')), jobType: v.optional( v.union( v.literal('user_change'), v.literal('maintenance_review'), v.literal('conflict_resolution'), ), ), status: v.union( v.literal('queued'), v.literal('claimed'), v.literal('preparing'), v.literal('running'), v.literal('checks_running'), v.literal('changes_ready'), v.literal('draft_pr_opened'), v.literal('failed'), v.literal('cancelled'), v.literal('timed_out'), ), prompt: v.string(), runtime: v.optional( v.union(v.literal('openai_direct'), v.literal('opencode')), ), workspaceStatus: v.optional( v.union( v.literal('not_started'), v.literal('starting'), v.literal('active'), v.literal('idle'), v.literal('stopped'), v.literal('expired'), v.literal('failed'), ), ), baseBranch: v.string(), workBranch: v.string(), opencodeSessionId: v.optional(v.string()), containerId: v.optional(v.string()), workspaceUrl: v.optional(v.string()), workspaceExpiresAt: v.optional(v.number()), lastHeartbeatAt: v.optional(v.number()), envFilePath: v.optional(v.string()), materializeEnvFile: v.optional(v.boolean()), githubInstallationId: v.optional(v.string()), forkOwner: v.string(), forkRepo: v.string(), forkUrl: v.string(), upstreamOwner: v.string(), upstreamRepo: v.string(), selectedSecretIds: v.array(v.id('spoonSecrets')), aiProviderProfileId: v.optional(v.id('aiProviderProfiles')), model: v.string(), reasoningEffort: v.union( v.literal('none'), v.literal('minimal'), v.literal('low'), v.literal('medium'), v.literal('high'), v.literal('xhigh'), ), commitSha: v.optional(v.string()), pullRequestUrl: v.optional(v.string()), pullRequestNumber: v.optional(v.number()), summary: v.optional(v.string()), error: v.optional(v.string()), claimedBy: v.optional(v.string()), claimedAt: v.optional(v.number()), startedAt: v.optional(v.number()), completedAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_owner', ['ownerId']) .index('by_spoon', ['spoonId']) .index('by_request', ['agentRequestId']) .index('by_status', ['status']) .index('by_claim', ['status', 'createdAt']), agentJobMessages: defineTable({ jobId: v.id('agentJobs'), spoonId: v.id('spoons'), ownerId: v.id('users'), role: v.union( v.literal('user'), v.literal('assistant'), v.literal('system'), v.literal('tool'), ), content: v.string(), status: v.union( v.literal('queued'), v.literal('streaming'), v.literal('completed'), v.literal('failed'), ), metadata: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_job', ['jobId']) .index('by_owner', ['ownerId']), agentWorkspaceChanges: defineTable({ jobId: v.id('agentJobs'), spoonId: v.id('spoons'), ownerId: v.id('users'), path: v.string(), source: v.union( v.literal('user'), v.literal('agent'), v.literal('command'), ), changeType: v.union( v.literal('added'), v.literal('modified'), v.literal('deleted'), v.literal('renamed'), ), diff: v.optional(v.string()), createdAt: v.number(), }) .index('by_job', ['jobId']) .index('by_path', ['jobId', 'path']), agentJobEvents: defineTable({ jobId: v.id('agentJobs'), spoonId: v.id('spoons'), ownerId: v.id('users'), level: v.union( v.literal('debug'), v.literal('info'), v.literal('warn'), v.literal('error'), ), phase: v.union( v.literal('queued'), v.literal('clone'), v.literal('plan'), v.literal('edit'), v.literal('install'), v.literal('check'), v.literal('test'), v.literal('commit'), v.literal('push'), v.literal('pr'), v.literal('cleanup'), ), message: v.string(), metadata: v.optional(v.string()), createdAt: v.number(), }) .index('by_job', ['jobId']) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']), agentJobArtifacts: defineTable({ jobId: v.id('agentJobs'), spoonId: v.id('spoons'), ownerId: v.id('users'), kind: v.union( v.literal('plan'), v.literal('diff'), v.literal('test_output'), v.literal('summary'), v.literal('error'), v.literal('pr_body'), ), title: v.string(), content: v.string(), contentType: v.union( v.literal('text/markdown'), v.literal('text/plain'), v.literal('application/json'), v.literal('text/x-diff'), ), createdAt: v.number(), }) .index('by_job', ['jobId']) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']), threads: defineTable({ ownerId: v.id('users'), spoonId: v.optional(v.id('spoons')), title: v.string(), summary: v.optional(v.string()), source: v.union( v.literal('user_request'), v.literal('upstream_update'), v.literal('merge_conflict'), v.literal('manual_review'), v.literal('system'), ), status: v.union( v.literal('open'), v.literal('queued'), v.literal('running'), v.literal('waiting_for_user'), v.literal('changes_ready'), v.literal('draft_pr_opened'), v.literal('resolved'), v.literal('ignored'), v.literal('failed'), v.literal('cancelled'), ), priority: v.union(v.literal('low'), v.literal('normal'), v.literal('high')), upstreamFrom: v.optional(v.string()), upstreamTo: v.optional(v.string()), forkHeadAtCreation: v.optional(v.string()), mergeBaseAtCreation: v.optional(v.string()), relatedSyncRunId: v.optional(v.id('syncRuns')), relatedAgentRequestId: v.optional(v.id('agentRequests')), latestAgentJobId: v.optional(v.id('agentJobs')), maintenanceOutcome: v.optional( v.union( v.literal('auto_synced'), v.literal('sync_recommended'), v.literal('ignored'), v.literal('review_pr_recommended'), v.literal('manual_review_required'), v.literal('conflict_resolution_required'), v.literal('failed'), v.literal('unknown'), ), ), ignoredCommitShas: v.optional(v.array(v.string())), ignoredReason: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), resolvedAt: v.optional(v.number()), }) .index('by_owner', ['ownerId']) .index('by_owner_status', ['ownerId', 'status']) .index('by_spoon', ['spoonId']) .index('by_source', ['ownerId', 'source']) .index('by_created', ['createdAt']), threadMessages: defineTable({ threadId: v.id('threads'), ownerId: v.id('users'), spoonId: v.optional(v.id('spoons')), role: v.union( v.literal('user'), v.literal('assistant'), v.literal('system'), v.literal('tool'), ), content: v.string(), status: v.union( v.literal('queued'), v.literal('streaming'), v.literal('completed'), v.literal('failed'), ), metadata: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index('by_thread', ['threadId']) .index('by_owner', ['ownerId']), ignoredUpstreamChanges: defineTable({ spoonId: v.id('spoons'), ownerId: v.id('users'), upstreamFrom: v.optional(v.string()), upstreamTo: v.string(), commitShas: v.array(v.string()), reason: v.string(), decidedBy: v.union(v.literal('agent'), v.literal('user')), threadId: v.optional(v.id('threads')), createdAt: v.number(), }) .index('by_spoon', ['spoonId']) .index('by_owner', ['ownerId']) .index('by_upstream_to', ['spoonId', 'upstreamTo']), }; export default defineSchema({ ...authTables, ...applicationTables, });