Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

schema.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

schema.tsBlame2890 lines · 3 contributors
fc1817aClaude1import {
2 pgTable,
3 text,
4 timestamp,
5 uuid,
6 boolean,
7 integer,
8 uniqueIndex,
79136bbClaude9 index,
10 serial,
abfa9adClaude11 bigint,
12 jsonb,
13 customType,
fc1817aClaude14} from "drizzle-orm/pg-core";
15
abfa9adClaude16// Postgres `bytea` — drizzle-orm doesn't ship a first-class bytea column, so
17// we declare one via customType that maps to Buffer on both read and write.
18// Used by `workflow_run_cache.content` where we really do need raw bytes
19// (unlike `workflow_artifacts.content`, which stayed base64-text for v1).
20const bytea = customType<{ data: Buffer; default: false }>({
21 dataType() {
22 return "bytea";
23 },
24});
25
fc1817aClaude26export const users = pgTable("users", {
27 id: uuid("id").primaryKey().defaultRandom(),
28 username: text("username").notNull().unique(),
29 email: text("email").notNull().unique(),
30 displayName: text("display_name"),
31 passwordHash: text("password_hash").notNull(),
32 avatarUrl: text("avatar_url"),
33 bio: text("bio"),
24cf2caClaude34 // Email notification preferences (Block A8). Default on; opt-out via /settings.
35 notifyEmailOnMention: boolean("notify_email_on_mention").default(true).notNull(),
36 notifyEmailOnAssign: boolean("notify_email_on_assign").default(true).notNull(),
37 notifyEmailOnGateFail: boolean("notify_email_on_gate_fail").default(true).notNull(),
08420cdClaude38 // Block I7 — weekly digest opt-in.
39 notifyEmailDigestWeekly: boolean("notify_email_digest_weekly").default(false).notNull(),
40 lastDigestSentAt: timestamp("last_digest_sent_at"),
46d6165Claude41 // Block L1 — Sleep Mode. When enabled, the autopilot sleep-mode-digest
42 // task delivers a daily "what Claude shipped overnight" report at the
43 // user-configured UTC hour (0-23, default 9). Reuses lastDigestSentAt
44 // as the 23h cooldown anchor — the cooldown is shared with the weekly
45 // digest, so a user cannot receive both on the same day.
46 sleepModeEnabled: boolean("sleep_mode_enabled").default(false).notNull(),
47 sleepModeDigestHourUtc: integer("sleep_mode_digest_hour_utc").default(9).notNull(),
534f04aClaude48 // Block M2 — Web Push per-event preferences. Default on; opt-out via /settings.
49 notifyPushOnMention: boolean("notify_push_on_mention").default(true).notNull(),
50 notifyPushOnAssign: boolean("notify_push_on_assign").default(true).notNull(),
51 notifyPushOnReviewRequest: boolean("notify_push_on_review_request")
52 .default(true)
53 .notNull(),
54 notifyPushOnDeployFailed: boolean("notify_push_on_deploy_failed")
55 .default(true)
56 .notNull(),
a4f3e24Claude57 isAdmin: boolean("is_admin").default(false).notNull(),
c63b860Claude58 // Block P2 — set when the user clicks the verification link. Soft-gate
59 // only: registration succeeds regardless; /dashboard surfaces a banner
60 // until this is non-null.
61 emailVerifiedAt: timestamp("email_verified_at"),
62 // Block P5 — Account deletion with 30-day grace period.
63 // See drizzle/0049_account_deletion.sql.
64 deletedAt: timestamp("deleted_at"),
65 deletionScheduledFor: timestamp("deletion_scheduled_for"),
66 // Block P3 — Terms of Service / Privacy Policy acceptance audit trail.
67 // Set on /register when the user ticks the accept_terms checkbox.
68 // termsVersion bumps when Terms change; UI prompts re-acceptance later.
69 termsAcceptedAt: timestamp("terms_accepted_at"),
70 termsVersion: text("terms_version"),
cd4f63bTest User71 // Block Q3 — Anonymous playground accounts. When `isPlayground=true`, the
72 // account was minted by /play with a synthetic email + random password
73 // and is hard-deleted by the autopilot `playground-purge` task once
74 // `playgroundExpiresAt` passes. Real accounts always carry false/null.
75 // See drizzle/0052_playground_accounts.sql.
76 isPlayground: boolean("is_playground").default(false).notNull(),
77 playgroundExpiresAt: timestamp("playground_expires_at"),
fc1817aClaude78 createdAt: timestamp("created_at").defaultNow().notNull(),
79 updatedAt: timestamp("updated_at").defaultNow().notNull(),
80});
81
06d5ffeClaude82export const sessions = pgTable("sessions", {
83 id: uuid("id").primaryKey().defaultRandom(),
84 userId: uuid("user_id")
85 .notNull()
86 .references(() => users.id, { onDelete: "cascade" }),
87 token: text("token").notNull().unique(),
88 expiresAt: timestamp("expires_at").notNull(),
7298a17Claude89 // B4: true when the user has entered their password but not yet their TOTP
90 // code. softAuth/requireAuth treat such sessions as anonymous; only
91 // /login/2fa can consume them. Flips to false on successful 2FA.
92 requires2fa: boolean("requires_2fa").default(false).notNull(),
06d5ffeClaude93 createdAt: timestamp("created_at").defaultNow().notNull(),
94});
95
45e31d0Claude96// @ts-ignore — self-referential FK on forkedFromId causes circular inference
fc1817aClaude97export const repositories = pgTable(
98 "repositories",
99 {
100 id: uuid("id").primaryKey().defaultRandom(),
101 name: text("name").notNull(),
7437605Claude102 // ownerId = creator / user-owner. Always set (for attribution + user
103 // namespace uniqueness). For org-owned repos, also represents "created by".
fc1817aClaude104 ownerId: uuid("owner_id")
105 .notNull()
106 .references(() => users.id),
7437605Claude107 // Block B2: nullable org owner. When set, the repo lives in the org
108 // namespace and URL resolution routes `/:orgSlug/:repo` to it.
109 orgId: uuid("org_id"),
fc1817aClaude110 description: text("description"),
111 isPrivate: boolean("is_private").default(false).notNull(),
3ef4c9dClaude112 isArchived: boolean("is_archived").default(false).notNull(),
71cd5ecClaude113 isTemplate: boolean("is_template").default(false).notNull(),
fc1817aClaude114 defaultBranch: text("default_branch").default("main").notNull(),
115 diskPath: text("disk_path").notNull(),
45e31d0Claude116 forkedFromId: uuid("forked_from_id"),
fc1817aClaude117 createdAt: timestamp("created_at").defaultNow().notNull(),
118 updatedAt: timestamp("updated_at").defaultNow().notNull(),
119 pushedAt: timestamp("pushed_at"),
120 starCount: integer("star_count").default(0).notNull(),
121 forkCount: integer("fork_count").default(0).notNull(),
79136bbClaude122 issueCount: integer("issue_count").default(0).notNull(),
534f04aClaude123 // Block M5: autopilot stale-sweep opt-out flags. Default true — the
124 // 2-stage close (poke at 7d/30d, close at 14d/60d after poke) runs on
125 // every repo unless an owner disables it via repo-settings.
126 autoCloseStalePrs: boolean("auto_close_stale_prs").default(true).notNull(),
127 autoCloseStaleIssues: boolean("auto_close_stale_issues")
128 .default(true)
129 .notNull(),
fc1817aClaude130 },
7437605Claude131 (table) => [
132 // Partial: uniqueness only in the user namespace (org-owned rows exempt).
133 // Matches the partial index in migration 0004.
134 uniqueIndex("repos_owner_name").on(table.ownerId, table.name),
135 index("repos_org").on(table.orgId),
136 ]
fc1817aClaude137);
138
3ef4c9dClaude139/**
140 * Per-repository gate + auto-repair configuration.
141 * Every new repo is created with all gates ENABLED by default —
142 * the "full green ecosystem" default. Owners can manually opt-out per setting.
143 */
144export const repoSettings = pgTable("repo_settings", {
145 id: uuid("id").primaryKey().defaultRandom(),
146 repositoryId: uuid("repository_id")
147 .notNull()
148 .unique()
149 .references(() => repositories.id, { onDelete: "cascade" }),
150 // Gates
151 gateTestEnabled: boolean("gate_test_enabled").default(true).notNull(),
152 aiReviewEnabled: boolean("ai_review_enabled").default(true).notNull(),
153 secretScanEnabled: boolean("secret_scan_enabled").default(true).notNull(),
154 securityScanEnabled: boolean("security_scan_enabled").default(true).notNull(),
155 dependencyScanEnabled: boolean("dependency_scan_enabled").default(true).notNull(),
156 lintEnabled: boolean("lint_enabled").default(true).notNull(),
157 typeCheckEnabled: boolean("type_check_enabled").default(true).notNull(),
158 testEnabled: boolean("test_enabled").default(true).notNull(),
159 // Auto-repair
160 autoFixEnabled: boolean("auto_fix_enabled").default(true).notNull(),
161 autoMergeResolveEnabled: boolean("auto_merge_resolve_enabled").default(true).notNull(),
162 autoFormatEnabled: boolean("auto_format_enabled").default(true).notNull(),
163 // AI features
164 aiCommitMessagesEnabled: boolean("ai_commit_messages_enabled").default(true).notNull(),
165 aiPrSummaryEnabled: boolean("ai_pr_summary_enabled").default(true).notNull(),
166 aiChangelogEnabled: boolean("ai_changelog_enabled").default(true).notNull(),
167 // Deploy
168 autoDeployEnabled: boolean("auto_deploy_enabled").default(true).notNull(),
169 deployRequireAllGreen: boolean("deploy_require_all_green").default(true).notNull(),
170 createdAt: timestamp("created_at").defaultNow().notNull(),
171 updatedAt: timestamp("updated_at").defaultNow().notNull(),
172});
173
174/**
175 * Branch protection rules — enforced on push and merge.
176 * Every repo's default branch gets a protection rule on creation.
177 */
178export const branchProtection = pgTable(
179 "branch_protection",
180 {
181 id: uuid("id").primaryKey().defaultRandom(),
182 repositoryId: uuid("repository_id")
183 .notNull()
184 .references(() => repositories.id, { onDelete: "cascade" }),
185 pattern: text("pattern").notNull(), // branch name or glob (e.g. "main", "release/*")
186 requirePullRequest: boolean("require_pull_request").default(true).notNull(),
187 requireGreenGates: boolean("require_green_gates").default(true).notNull(),
188 requireAiApproval: boolean("require_ai_approval").default(true).notNull(),
189 requireHumanReview: boolean("require_human_review").default(false).notNull(),
190 requiredApprovals: integer("required_approvals").default(0).notNull(),
191 allowForcePush: boolean("allow_force_push").default(false).notNull(),
192 allowDeletion: boolean("allow_deletion").default(false).notNull(),
193 dismissStaleReviews: boolean("dismiss_stale_reviews").default(true).notNull(),
4626e61Claude194 // K2 — Auto-merge opt-in. When true, the autopilot ticker may auto-merge
195 // a PR whose base branch matches this rule, provided every gate the
196 // manual-merge path enforces is green. Default-deny.
197 enableAutoMerge: boolean("enable_auto_merge").default(false).notNull(),
3ef4c9dClaude198 createdAt: timestamp("created_at").defaultNow().notNull(),
199 updatedAt: timestamp("updated_at").defaultNow().notNull(),
200 },
201 (table) => [
202 uniqueIndex("branch_protection_repo_pattern").on(
203 table.repositoryId,
204 table.pattern
205 ),
206 ]
207);
208
209/**
210 * Gate run history. Every push + every PR creates gate_runs entries —
211 * one per configured gate. Serves as the source of truth for "is this green?".
212 */
213export const gateRuns = pgTable(
214 "gate_runs",
215 {
216 id: uuid("id").primaryKey().defaultRandom(),
217 repositoryId: uuid("repository_id")
218 .notNull()
219 .references(() => repositories.id, { onDelete: "cascade" }),
220 pullRequestId: uuid("pull_request_id").references(() => pullRequests.id, {
221 onDelete: "cascade",
222 }),
223 commitSha: text("commit_sha").notNull(),
224 ref: text("ref").notNull(),
225 gateName: text("gate_name").notNull(), // e.g. "GateTest", "AI Review", "Secret Scan", "Type Check"
226 status: text("status").notNull(), // pending, running, passed, failed, skipped, repaired
227 summary: text("summary"),
228 details: text("details"), // JSON: per-check output, affected files, etc
229 repairAttempted: boolean("repair_attempted").default(false).notNull(),
230 repairSucceeded: boolean("repair_succeeded").default(false).notNull(),
231 repairCommitSha: text("repair_commit_sha"),
232 durationMs: integer("duration_ms"),
233 createdAt: timestamp("created_at").defaultNow().notNull(),
234 completedAt: timestamp("completed_at"),
235 },
236 (table) => [
237 index("gate_runs_repo_sha").on(table.repositoryId, table.commitSha),
238 index("gate_runs_pr").on(table.pullRequestId),
239 index("gate_runs_created").on(table.createdAt),
240 ]
241);
242
243/**
244 * In-app notifications. Powered by the activity feed + explicit mentions.
245 */
246export const notifications = pgTable(
247 "notifications",
248 {
249 id: uuid("id").primaryKey().defaultRandom(),
250 userId: uuid("user_id")
251 .notNull()
252 .references(() => users.id, { onDelete: "cascade" }),
253 repositoryId: uuid("repository_id").references(() => repositories.id, {
254 onDelete: "cascade",
255 }),
256 kind: text("kind").notNull(), // mention, review_requested, pr_merged, pr_closed, gate_failed, gate_repaired, ai_review, assigned, security_alert, deploy_success, deploy_failed
257 title: text("title").notNull(),
258 body: text("body"),
259 url: text("url"), // link to the relevant page
260 readAt: timestamp("read_at"),
261 createdAt: timestamp("created_at").defaultNow().notNull(),
262 },
263 (table) => [
264 index("notifications_user_unread").on(table.userId, table.readAt),
265 index("notifications_user_created").on(table.userId, table.createdAt),
266 ]
267);
268
269/**
270 * Releases — named snapshots of a repo at a tag/commit.
271 * AI-generated changelogs bundled in notes field.
272 */
273export const releases = pgTable(
274 "releases",
275 {
276 id: uuid("id").primaryKey().defaultRandom(),
277 repositoryId: uuid("repository_id")
278 .notNull()
279 .references(() => repositories.id, { onDelete: "cascade" }),
280 authorId: uuid("author_id")
281 .notNull()
282 .references(() => users.id),
283 tag: text("tag").notNull(),
284 name: text("name").notNull(),
285 body: text("body"), // AI-generated release notes + changelog
286 targetCommit: text("target_commit").notNull(),
287 isDraft: boolean("is_draft").default(false).notNull(),
288 isPrerelease: boolean("is_prerelease").default(false).notNull(),
289 createdAt: timestamp("created_at").defaultNow().notNull(),
290 publishedAt: timestamp("published_at"),
291 },
292 (table) => [
293 uniqueIndex("releases_repo_tag").on(table.repositoryId, table.tag),
294 ]
295);
296
297/**
298 * Milestones — group issues + PRs toward a shared goal.
299 */
300export const milestones = pgTable(
301 "milestones",
302 {
303 id: uuid("id").primaryKey().defaultRandom(),
304 repositoryId: uuid("repository_id")
305 .notNull()
306 .references(() => repositories.id, { onDelete: "cascade" }),
307 title: text("title").notNull(),
308 description: text("description"),
309 state: text("state").notNull().default("open"), // open, closed
310 dueDate: timestamp("due_date"),
311 createdAt: timestamp("created_at").defaultNow().notNull(),
312 closedAt: timestamp("closed_at"),
313 },
314 (table) => [index("milestones_repo_state").on(table.repositoryId, table.state)]
315);
316
317/**
318 * Reactions on issues, PRs, and comments. Universal target pointer.
319 */
320export const reactions = pgTable(
321 "reactions",
322 {
323 id: uuid("id").primaryKey().defaultRandom(),
324 userId: uuid("user_id")
325 .notNull()
326 .references(() => users.id, { onDelete: "cascade" }),
327 targetType: text("target_type").notNull(), // issue, pr, issue_comment, pr_comment
328 targetId: uuid("target_id").notNull(),
329 emoji: text("emoji").notNull(), // thumbs_up, thumbs_down, rocket, heart, eyes, laugh, hooray, confused
330 createdAt: timestamp("created_at").defaultNow().notNull(),
331 },
332 (table) => [
333 uniqueIndex("reactions_unique").on(
334 table.userId,
335 table.targetType,
336 table.targetId,
337 table.emoji
338 ),
339 index("reactions_target").on(table.targetType, table.targetId),
340 ]
341);
342
343/**
344 * PR reviews (formal approve/request-changes).
345 * Separate from inline comments in pr_comments.
346 */
347export const prReviews = pgTable(
348 "pr_reviews",
349 {
350 id: uuid("id").primaryKey().defaultRandom(),
351 pullRequestId: uuid("pull_request_id")
352 .notNull()
353 .references(() => pullRequests.id, { onDelete: "cascade" }),
354 reviewerId: uuid("reviewer_id")
355 .notNull()
356 .references(() => users.id),
357 state: text("state").notNull(), // approved, changes_requested, commented
358 body: text("body"),
359 isAi: boolean("is_ai").default(false).notNull(),
360 createdAt: timestamp("created_at").defaultNow().notNull(),
361 },
362 (table) => [index("pr_reviews_pr").on(table.pullRequestId)]
363);
364
365/**
366 * Code owners — who owns which paths (auto-request review on PR).
367 * Parsed from a CODEOWNERS file at the root of the default branch.
368 */
369export const codeOwners = pgTable(
370 "code_owners",
371 {
372 id: uuid("id").primaryKey().defaultRandom(),
373 repositoryId: uuid("repository_id")
374 .notNull()
375 .references(() => repositories.id, { onDelete: "cascade" }),
376 pathPattern: text("path_pattern").notNull(),
377 ownerUsernames: text("owner_usernames").notNull(), // comma-separated
378 createdAt: timestamp("created_at").defaultNow().notNull(),
379 },
380 (table) => [index("code_owners_repo").on(table.repositoryId)]
381);
382
383/**
384 * Per-repo AI chat sessions — conversational repo assistant.
385 */
386export const aiChats = pgTable(
387 "ai_chats",
388 {
389 id: uuid("id").primaryKey().defaultRandom(),
390 userId: uuid("user_id")
391 .notNull()
392 .references(() => users.id, { onDelete: "cascade" }),
393 repositoryId: uuid("repository_id").references(() => repositories.id, {
394 onDelete: "cascade",
395 }),
396 title: text("title"),
397 messages: text("messages").notNull().default("[]"), // JSON array of {role, content}
398 createdAt: timestamp("created_at").defaultNow().notNull(),
399 updatedAt: timestamp("updated_at").defaultNow().notNull(),
400 },
401 (table) => [
402 index("ai_chats_user").on(table.userId),
403 index("ai_chats_repo").on(table.repositoryId),
404 ]
405);
406
407/**
408 * Audit log — every sensitive action. Who did what, when, from where.
409 */
410export const auditLog = pgTable(
411 "audit_log",
412 {
413 id: uuid("id").primaryKey().defaultRandom(),
414 userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }),
415 repositoryId: uuid("repository_id").references(() => repositories.id, {
416 onDelete: "set null",
417 }),
418 action: text("action").notNull(), // repo.create, repo.delete, repo.transfer, token.create, token.revoke, merge, force_push, branch_protection.update, deploy, ...
419 targetType: text("target_type"),
420 targetId: text("target_id"),
421 ip: text("ip"),
422 userAgent: text("user_agent"),
423 metadata: text("metadata"), // JSON for extra context
424 createdAt: timestamp("created_at").defaultNow().notNull(),
425 },
426 (table) => [
427 index("audit_log_user").on(table.userId),
428 index("audit_log_repo").on(table.repositoryId),
429 index("audit_log_created").on(table.createdAt),
430 ]
431);
432
433/**
434 * Deployments — tracks every deploy to downstream systems (Crontech, etc).
435 * Each deploy is gated on ALL green gates passing.
436 */
437export const deployments = pgTable(
438 "deployments",
439 {
440 id: uuid("id").primaryKey().defaultRandom(),
441 repositoryId: uuid("repository_id")
442 .notNull()
443 .references(() => repositories.id, { onDelete: "cascade" }),
444 environment: text("environment").notNull().default("production"),
445 commitSha: text("commit_sha").notNull(),
446 ref: text("ref").notNull(),
a76d984Claude447 status: text("status").notNull(), // pending, running, success, failed, blocked, waiting_timer
3ef4c9dClaude448 blockedReason: text("blocked_reason"),
449 target: text("target"), // e.g. "crontech", "fly.io"
450 triggeredBy: uuid("triggered_by").references(() => users.id),
a76d984Claude451 /**
452 * Set when an approved deploy is held by `environments.wait_timer_minutes`.
453 * NULL = no wait; non-null = autopilot flips status from "waiting_timer"
454 * to "pending" once `now() >= ready_after`.
455 */
456 readyAfter: timestamp("ready_after"),
3ef4c9dClaude457 createdAt: timestamp("created_at").defaultNow().notNull(),
458 completedAt: timestamp("completed_at"),
459 },
460 (table) => [
461 index("deployments_repo").on(table.repositoryId),
462 index("deployments_created").on(table.createdAt),
463 ]
464);
465
466/**
467 * Rate-limit buckets — in-memory or persisted counter per IP / token / route.
468 */
469export const rateLimitBuckets = pgTable(
470 "rate_limit_buckets",
471 {
472 id: uuid("id").primaryKey().defaultRandom(),
473 bucketKey: text("bucket_key").notNull().unique(), // "ip:1.2.3.4:api" or "token:abc:api"
474 count: integer("count").default(0).notNull(),
475 windowStart: timestamp("window_start").defaultNow().notNull(),
476 expiresAt: timestamp("expires_at").notNull(),
477 },
478 (table) => [index("rate_limit_expires").on(table.expiresAt)]
fc1817aClaude479);
480
06d5ffeClaude481export const stars = pgTable(
482 "stars",
483 {
484 id: uuid("id").primaryKey().defaultRandom(),
485 userId: uuid("user_id")
486 .notNull()
487 .references(() => users.id, { onDelete: "cascade" }),
488 repositoryId: uuid("repository_id")
489 .notNull()
490 .references(() => repositories.id, { onDelete: "cascade" }),
491 createdAt: timestamp("created_at").defaultNow().notNull(),
492 },
79136bbClaude493 (table) => [
494 uniqueIndex("stars_user_repo").on(table.userId, table.repositoryId),
495 ]
496);
497
498export const issues = pgTable(
499 "issues",
500 {
501 id: uuid("id").primaryKey().defaultRandom(),
502 number: serial("number"),
503 repositoryId: uuid("repository_id")
504 .notNull()
505 .references(() => repositories.id, { onDelete: "cascade" }),
506 authorId: uuid("author_id")
507 .notNull()
508 .references(() => users.id),
509 title: text("title").notNull(),
510 body: text("body"),
511 state: text("state").notNull().default("open"), // open, closed
512 createdAt: timestamp("created_at").defaultNow().notNull(),
513 updatedAt: timestamp("updated_at").defaultNow().notNull(),
514 closedAt: timestamp("closed_at"),
515 },
516 (table) => [
517 index("issues_repo_state").on(table.repositoryId, table.state),
518 index("issues_repo_number").on(table.repositoryId, table.number),
519 ]
520);
521
522export const issueComments = pgTable(
523 "issue_comments",
524 {
525 id: uuid("id").primaryKey().defaultRandom(),
526 issueId: uuid("issue_id")
527 .notNull()
528 .references(() => issues.id, { onDelete: "cascade" }),
529 authorId: uuid("author_id")
530 .notNull()
531 .references(() => users.id),
532 body: text("body").notNull(),
533 createdAt: timestamp("created_at").defaultNow().notNull(),
534 updatedAt: timestamp("updated_at").defaultNow().notNull(),
535 },
536 (table) => [index("comments_issue").on(table.issueId)]
537);
538
539export const labels = pgTable(
540 "labels",
541 {
542 id: uuid("id").primaryKey().defaultRandom(),
543 repositoryId: uuid("repository_id")
544 .notNull()
545 .references(() => repositories.id, { onDelete: "cascade" }),
546 name: text("name").notNull(),
547 color: text("color").notNull().default("#8b949e"),
548 description: text("description"),
549 },
550 (table) => [
551 uniqueIndex("labels_repo_name").on(table.repositoryId, table.name),
552 ]
553);
554
555export const issueLabels = pgTable(
556 "issue_labels",
557 {
558 id: uuid("id").primaryKey().defaultRandom(),
559 issueId: uuid("issue_id")
560 .notNull()
561 .references(() => issues.id, { onDelete: "cascade" }),
562 labelId: uuid("label_id")
563 .notNull()
564 .references(() => labels.id, { onDelete: "cascade" }),
565 },
566 (table) => [
567 uniqueIndex("issue_labels_unique").on(table.issueId, table.labelId),
568 ]
06d5ffeClaude569);
570
0074234Claude571export const pullRequests = pgTable(
572 "pull_requests",
573 {
574 id: uuid("id").primaryKey().defaultRandom(),
575 number: serial("number"),
576 repositoryId: uuid("repository_id")
577 .notNull()
578 .references(() => repositories.id, { onDelete: "cascade" }),
579 authorId: uuid("author_id")
580 .notNull()
581 .references(() => users.id),
582 title: text("title").notNull(),
583 body: text("body"),
584 state: text("state").notNull().default("open"), // open, closed, merged
585 baseBranch: text("base_branch").notNull(),
586 headBranch: text("head_branch").notNull(),
3ef4c9dClaude587 isDraft: boolean("is_draft").default(false).notNull(),
588 mergeStrategy: text("merge_strategy").default("merge").notNull(), // merge, squash, rebase
589 milestoneId: uuid("milestone_id"),
0074234Claude590 mergedAt: timestamp("merged_at"),
591 mergedBy: uuid("merged_by").references(() => users.id),
592 createdAt: timestamp("created_at").defaultNow().notNull(),
593 updatedAt: timestamp("updated_at").defaultNow().notNull(),
594 closedAt: timestamp("closed_at"),
595 },
596 (table) => [
597 index("prs_repo_state").on(table.repositoryId, table.state),
598 index("prs_repo_number").on(table.repositoryId, table.number),
599 ]
600);
601
602export const prComments = pgTable(
603 "pr_comments",
604 {
605 id: uuid("id").primaryKey().defaultRandom(),
606 pullRequestId: uuid("pull_request_id")
607 .notNull()
608 .references(() => pullRequests.id, { onDelete: "cascade" }),
609 authorId: uuid("author_id")
610 .notNull()
611 .references(() => users.id),
612 body: text("body").notNull(),
613 isAiReview: boolean("is_ai_review").default(false).notNull(),
614 filePath: text("file_path"),
615 lineNumber: integer("line_number"),
616 createdAt: timestamp("created_at").defaultNow().notNull(),
617 updatedAt: timestamp("updated_at").defaultNow().notNull(),
618 },
619 (table) => [index("pr_comments_pr").on(table.pullRequestId)]
620);
621
622export const activityFeed = pgTable(
623 "activity_feed",
624 {
625 id: uuid("id").primaryKey().defaultRandom(),
626 repositoryId: uuid("repository_id")
627 .notNull()
628 .references(() => repositories.id, { onDelete: "cascade" }),
629 userId: uuid("user_id").references(() => users.id),
630 action: text("action").notNull(), // push, issue_open, issue_close, pr_open, pr_merge, star, comment
631 targetType: text("target_type"), // issue, pr, commit
632 targetId: text("target_id"),
633 metadata: text("metadata"), // JSON string for extra data
634 createdAt: timestamp("created_at").defaultNow().notNull(),
635 },
636 (table) => [
637 index("activity_repo").on(table.repositoryId),
638 index("activity_user").on(table.userId),
639 ]
640);
641
c81ab7aClaude642export const webhooks = pgTable(
643 "webhooks",
644 {
645 id: uuid("id").primaryKey().defaultRandom(),
646 repositoryId: uuid("repository_id")
647 .notNull()
648 .references(() => repositories.id, { onDelete: "cascade" }),
649 url: text("url").notNull(),
650 secret: text("secret"),
651 events: text("events").notNull().default("push"), // comma-separated: push,issue,pr
652 isActive: boolean("is_active").default(true).notNull(),
653 lastDeliveredAt: timestamp("last_delivered_at"),
654 lastStatus: integer("last_status"),
655 createdAt: timestamp("created_at").defaultNow().notNull(),
656 },
657 (table) => [index("webhooks_repo").on(table.repositoryId)]
658);
659
660export const apiTokens = pgTable("api_tokens", {
661 id: uuid("id").primaryKey().defaultRandom(),
662 userId: uuid("user_id")
663 .notNull()
664 .references(() => users.id, { onDelete: "cascade" }),
665 name: text("name").notNull(),
666 tokenHash: text("token_hash").notNull(),
667 tokenPrefix: text("token_prefix").notNull(), // first 8 chars for display
668 scopes: text("scopes").notNull().default("repo"), // comma-separated
669 lastUsedAt: timestamp("last_used_at"),
670 expiresAt: timestamp("expires_at"),
671 createdAt: timestamp("created_at").defaultNow().notNull(),
672});
673
674export const repoTopics = pgTable(
675 "repo_topics",
676 {
677 id: uuid("id").primaryKey().defaultRandom(),
678 repositoryId: uuid("repository_id")
679 .notNull()
680 .references(() => repositories.id, { onDelete: "cascade" }),
681 topic: text("topic").notNull(),
682 },
683 (table) => [
684 uniqueIndex("repo_topics_unique").on(table.repositoryId, table.topic),
685 index("topics_name").on(table.topic),
686 ]
687);
688
fc1817aClaude689export const sshKeys = pgTable("ssh_keys", {
690 id: uuid("id").primaryKey().defaultRandom(),
691 userId: uuid("user_id")
692 .notNull()
06d5ffeClaude693 .references(() => users.id, { onDelete: "cascade" }),
fc1817aClaude694 title: text("title").notNull(),
695 fingerprint: text("fingerprint").notNull(),
696 publicKey: text("public_key").notNull(),
697 lastUsedAt: timestamp("last_used_at"),
698 createdAt: timestamp("created_at").defaultNow().notNull(),
699});
700
534f04aClaude701// Block M2 — Web Push subscriptions. One row per (user, endpoint).
702// Endpoint is the browser-issued push URL; p256dh + auth are the W3C
703// payload-encryption keys (base64url). Stale endpoints (HTTP 410) are
704// deleted lazily on next `sendPushToUser` pass.
705export const pushSubscriptions = pgTable(
706 "push_subscriptions",
707 {
708 id: uuid("id").primaryKey().defaultRandom(),
709 userId: uuid("user_id")
710 .notNull()
711 .references(() => users.id, { onDelete: "cascade" }),
712 endpoint: text("endpoint").notNull(),
713 p256dh: text("p256dh").notNull(),
714 auth: text("auth").notNull(),
715 userAgent: text("user_agent"),
716 createdAt: timestamp("created_at").defaultNow().notNull(),
717 lastUsedAt: timestamp("last_used_at"),
718 },
719 (table) => [
720 uniqueIndex("push_subscriptions_user_endpoint").on(
721 table.userId,
722 table.endpoint
723 ),
724 index("idx_push_subscriptions_user").on(table.userId),
725 ]
726);
727
fc1817aClaude728export type User = typeof users.$inferSelect;
729export type NewUser = typeof users.$inferInsert;
730export type Repository = typeof repositories.$inferSelect;
731export type NewRepository = typeof repositories.$inferInsert;
06d5ffeClaude732export type Session = typeof sessions.$inferSelect;
733export type Star = typeof stars.$inferSelect;
734export type SshKey = typeof sshKeys.$inferSelect;
534f04aClaude735export type PushSubscription = typeof pushSubscriptions.$inferSelect;
736export type NewPushSubscription = typeof pushSubscriptions.$inferInsert;
79136bbClaude737export type Issue = typeof issues.$inferSelect;
738export type IssueComment = typeof issueComments.$inferSelect;
739export type Label = typeof labels.$inferSelect;
0074234Claude740export type PullRequest = typeof pullRequests.$inferSelect;
741export type PrComment = typeof prComments.$inferSelect;
742export type ActivityEntry = typeof activityFeed.$inferSelect;
c81ab7aClaude743export type Webhook = typeof webhooks.$inferSelect;
744export type ApiToken = typeof apiTokens.$inferSelect;
745export type RepoTopic = typeof repoTopics.$inferSelect;
3ef4c9dClaude746export type RepoSettings = typeof repoSettings.$inferSelect;
747export type BranchProtection = typeof branchProtection.$inferSelect;
748export type GateRun = typeof gateRuns.$inferSelect;
749export type Notification = typeof notifications.$inferSelect;
750export type Release = typeof releases.$inferSelect;
751export type Milestone = typeof milestones.$inferSelect;
752export type Reaction = typeof reactions.$inferSelect;
753export type PrReview = typeof prReviews.$inferSelect;
754export type CodeOwner = typeof codeOwners.$inferSelect;
755export type AiChat = typeof aiChats.$inferSelect;
756export type AuditLogEntry = typeof auditLog.$inferSelect;
757export type Deployment = typeof deployments.$inferSelect;
24cf2caClaude758
759/**
760 * Saved replies — per-user canned responses, insertable into any
761 * issue / PR comment textarea. Shortcut name must be unique per user.
762 */
763export const savedReplies = pgTable(
764 "saved_replies",
765 {
766 id: uuid("id").primaryKey().defaultRandom(),
767 userId: uuid("user_id")
768 .notNull()
769 .references(() => users.id, { onDelete: "cascade" }),
770 shortcut: text("shortcut").notNull(),
771 body: text("body").notNull(),
772 createdAt: timestamp("created_at").defaultNow().notNull(),
773 updatedAt: timestamp("updated_at").defaultNow().notNull(),
774 },
775 (table) => [
776 uniqueIndex("saved_replies_user_shortcut").on(table.userId, table.shortcut),
777 ]
778);
779
780export type SavedReply = typeof savedReplies.$inferSelect;
5cc5d95Claude781
782/**
783 * Organizations (Block B1) — multi-user namespaces. Distinct from `users`.
784 * An org has members (with org-level roles) and may contain teams.
785 * Repos can be owned by an org via `repositories.orgId` (added in Block B2).
786 *
787 * Slug is globally unique against itself; collision with a username is
788 * checked at create time in the route handler (no DB-level cross-table
789 * uniqueness in Postgres).
790 */
791export const organizations = pgTable("organizations", {
792 id: uuid("id").primaryKey().defaultRandom(),
793 slug: text("slug").notNull().unique(),
794 name: text("name").notNull(),
795 description: text("description"),
796 avatarUrl: text("avatar_url"),
797 billingEmail: text("billing_email"),
798 createdById: uuid("created_by_id")
799 .notNull()
800 .references(() => users.id, { onDelete: "restrict" }),
801 createdAt: timestamp("created_at").defaultNow().notNull(),
802 updatedAt: timestamp("updated_at").defaultNow().notNull(),
803});
804
805/**
806 * Org membership. Roles: owner (full control, billing), admin (manage
807 * members + teams + repos), member (default; can be added to teams).
808 */
809export const orgMembers = pgTable(
810 "org_members",
811 {
812 id: uuid("id").primaryKey().defaultRandom(),
813 orgId: uuid("org_id")
814 .notNull()
815 .references(() => organizations.id, { onDelete: "cascade" }),
816 userId: uuid("user_id")
817 .notNull()
818 .references(() => users.id, { onDelete: "cascade" }),
819 role: text("role").notNull().default("member"), // owner | admin | member
820 createdAt: timestamp("created_at").defaultNow().notNull(),
821 },
822 (table) => [
823 uniqueIndex("org_members_unique").on(table.orgId, table.userId),
824 index("org_members_user").on(table.userId),
825 ]
826);
827
828/**
829 * Teams within an org. Slug is unique within an org.
830 * `parentTeamId` allows nesting (GitHub-style child teams). Optional.
831 */
832export const teams = pgTable(
833 "teams",
834 {
835 id: uuid("id").primaryKey().defaultRandom(),
836 orgId: uuid("org_id")
837 .notNull()
838 .references(() => organizations.id, { onDelete: "cascade" }),
839 slug: text("slug").notNull(),
840 name: text("name").notNull(),
841 description: text("description"),
842 parentTeamId: uuid("parent_team_id"),
843 createdAt: timestamp("created_at").defaultNow().notNull(),
844 updatedAt: timestamp("updated_at").defaultNow().notNull(),
845 },
846 (table) => [uniqueIndex("teams_org_slug").on(table.orgId, table.slug)]
847);
848
849/**
850 * Team membership. Roles: maintainer (can edit team), member (default).
851 * A user can belong to many teams; team membership requires org membership
852 * but that invariant is enforced at the route layer, not the DB layer.
853 */
854export const teamMembers = pgTable(
855 "team_members",
856 {
857 id: uuid("id").primaryKey().defaultRandom(),
858 teamId: uuid("team_id")
859 .notNull()
860 .references(() => teams.id, { onDelete: "cascade" }),
861 userId: uuid("user_id")
862 .notNull()
863 .references(() => users.id, { onDelete: "cascade" }),
864 role: text("role").notNull().default("member"), // maintainer | member
865 createdAt: timestamp("created_at").defaultNow().notNull(),
866 },
867 (table) => [
868 uniqueIndex("team_members_unique").on(table.teamId, table.userId),
869 index("team_members_user").on(table.userId),
870 ]
871);
872
873export type Organization = typeof organizations.$inferSelect;
874export type OrgMember = typeof orgMembers.$inferSelect;
875export type Team = typeof teams.$inferSelect;
876export type TeamMember = typeof teamMembers.$inferSelect;
877export type OrgRole = "owner" | "admin" | "member";
878export type TeamRole = "maintainer" | "member";
7298a17Claude879
880/**
881 * 2FA / TOTP (Block B4).
882 *
883 * Secret is stored in plain Base32 for now — the DB has row-level-secure
884 * access and the app boundary is the only code that reads it. A follow-up
885 * (B4.1) will wrap it with AES-GCM at rest once we standardise the KEK.
886 *
887 * `enabledAt` is set only after the user has successfully entered their
888 * first code (confirming the authenticator was set up correctly). Rows with
889 * `enabledAt = NULL` represent pending enrolment.
890 */
891export const userTotp = pgTable("user_totp", {
892 userId: uuid("user_id")
893 .primaryKey()
894 .references(() => users.id, { onDelete: "cascade" }),
895 secret: text("secret").notNull(),
896 enabledAt: timestamp("enabled_at"),
897 lastUsedAt: timestamp("last_used_at"),
898 createdAt: timestamp("created_at").defaultNow().notNull(),
899});
900
901/**
902 * Recovery codes — single-use fallback when the authenticator is lost.
903 * Stored as SHA-256 hashes; used rows are marked with `usedAt` rather than
904 * deleted so the audit log keeps the full history.
905 */
906export const userRecoveryCodes = pgTable(
907 "user_recovery_codes",
908 {
909 id: uuid("id").primaryKey().defaultRandom(),
910 userId: uuid("user_id")
911 .notNull()
912 .references(() => users.id, { onDelete: "cascade" }),
913 codeHash: text("code_hash").notNull(),
914 usedAt: timestamp("used_at"),
915 createdAt: timestamp("created_at").defaultNow().notNull(),
916 },
917 (table) => [
918 index("recovery_codes_user").on(table.userId),
919 uniqueIndex("recovery_codes_user_hash").on(table.userId, table.codeHash),
920 ]
921);
922
923export type UserTotp = typeof userTotp.$inferSelect;
924export type UserRecoveryCode = typeof userRecoveryCodes.$inferSelect;
2df1f8cClaude925
926/**
927 * WebAuthn passkeys (Block B5).
928 *
929 * Each row is one registered authenticator. The `credentialId` is the
930 * globally-unique identifier the browser returns; `publicKey` is the
931 * COSE-encoded public key we use to verify signatures. `counter` tracks
932 * the authenticator's signature counter for replay-protection.
933 *
934 * `transports` is a JSON array (stored as text) of the
935 * AuthenticatorTransport values ("usb" | "nfc" | "ble" | "internal" | "hybrid").
936 */
937export const userPasskeys = pgTable(
938 "user_passkeys",
939 {
940 id: uuid("id").primaryKey().defaultRandom(),
941 userId: uuid("user_id")
942 .notNull()
943 .references(() => users.id, { onDelete: "cascade" }),
944 credentialId: text("credential_id").notNull().unique(),
945 publicKey: text("public_key").notNull(), // base64url of COSE key
946 counter: integer("counter").default(0).notNull(),
947 transports: text("transports"), // JSON array string
948 name: text("name").notNull().default("Passkey"),
949 lastUsedAt: timestamp("last_used_at"),
950 createdAt: timestamp("created_at").defaultNow().notNull(),
951 },
952 (table) => [index("passkeys_user").on(table.userId)]
953);
954
955/**
956 * Short-lived WebAuthn challenges. A row is written when we issue options
957 * (registration or authentication) and deleted after the verify step or when
958 * it expires (5 min). Keeping them in the DB lets us verify without sticky
959 * sessions or client-side state.
960 */
961export const webauthnChallenges = pgTable(
962 "webauthn_challenges",
963 {
964 id: uuid("id").primaryKey().defaultRandom(),
965 userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }),
966 // For passwordless login we don't know the user yet, so userId is nullable
967 // and we bind the challenge to a short-lived cookie token instead.
968 sessionKey: text("session_key").notNull().unique(),
969 challenge: text("challenge").notNull(),
970 kind: text("kind").notNull(), // "register" | "authenticate"
971 expiresAt: timestamp("expires_at").notNull(),
972 createdAt: timestamp("created_at").defaultNow().notNull(),
973 },
974 (table) => [index("webauthn_challenges_expires").on(table.expiresAt)]
975);
976
977export type UserPasskey = typeof userPasskeys.$inferSelect;
978export type WebauthnChallenge = typeof webauthnChallenges.$inferSelect;
bfdb5e7Claude979
980/**
981 * OAuth 2.0 provider (Block B6).
982 *
983 * `oauthApps` is a third-party app registered by a developer. Each app has
984 * a public `client_id`, a hashed `client_secret`, and one or more allowed
985 * `redirect_uris` (newline-separated). The plaintext secret is shown to the
986 * developer exactly once at creation and cannot be recovered; they can
987 * rotate it instead.
988 *
989 * `oauthAuthorizations` is a short-lived authorization code issued after
990 * the user consents at /oauth/authorize. Single-use: `usedAt` is set on
991 * redemption so a replay after-the-fact fails.
992 *
993 * `oauthAccessTokens` is a long-lived bearer token plus an optional
994 * refresh token. Both are stored as SHA-256 hashes; the plaintext values
995 * are only returned to the client once in the /oauth/token response.
996 */
997export const oauthApps = pgTable(
998 "oauth_apps",
999 {
1000 id: uuid("id").primaryKey().defaultRandom(),
1001 ownerId: uuid("owner_id")
1002 .notNull()
1003 .references(() => users.id, { onDelete: "cascade" }),
1004 name: text("name").notNull(),
1005 clientId: text("client_id").notNull().unique(),
1006 clientSecretHash: text("client_secret_hash").notNull(),
1007 clientSecretPrefix: text("client_secret_prefix").notNull(), // first 8 chars for display
1008 /** Newline-separated list of allowed redirect URIs. */
1009 redirectUris: text("redirect_uris").notNull(),
1010 homepageUrl: text("homepage_url"),
1011 description: text("description"),
1012 /**
1013 * If `true`, the app must present its client_secret at /oauth/token.
1014 * Public SPA/mobile apps should set this to `false` and use PKCE.
1015 */
1016 confidential: boolean("confidential").default(true).notNull(),
1017 revokedAt: timestamp("revoked_at"),
1018 createdAt: timestamp("created_at").defaultNow().notNull(),
1019 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1020 },
1021 (table) => [index("oauth_apps_owner").on(table.ownerId)]
1022);
1023
1024export const oauthAuthorizations = pgTable(
1025 "oauth_authorizations",
1026 {
1027 id: uuid("id").primaryKey().defaultRandom(),
1028 appId: uuid("app_id")
1029 .notNull()
1030 .references(() => oauthApps.id, { onDelete: "cascade" }),
1031 userId: uuid("user_id")
1032 .notNull()
1033 .references(() => users.id, { onDelete: "cascade" }),
1034 codeHash: text("code_hash").notNull().unique(),
1035 redirectUri: text("redirect_uri").notNull(),
1036 scopes: text("scopes").notNull().default(""),
1037 codeChallenge: text("code_challenge"),
1038 codeChallengeMethod: text("code_challenge_method"), // "S256" | "plain"
1039 expiresAt: timestamp("expires_at").notNull(),
1040 usedAt: timestamp("used_at"),
1041 createdAt: timestamp("created_at").defaultNow().notNull(),
1042 },
1043 (table) => [index("oauth_authorizations_expires").on(table.expiresAt)]
1044);
1045
1046export const oauthAccessTokens = pgTable(
1047 "oauth_access_tokens",
1048 {
1049 id: uuid("id").primaryKey().defaultRandom(),
1050 appId: uuid("app_id")
1051 .notNull()
1052 .references(() => oauthApps.id, { onDelete: "cascade" }),
1053 userId: uuid("user_id")
1054 .notNull()
1055 .references(() => users.id, { onDelete: "cascade" }),
1056 accessTokenHash: text("access_token_hash").notNull().unique(),
1057 refreshTokenHash: text("refresh_token_hash").unique(),
1058 scopes: text("scopes").notNull().default(""),
1059 expiresAt: timestamp("expires_at").notNull(),
1060 refreshExpiresAt: timestamp("refresh_expires_at"),
1061 revokedAt: timestamp("revoked_at"),
1062 lastUsedAt: timestamp("last_used_at"),
1063 createdAt: timestamp("created_at").defaultNow().notNull(),
1064 },
1065 (table) => [
1066 index("oauth_access_tokens_user").on(table.userId),
1067 index("oauth_access_tokens_app").on(table.appId),
1068 index("oauth_access_tokens_expires").on(table.expiresAt),
1069 ]
1070);
1071
1072export type OauthApp = typeof oauthApps.$inferSelect;
1073export type OauthAuthorization = typeof oauthAuthorizations.$inferSelect;
1074export type OauthAccessToken = typeof oauthAccessTokens.$inferSelect;
eafe8c6Claude1075
1076/**
1077 * Actions-equivalent workflow runner (Block C1).
1078 *
1079 * `workflows` rows are the YAML files discovered at `.gluecron/workflows/*.yml`
1080 * on the repo's default branch. `parsed` is the normalised JSON form used by
1081 * the runner so we don't re-parse on every trigger.
1082 *
1083 * `workflow_runs` is one execution: one row per trigger event. Status
1084 * progression: queued → running → success|failure|cancelled. `conclusion`
1085 * stays null until `status` is terminal.
1086 *
1087 * `workflow_jobs` is a single job within a run — each has its own steps
1088 * array and concatenated logs. We keep logs inline for v1 (no streaming)
1089 * to avoid a fifth table; they're truncated at the runner.
1090 *
1091 * `workflow_artifacts` persist files a job uploaded. `content` is a bytea;
1092 * we'll move this to object storage once we hit size limits.
1093 */
1094export const workflows = pgTable(
1095 "workflows",
1096 {
1097 id: uuid("id").primaryKey().defaultRandom(),
1098 repositoryId: uuid("repository_id")
1099 .notNull()
1100 .references(() => repositories.id, { onDelete: "cascade" }),
1101 name: text("name").notNull(),
1102 path: text("path").notNull(), // e.g. ".gluecron/workflows/ci.yml"
1103 yaml: text("yaml").notNull(),
1104 parsed: text("parsed").notNull(), // JSON string
1105 onEvents: text("on_events").notNull().default("[]"), // JSON array of event names
1106 disabled: boolean("disabled").default(false).notNull(),
1107 createdAt: timestamp("created_at").defaultNow().notNull(),
1108 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1109 },
1110 (table) => [
1111 index("workflows_repo").on(table.repositoryId),
1112 uniqueIndex("workflows_repo_path").on(table.repositoryId, table.path),
1113 ]
1114);
1115
1116export const workflowRuns = pgTable(
1117 "workflow_runs",
1118 {
1119 id: uuid("id").primaryKey().defaultRandom(),
1120 workflowId: uuid("workflow_id")
1121 .notNull()
1122 .references(() => workflows.id, { onDelete: "cascade" }),
1123 repositoryId: uuid("repository_id")
1124 .notNull()
1125 .references(() => repositories.id, { onDelete: "cascade" }),
1126 runNumber: integer("run_number").notNull(),
1127 event: text("event").notNull(), // "push" | "pull_request" | "manual" | ...
1128 ref: text("ref"),
1129 commitSha: text("commit_sha"),
1130 triggeredBy: uuid("triggered_by").references(() => users.id, {
1131 onDelete: "set null",
1132 }),
1133 status: text("status").notNull().default("queued"), // queued|running|success|failure|cancelled
1134 conclusion: text("conclusion"),
1135 queuedAt: timestamp("queued_at").defaultNow().notNull(),
1136 startedAt: timestamp("started_at"),
1137 finishedAt: timestamp("finished_at"),
1138 createdAt: timestamp("created_at").defaultNow().notNull(),
1139 },
1140 (table) => [
1141 index("workflow_runs_repo").on(table.repositoryId),
1142 index("workflow_runs_status").on(table.status),
1143 index("workflow_runs_workflow").on(table.workflowId),
1144 ]
1145);
1146
1147export const workflowJobs = pgTable(
1148 "workflow_jobs",
1149 {
1150 id: uuid("id").primaryKey().defaultRandom(),
1151 runId: uuid("run_id")
1152 .notNull()
1153 .references(() => workflowRuns.id, { onDelete: "cascade" }),
1154 name: text("name").notNull(),
1155 jobOrder: integer("job_order").default(0).notNull(),
1156 runsOn: text("runs_on").notNull().default("default"),
1157 status: text("status").notNull().default("queued"),
1158 conclusion: text("conclusion"),
1159 exitCode: integer("exit_code"),
1160 steps: text("steps").notNull().default("[]"), // JSON array of step results
1161 logs: text("logs").notNull().default(""),
1162 startedAt: timestamp("started_at"),
1163 finishedAt: timestamp("finished_at"),
1164 createdAt: timestamp("created_at").defaultNow().notNull(),
1165 },
1166 (table) => [index("workflow_jobs_run").on(table.runId)]
1167);
1168
1169export const workflowArtifacts = pgTable(
1170 "workflow_artifacts",
1171 {
1172 id: uuid("id").primaryKey().defaultRandom(),
1173 runId: uuid("run_id")
1174 .notNull()
1175 .references(() => workflowRuns.id, { onDelete: "cascade" }),
1176 jobId: uuid("job_id").references(() => workflowJobs.id, {
1177 onDelete: "set null",
1178 }),
1179 name: text("name").notNull(),
1180 sizeBytes: integer("size_bytes").default(0).notNull(),
1181 contentType: text("content_type")
1182 .default("application/octet-stream")
1183 .notNull(),
1184 // bytea — drizzle doesn't have a built-in bytea type at the level we use
1185 // elsewhere; store as text (base64) for v1. Migration uses real bytea so
1186 // we can swap the column type later.
1187 content: text("content"),
1188 createdAt: timestamp("created_at").defaultNow().notNull(),
1189 },
1190 (table) => [index("workflow_artifacts_run").on(table.runId)]
1191);
1192
1193export type Workflow = typeof workflows.$inferSelect;
1194export type WorkflowRun = typeof workflowRuns.$inferSelect;
1195export type WorkflowJob = typeof workflowJobs.$inferSelect;
1196export type WorkflowArtifact = typeof workflowArtifacts.$inferSelect;
e2da5c6Claude1197
1198// ---------------------------------------------------------------------------
1199// Block C2 — Package registry (npm-compatible)
1200// ---------------------------------------------------------------------------
1201
1202export const packages = pgTable(
1203 "packages",
1204 {
1205 id: uuid("id").primaryKey().defaultRandom(),
1206 repositoryId: uuid("repository_id")
1207 .notNull()
1208 .references(() => repositories.id, { onDelete: "cascade" }),
1209 ecosystem: text("ecosystem").notNull().default("npm"), // "npm" | "container"
1210 scope: text("scope"), // "@acme" for npm; null for unscoped
1211 name: text("name").notNull(), // "my-lib" (without scope)
1212 description: text("description"),
1213 readme: text("readme"),
1214 homepage: text("homepage"),
1215 license: text("license"),
1216 visibility: text("visibility").notNull().default("public"), // "public" | "private"
1217 createdAt: timestamp("created_at").defaultNow().notNull(),
1218 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1219 },
1220 (table) => [
1221 index("packages_repo").on(table.repositoryId),
1222 uniqueIndex("packages_eco_scope_name").on(
1223 table.ecosystem,
1224 table.scope,
1225 table.name
1226 ),
1227 ]
1228);
1229
1230export const packageVersions = pgTable(
1231 "package_versions",
1232 {
1233 id: uuid("id").primaryKey().defaultRandom(),
1234 packageId: uuid("package_id")
1235 .notNull()
1236 .references(() => packages.id, { onDelete: "cascade" }),
1237 version: text("version").notNull(), // "1.2.3"
1238 shasum: text("shasum").notNull(), // sha1 (for npm compat) hex
1239 integrity: text("integrity"), // "sha512-..." base64
1240 sizeBytes: integer("size_bytes").default(0).notNull(),
1241 metadata: text("metadata").notNull().default("{}"), // package.json JSON
1242 tarball: text("tarball"), // base64-encoded; bytea in migration
1243 publishedBy: uuid("published_by").references(() => users.id, {
1244 onDelete: "set null",
1245 }),
1246 yanked: boolean("yanked").default(false).notNull(),
1247 yankedReason: text("yanked_reason"),
1248 publishedAt: timestamp("published_at").defaultNow().notNull(),
1249 },
1250 (table) => [
1251 index("package_versions_pkg").on(table.packageId),
1252 uniqueIndex("package_versions_pkg_version").on(table.packageId, table.version),
1253 ]
1254);
1255
1256export const packageTags = pgTable(
1257 "package_tags",
1258 {
1259 id: uuid("id").primaryKey().defaultRandom(),
1260 packageId: uuid("package_id")
1261 .notNull()
1262 .references(() => packages.id, { onDelete: "cascade" }),
1263 tag: text("tag").notNull(), // "latest" | "beta" | ...
1264 versionId: uuid("version_id")
1265 .notNull()
1266 .references(() => packageVersions.id, { onDelete: "cascade" }),
1267 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1268 },
1269 (table) => [
1270 uniqueIndex("package_tags_pkg_tag").on(table.packageId, table.tag),
1271 ]
1272);
1273
1274export type Package = typeof packages.$inferSelect;
1275export type PackageVersion = typeof packageVersions.$inferSelect;
1276export type PackageTag = typeof packageTags.$inferSelect;
1277
1278// ---------------------------------------------------------------------------
1279// Block C3 — Pages / static hosting
1280// ---------------------------------------------------------------------------
1281
1282export const pagesDeployments = pgTable(
1283 "pages_deployments",
1284 {
1285 id: uuid("id").primaryKey().defaultRandom(),
1286 repositoryId: uuid("repository_id")
1287 .notNull()
1288 .references(() => repositories.id, { onDelete: "cascade" }),
1289 ref: text("ref").notNull().default("refs/heads/gh-pages"),
1290 commitSha: text("commit_sha").notNull(),
1291 status: text("status").notNull().default("success"), // "success" | "failed"
1292 triggeredBy: uuid("triggered_by").references(() => users.id, {
1293 onDelete: "set null",
1294 }),
1295 createdAt: timestamp("created_at").defaultNow().notNull(),
1296 },
1297 (table) => [
1298 index("pages_deployments_repo").on(table.repositoryId),
1299 index("pages_deployments_created").on(table.createdAt),
1300 ]
1301);
1302
1303export const pagesSettings = pgTable("pages_settings", {
1304 repositoryId: uuid("repository_id")
1305 .primaryKey()
1306 .references(() => repositories.id, { onDelete: "cascade" }),
1307 enabled: boolean("enabled").default(true).notNull(),
1308 sourceBranch: text("source_branch").notNull().default("gh-pages"),
1309 sourceDir: text("source_dir").notNull().default("/"), // e.g. "/" or "/docs"
1310 customDomain: text("custom_domain"),
1311 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1312});
1313
1314export type PagesDeployment = typeof pagesDeployments.$inferSelect;
1315export type PagesSettings = typeof pagesSettings.$inferSelect;
1316
1317// ---------------------------------------------------------------------------
1318// Block C4 — Environments with protected approvals
1319// ---------------------------------------------------------------------------
1320
1321export const environments = pgTable(
1322 "environments",
1323 {
1324 id: uuid("id").primaryKey().defaultRandom(),
1325 repositoryId: uuid("repository_id")
1326 .notNull()
1327 .references(() => repositories.id, { onDelete: "cascade" }),
1328 name: text("name").notNull(), // "production" | "staging" | "preview"
1329 requireApproval: boolean("require_approval").default(false).notNull(),
1330 // JSON array of user IDs that can approve deploys.
1331 reviewers: text("reviewers").notNull().default("[]"),
1332 waitTimerMinutes: integer("wait_timer_minutes").default(0).notNull(),
1333 allowedBranches: text("allowed_branches").notNull().default("[]"), // JSON glob patterns
1334 createdAt: timestamp("created_at").defaultNow().notNull(),
1335 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1336 },
1337 (table) => [
1338 uniqueIndex("environments_repo_name").on(table.repositoryId, table.name),
1339 ]
1340);
1341
1342export const deploymentApprovals = pgTable(
1343 "deployment_approvals",
1344 {
1345 id: uuid("id").primaryKey().defaultRandom(),
1346 deploymentId: uuid("deployment_id")
1347 .notNull()
1348 .references(() => deployments.id, { onDelete: "cascade" }),
1349 userId: uuid("user_id")
1350 .notNull()
1351 .references(() => users.id, { onDelete: "cascade" }),
1352 decision: text("decision").notNull(), // "approved" | "rejected"
1353 comment: text("comment"),
1354 createdAt: timestamp("created_at").defaultNow().notNull(),
1355 },
1356 (table) => [
1357 index("deployment_approvals_deployment").on(table.deploymentId),
1358 ]
1359);
1360
1361export type Environment = typeof environments.$inferSelect;
1362export type DeploymentApproval = typeof deploymentApprovals.$inferSelect;
3cbe3d6Claude1363
1364// ---------------------------------------------------------------------------
1365// Block D — AI-native differentiation (migration 0012)
1366// ---------------------------------------------------------------------------
1367
1368// D6 — cached "explain this codebase" markdown keyed on commit sha.
1369export const codebaseExplanations = pgTable(
1370 "codebase_explanations",
1371 {
1372 id: uuid("id").primaryKey().defaultRandom(),
1373 repositoryId: uuid("repository_id")
1374 .notNull()
1375 .references(() => repositories.id, { onDelete: "cascade" }),
1376 commitSha: text("commit_sha").notNull(),
1377 summary: text("summary").notNull(),
1378 markdown: text("markdown").notNull(),
1379 model: text("model").notNull(),
1380 generatedAt: timestamp("generated_at").defaultNow().notNull(),
1381 },
1382 (table) => [
1383 uniqueIndex("codebase_explanations_repo_sha").on(
1384 table.repositoryId,
1385 table.commitSha
1386 ),
1387 ]
1388);
1389
1390// D2 — AI dependency bumper run history.
1391export const depUpdateRuns = pgTable(
1392 "dep_update_runs",
1393 {
1394 id: uuid("id").primaryKey().defaultRandom(),
1395 repositoryId: uuid("repository_id")
1396 .notNull()
1397 .references(() => repositories.id, { onDelete: "cascade" }),
1398 status: text("status").notNull().default("pending"), // pending|running|success|failed|no_updates
1399 ecosystem: text("ecosystem").notNull(), // npm|bun
1400 manifestPath: text("manifest_path").notNull(),
1401 attemptedBumps: text("attempted_bumps").notNull().default("[]"), // JSON
1402 appliedBumps: text("applied_bumps").notNull().default("[]"), // JSON
1403 branchName: text("branch_name"),
1404 prNumber: integer("pr_number"),
1405 errorMessage: text("error_message"),
1406 triggeredBy: uuid("triggered_by").references(() => users.id, {
1407 onDelete: "set null",
1408 }),
1409 createdAt: timestamp("created_at").defaultNow().notNull(),
1410 completedAt: timestamp("completed_at"),
1411 },
1412 (table) => [
1413 index("dep_update_runs_repo").on(table.repositoryId),
1414 index("dep_update_runs_created").on(table.createdAt),
1415 ]
1416);
1417
1418// D1 — code chunks for semantic search. Embedding stored as JSON-encoded
1419// number array in text to avoid requiring pgvector; cosine similarity is
1420// computed in JS. Upgrade path: ALTER COLUMN embedding TYPE vector(1024).
1421export const codeChunks = pgTable(
1422 "code_chunks",
1423 {
1424 id: uuid("id").primaryKey().defaultRandom(),
1425 repositoryId: uuid("repository_id")
1426 .notNull()
1427 .references(() => repositories.id, { onDelete: "cascade" }),
1428 commitSha: text("commit_sha").notNull(),
1429 path: text("path").notNull(),
1430 startLine: integer("start_line").notNull(),
1431 endLine: integer("end_line").notNull(),
1432 content: text("content").notNull(),
1433 embedding: text("embedding"), // JSON number[]
1434 embeddingModel: text("embedding_model"),
1435 createdAt: timestamp("created_at").defaultNow().notNull(),
1436 },
1437 (table) => [
1438 index("code_chunks_repo").on(table.repositoryId),
1439 index("code_chunks_repo_path").on(table.repositoryId, table.path),
1440 ]
1441);
1442
1443export type CodebaseExplanation = typeof codebaseExplanations.$inferSelect;
1444export type DepUpdateRun = typeof depUpdateRuns.$inferSelect;
1e162a8Claude1445
1446// ---------------------------------------------------------------------------
1447// Block E2 — Discussions (migration 0013)
1448// ---------------------------------------------------------------------------
1449
1450/**
1451 * Discussions — forum-style threaded conversations attached to a repo.
1452 * Similar to GitHub Discussions: categorised + pinnable + answerable.
1453 */
1454export const discussions = pgTable(
1455 "discussions",
1456 {
1457 id: uuid("id").primaryKey().defaultRandom(),
1458 number: serial("number"),
1459 repositoryId: uuid("repository_id")
1460 .notNull()
1461 .references(() => repositories.id, { onDelete: "cascade" }),
1462 authorId: uuid("author_id")
1463 .notNull()
1464 .references(() => users.id),
1465 // one of: "general" | "q-and-a" | "ideas" | "announcements" | "show-and-tell"
1466 category: text("category").notNull().default("general"),
1467 title: text("title").notNull(),
1468 body: text("body"),
1469 state: text("state").notNull().default("open"), // open, closed
1470 locked: boolean("locked").notNull().default(false),
1471 answerCommentId: uuid("answer_comment_id"),
1472 pinned: boolean("pinned").notNull().default(false),
1473 createdAt: timestamp("created_at").defaultNow().notNull(),
1474 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1475 },
1476 (table) => [
1477 index("discussions_repo").on(table.repositoryId),
1478 uniqueIndex("discussions_repo_number").on(
1479 table.repositoryId,
1480 table.number
1481 ),
1482 ]
1483);
1484
1485export const discussionComments = pgTable(
1486 "discussion_comments",
1487 {
1488 id: uuid("id").primaryKey().defaultRandom(),
1489 discussionId: uuid("discussion_id")
1490 .notNull()
1491 .references(() => discussions.id, { onDelete: "cascade" }),
1492 parentCommentId: uuid("parent_comment_id"),
1493 authorId: uuid("author_id")
1494 .notNull()
1495 .references(() => users.id),
1496 body: text("body").notNull(),
1497 isAnswer: boolean("is_answer").notNull().default(false),
1498 createdAt: timestamp("created_at").defaultNow().notNull(),
1499 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1500 },
1501 (table) => [
1502 index("discussion_comments_discussion").on(table.discussionId),
1503 ]
1504);
1505
1506export type Discussion = typeof discussions.$inferSelect;
1507export type DiscussionComment = typeof discussionComments.$inferSelect;
3cbe3d6Claude1508export type CodeChunk = typeof codeChunks.$inferSelect;
1e162a8Claude1509
1510// ---------------------------------------------------------------------------
1511// Block E4 — Gists (migration 0014)
1512// ---------------------------------------------------------------------------
1513//
1514// User-owned small snippets/files that behave like tiny repos. DB-backed
1515// for v1 (no bare git repo): each gist owns a collection of gist_files and
1516// every edit appends a gist_revisions row with a JSON snapshot.
1517
1518export const gists = pgTable(
1519 "gists",
1520 {
1521 id: uuid("id").primaryKey().defaultRandom(),
1522 ownerId: uuid("owner_id")
1523 .notNull()
1524 .references(() => users.id, { onDelete: "cascade" }),
1525 // 8-char hex slug used in pretty URLs (e.g. /gists/a1b2c3d4).
1526 slug: text("slug").notNull().unique(),
1527 title: text("title").notNull().default(""),
1528 description: text("description").notNull().default(""),
1529 isPublic: boolean("is_public").default(true).notNull(),
1530 createdAt: timestamp("created_at").defaultNow().notNull(),
1531 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1532 },
1533 (table) => [index("gists_owner").on(table.ownerId)]
1534);
1535
1536export const gistFiles = pgTable(
1537 "gist_files",
1538 {
1539 id: uuid("id").primaryKey().defaultRandom(),
1540 gistId: uuid("gist_id")
1541 .notNull()
1542 .references(() => gists.id, { onDelete: "cascade" }),
1543 filename: text("filename").notNull(),
1544 // Optional explicit language override; falls back to filename detection.
1545 language: text("language"),
1546 content: text("content").notNull().default(""),
1547 sizeBytes: integer("size_bytes").default(0).notNull(),
1548 },
1549 (table) => [
1550 index("gist_files_gist").on(table.gistId),
1551 uniqueIndex("gist_files_gist_filename").on(table.gistId, table.filename),
1552 ]
1553);
1554
1555export const gistRevisions = pgTable(
1556 "gist_revisions",
1557 {
1558 id: uuid("id").primaryKey().defaultRandom(),
1559 gistId: uuid("gist_id")
1560 .notNull()
1561 .references(() => gists.id, { onDelete: "cascade" }),
1562 revision: integer("revision").notNull(),
1563 // JSON-encoded {filename: content} map capturing the full snapshot at
1564 // this revision. Stored as text to avoid requiring jsonb.
1565 snapshot: text("snapshot").notNull().default("{}"),
1566 authorId: uuid("author_id")
1567 .notNull()
1568 .references(() => users.id, { onDelete: "cascade" }),
1569 message: text("message"),
1570 createdAt: timestamp("created_at").defaultNow().notNull(),
1571 },
1572 (table) => [
1573 index("gist_revisions_gist_rev").on(table.gistId, table.revision),
1574 ]
1575);
1576
1577export const gistStars = pgTable(
1578 "gist_stars",
1579 {
1580 id: uuid("id").primaryKey().defaultRandom(),
1581 gistId: uuid("gist_id")
1582 .notNull()
1583 .references(() => gists.id, { onDelete: "cascade" }),
1584 userId: uuid("user_id")
1585 .notNull()
1586 .references(() => users.id, { onDelete: "cascade" }),
1587 createdAt: timestamp("created_at").defaultNow().notNull(),
1588 },
1589 (table) => [uniqueIndex("gist_stars_gist_user").on(table.gistId, table.userId)]
1590);
1591
1592export type Gist = typeof gists.$inferSelect;
1593export type GistFile = typeof gistFiles.$inferSelect;
1594export type GistRevision = typeof gistRevisions.$inferSelect;
1595export type GistStar = typeof gistStars.$inferSelect;
1596
1597// ---------------------------------------------------------------------------
1598// Block E1 — Projects / kanban (migration 0015)
1599// ---------------------------------------------------------------------------
1600
1601export const projects = pgTable(
1602 "projects",
1603 {
1604 id: uuid("id").primaryKey().defaultRandom(),
1605 number: serial("number"),
1606 repositoryId: uuid("repository_id")
1607 .notNull()
1608 .references(() => repositories.id, { onDelete: "cascade" }),
1609 ownerId: uuid("owner_id")
1610 .notNull()
1611 .references(() => users.id),
1612 title: text("title").notNull(),
1613 description: text("description").notNull().default(""),
1614 state: text("state").notNull().default("open"), // open | closed
1615 createdAt: timestamp("created_at").defaultNow().notNull(),
1616 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1617 },
1618 (table) => [
1619 index("projects_repo").on(table.repositoryId),
1620 uniqueIndex("projects_repo_number").on(table.repositoryId, table.number),
1621 ]
1622);
1623
1624export const projectColumns = pgTable(
1625 "project_columns",
1626 {
1627 id: uuid("id").primaryKey().defaultRandom(),
1628 projectId: uuid("project_id")
1629 .notNull()
1630 .references(() => projects.id, { onDelete: "cascade" }),
1631 name: text("name").notNull(),
1632 position: integer("position").notNull().default(0),
1633 createdAt: timestamp("created_at").defaultNow().notNull(),
1634 },
1635 (table) => [index("project_columns_project").on(table.projectId)]
1636);
1637
1638export const projectItems = pgTable(
1639 "project_items",
1640 {
1641 id: uuid("id").primaryKey().defaultRandom(),
1642 projectId: uuid("project_id")
1643 .notNull()
1644 .references(() => projects.id, { onDelete: "cascade" }),
1645 columnId: uuid("column_id")
1646 .notNull()
1647 .references(() => projectColumns.id, { onDelete: "cascade" }),
1648 position: integer("position").notNull().default(0),
1649 // "note" | "issue" | "pr" — application-level FK on itemId by type
1650 itemType: text("item_type").notNull().default("note"),
1651 itemId: uuid("item_id"),
1652 title: text("title").notNull().default(""),
1653 note: text("note").notNull().default(""),
1654 createdAt: timestamp("created_at").defaultNow().notNull(),
1655 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1656 },
1657 (table) => [
1658 index("project_items_project").on(table.projectId),
1659 index("project_items_column").on(table.columnId, table.position),
1660 ]
1661);
1662
1663export type Project = typeof projects.$inferSelect;
1664export type ProjectColumn = typeof projectColumns.$inferSelect;
1665export type ProjectItem = typeof projectItems.$inferSelect;
1666
1667// ---------------------------------------------------------------------------
1668// Block E3 — Wikis (migration 0016)
1669// ---------------------------------------------------------------------------
1670// DB-backed for v1; git-backed mirror is a future upgrade.
1671
1672export const wikiPages = pgTable(
1673 "wiki_pages",
1674 {
1675 id: uuid("id").primaryKey().defaultRandom(),
1676 repositoryId: uuid("repository_id")
1677 .notNull()
1678 .references(() => repositories.id, { onDelete: "cascade" }),
1679 slug: text("slug").notNull(),
1680 title: text("title").notNull(),
1681 body: text("body").notNull().default(""),
1682 revision: integer("revision").notNull().default(1),
1683 createdAt: timestamp("created_at").defaultNow().notNull(),
1684 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1685 updatedBy: uuid("updated_by").references(() => users.id),
1686 },
1687 (table) => [
1688 index("wiki_pages_repo").on(table.repositoryId),
1689 uniqueIndex("wiki_pages_repo_slug").on(table.repositoryId, table.slug),
1690 ]
1691);
1692
1693export const wikiRevisions = pgTable(
1694 "wiki_revisions",
1695 {
1696 id: uuid("id").primaryKey().defaultRandom(),
1697 pageId: uuid("page_id")
1698 .notNull()
1699 .references(() => wikiPages.id, { onDelete: "cascade" }),
1700 revision: integer("revision").notNull(),
1701 title: text("title").notNull(),
1702 body: text("body").notNull().default(""),
1703 message: text("message"),
1704 authorId: uuid("author_id")
1705 .notNull()
1706 .references(() => users.id),
1707 createdAt: timestamp("created_at").defaultNow().notNull(),
1708 },
1709 (table) => [index("wiki_revisions_page").on(table.pageId, table.revision)]
1710);
1711
1712export type WikiPage = typeof wikiPages.$inferSelect;
1713export type WikiRevision = typeof wikiRevisions.$inferSelect;
a79a9edClaude1714
1715// ---------------------------------------------------------------------------
1716// Block E5 — Merge queues (migration 0017)
1717// ---------------------------------------------------------------------------
1718
1719export const mergeQueueEntries = pgTable(
1720 "merge_queue_entries",
1721 {
1722 id: uuid("id").primaryKey().defaultRandom(),
1723 repositoryId: uuid("repository_id")
1724 .notNull()
1725 .references(() => repositories.id, { onDelete: "cascade" }),
1726 pullRequestId: uuid("pull_request_id")
1727 .notNull()
1728 .references(() => pullRequests.id, { onDelete: "cascade" }),
1729 baseBranch: text("base_branch").notNull(),
1730 // queued | running | merged | failed | dequeued
1731 state: text("state").notNull().default("queued"),
1732 position: integer("position").notNull().default(0),
1733 enqueuedBy: uuid("enqueued_by").references(() => users.id),
1734 enqueuedAt: timestamp("enqueued_at").defaultNow().notNull(),
1735 startedAt: timestamp("started_at"),
1736 finishedAt: timestamp("finished_at"),
1737 errorMessage: text("error_message"),
1738 },
1739 (table) => [
1740 index("merge_queue_repo_branch").on(
1741 table.repositoryId,
1742 table.baseBranch,
1743 table.state
1744 ),
1745 ]
1746);
1747
1748export type MergeQueueEntry = typeof mergeQueueEntries.$inferSelect;
1749
1750// ---------------------------------------------------------------------------
1751// Block E6 — Required status checks matrix (migration 0018)
1752// ---------------------------------------------------------------------------
1753
1754export const branchRequiredChecks = pgTable(
1755 "branch_required_checks",
1756 {
1757 id: uuid("id").primaryKey().defaultRandom(),
1758 branchProtectionId: uuid("branch_protection_id")
1759 .notNull()
1760 .references(() => branchProtection.id, { onDelete: "cascade" }),
1761 checkName: text("check_name").notNull(),
1762 createdAt: timestamp("created_at").defaultNow().notNull(),
1763 },
1764 (table) => [
1765 index("branch_required_checks_rule").on(table.branchProtectionId),
1766 uniqueIndex("branch_required_checks_unique").on(
1767 table.branchProtectionId,
1768 table.checkName
1769 ),
1770 ]
1771);
1772
1773export type BranchRequiredCheck = typeof branchRequiredChecks.$inferSelect;
1774
1775// ---------------------------------------------------------------------------
1776// Block E7 — Protected tags (migration 0019)
1777// ---------------------------------------------------------------------------
1778
1779export const protectedTags = pgTable(
1780 "protected_tags",
1781 {
1782 id: uuid("id").primaryKey().defaultRandom(),
1783 repositoryId: uuid("repository_id")
1784 .notNull()
1785 .references(() => repositories.id, { onDelete: "cascade" }),
1786 pattern: text("pattern").notNull(),
1787 createdAt: timestamp("created_at").defaultNow().notNull(),
1788 createdBy: uuid("created_by").references(() => users.id),
1789 },
1790 (table) => [
1791 index("protected_tags_repo").on(table.repositoryId),
1792 uniqueIndex("protected_tags_repo_pattern").on(
1793 table.repositoryId,
1794 table.pattern
1795 ),
1796 ]
1797);
1798
1799export type ProtectedTag = typeof protectedTags.$inferSelect;
8f50ed0Claude1800
1801// ---------------------------------------------------------------------------
1802// Block F — Observability + admin (migration 0020)
1803// ---------------------------------------------------------------------------
1804
1805// F1 — Traffic analytics per repo
1806export const repoTrafficEvents = pgTable(
1807 "repo_traffic_events",
1808 {
1809 id: uuid("id").primaryKey().defaultRandom(),
1810 repositoryId: uuid("repository_id")
1811 .notNull()
1812 .references(() => repositories.id, { onDelete: "cascade" }),
1813 kind: text("kind").notNull(), // view | clone | api | ui
1814 path: text("path"),
1815 userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }),
1816 ipHash: text("ip_hash"),
1817 userAgent: text("user_agent"),
1818 referer: text("referer"),
1819 createdAt: timestamp("created_at").defaultNow().notNull(),
1820 },
1821 (table) => [
1822 index("repo_traffic_events_repo_time").on(
1823 table.repositoryId,
1824 table.createdAt
1825 ),
1826 index("repo_traffic_events_kind").on(
1827 table.repositoryId,
1828 table.kind,
1829 table.createdAt
1830 ),
1831 ]
1832);
1833
1834export type RepoTrafficEvent = typeof repoTrafficEvents.$inferSelect;
1835
1836// F3 — Admin panel (site admins + toggleable flags)
1837export const systemFlags = pgTable("system_flags", {
1838 key: text("key").primaryKey(),
1839 value: text("value").notNull().default(""),
1840 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1841 updatedBy: uuid("updated_by").references(() => users.id),
1842});
1843
1844export type SystemFlag = typeof systemFlags.$inferSelect;
1845
1846export const siteAdmins = pgTable("site_admins", {
1847 userId: uuid("user_id")
1848 .primaryKey()
1849 .references(() => users.id, { onDelete: "cascade" }),
1850 grantedAt: timestamp("granted_at").defaultNow().notNull(),
1851 grantedBy: uuid("granted_by").references(() => users.id),
1852});
1853
1854export type SiteAdmin = typeof siteAdmins.$inferSelect;
1855
1856// F4 — Billing + quotas
1857export const billingPlans = pgTable("billing_plans", {
1858 id: uuid("id").primaryKey().defaultRandom(),
1859 slug: text("slug").notNull().unique(),
1860 name: text("name").notNull(),
1861 priceCents: integer("price_cents").notNull().default(0),
1862 repoLimit: integer("repo_limit").notNull().default(10),
1863 storageMbLimit: integer("storage_mb_limit").notNull().default(1024),
1864 aiTokensMonthly: integer("ai_tokens_monthly").notNull().default(100000),
1865 bandwidthGbMonthly: integer("bandwidth_gb_monthly").notNull().default(10),
1866 privateRepos: boolean("private_repos").notNull().default(false),
1867 createdAt: timestamp("created_at").defaultNow().notNull(),
1868});
1869
1870export type BillingPlan = typeof billingPlans.$inferSelect;
1871
1872export const userQuotas = pgTable("user_quotas", {
1873 userId: uuid("user_id")
1874 .primaryKey()
1875 .references(() => users.id, { onDelete: "cascade" }),
1876 planSlug: text("plan_slug").notNull().default("free"),
1877 storageMbUsed: integer("storage_mb_used").notNull().default(0),
1878 aiTokensUsedThisMonth: integer("ai_tokens_used_this_month")
1879 .notNull()
1880 .default(0),
1881 bandwidthGbUsedThisMonth: integer("bandwidth_gb_used_this_month")
1882 .notNull()
1883 .default(0),
1884 cycleStart: timestamp("cycle_start").defaultNow().notNull(),
1885 updatedAt: timestamp("updated_at").defaultNow().notNull(),
619109aClaude1886 // Stripe linkage (migration 0038). Null until the user first upgrades.
1887 stripeCustomerId: text("stripe_customer_id"),
1888 stripeSubscriptionId: text("stripe_subscription_id"),
1889 stripeSubscriptionStatus: text("stripe_subscription_status"),
1890 currentPeriodEnd: timestamp("current_period_end"),
8f50ed0Claude1891});
1892
1893export type UserQuota = typeof userQuotas.$inferSelect;
06139e6Claude1894
1895// Block H — App marketplace + bot identities (GitHub Apps equivalent)
1896
1897export const apps = pgTable(
1898 "apps",
1899 {
1900 id: uuid("id").primaryKey().defaultRandom(),
1901 slug: text("slug").notNull().unique(),
1902 name: text("name").notNull(),
1903 description: text("description").notNull().default(""),
1904 iconUrl: text("icon_url"),
1905 homepageUrl: text("homepage_url"),
1906 webhookUrl: text("webhook_url"),
1907 webhookSecret: text("webhook_secret"),
1908 creatorId: uuid("creator_id")
1909 .notNull()
1910 .references(() => users.id, { onDelete: "cascade" }),
1911 permissions: text("permissions").notNull().default("[]"), // JSON array
1912 defaultEvents: text("default_events").notNull().default("[]"), // JSON array
1913 isPublic: boolean("is_public").notNull().default(true),
1914 createdAt: timestamp("created_at").defaultNow().notNull(),
1915 updatedAt: timestamp("updated_at").defaultNow().notNull(),
1916 },
1917 (table) => [index("apps_public_slug").on(table.isPublic, table.slug)]
1918);
1919
1920export type App = typeof apps.$inferSelect;
1921
1922export const appInstallations = pgTable(
1923 "app_installations",
1924 {
1925 id: uuid("id").primaryKey().defaultRandom(),
1926 appId: uuid("app_id")
1927 .notNull()
1928 .references(() => apps.id, { onDelete: "cascade" }),
1929 installedBy: uuid("installed_by")
1930 .notNull()
1931 .references(() => users.id, { onDelete: "cascade" }),
1932 targetType: text("target_type").notNull(), // user | org | repository
1933 targetId: uuid("target_id").notNull(),
1934 grantedPermissions: text("granted_permissions").notNull().default("[]"),
1935 suspendedAt: timestamp("suspended_at"),
1936 createdAt: timestamp("created_at").defaultNow().notNull(),
1937 uninstalledAt: timestamp("uninstalled_at"),
1938 },
1939 (table) => [
1940 index("app_installations_app").on(table.appId),
1941 index("app_installations_target").on(table.targetType, table.targetId),
1942 ]
1943);
1944
1945export type AppInstallation = typeof appInstallations.$inferSelect;
1946
1947export const appBots = pgTable("app_bots", {
1948 id: uuid("id").primaryKey().defaultRandom(),
1949 appId: uuid("app_id")
1950 .notNull()
1951 .unique()
1952 .references(() => apps.id, { onDelete: "cascade" }),
1953 username: text("username").notNull().unique(),
1954 displayName: text("display_name").notNull(),
1955 avatarUrl: text("avatar_url"),
1956 createdAt: timestamp("created_at").defaultNow().notNull(),
1957});
1958
1959export type AppBot = typeof appBots.$inferSelect;
1960
1961export const appInstallTokens = pgTable(
1962 "app_install_tokens",
1963 {
1964 id: uuid("id").primaryKey().defaultRandom(),
1965 installationId: uuid("installation_id")
1966 .notNull()
1967 .references(() => appInstallations.id, { onDelete: "cascade" }),
1968 tokenHash: text("token_hash").notNull().unique(),
1969 expiresAt: timestamp("expires_at").notNull(),
1970 createdAt: timestamp("created_at").defaultNow().notNull(),
1971 revokedAt: timestamp("revoked_at"),
1972 },
1973 (table) => [index("app_install_tokens_hash").on(table.tokenHash)]
1974);
1975
1976export type AppInstallToken = typeof appInstallTokens.$inferSelect;
1977
1978export const appEvents = pgTable(
1979 "app_events",
1980 {
1981 id: uuid("id").primaryKey().defaultRandom(),
1982 appId: uuid("app_id")
1983 .notNull()
1984 .references(() => apps.id, { onDelete: "cascade" }),
1985 installationId: uuid("installation_id"),
1986 kind: text("kind").notNull(), // installed | uninstalled | delivery_ok | delivery_fail
1987 payload: text("payload"),
1988 responseStatus: integer("response_status"),
1989 createdAt: timestamp("created_at").defaultNow().notNull(),
1990 },
1991 (table) => [index("app_events_app_time").on(table.appId, table.createdAt)]
1992);
1993
1994export type AppEvent = typeof appEvents.$inferSelect;
71cd5ecClaude1995
1996// ---------- Block I3 — Repository transfer history ----------
1997
1998export const repoTransfers = pgTable(
1999 "repo_transfers",
2000 {
2001 id: uuid("id").primaryKey().defaultRandom(),
2002 repositoryId: uuid("repository_id")
2003 .notNull()
2004 .references(() => repositories.id, { onDelete: "cascade" }),
2005 fromOwnerId: uuid("from_owner_id").notNull(),
2006 fromOrgId: uuid("from_org_id"),
2007 toOwnerId: uuid("to_owner_id").notNull(),
2008 toOrgId: uuid("to_org_id"),
2009 initiatedBy: uuid("initiated_by")
2010 .notNull()
2011 .references(() => users.id, { onDelete: "cascade" }),
2012 createdAt: timestamp("created_at").defaultNow().notNull(),
2013 },
2014 (table) => [
2015 index("repo_transfers_repo").on(table.repositoryId, table.createdAt),
2016 ]
2017);
2018
2019export type RepoTransfer = typeof repoTransfers.$inferSelect;
08420cdClaude2020
2021// ---------- Block I6 — Sponsors ----------
2022
2023export const sponsorshipTiers = pgTable(
2024 "sponsorship_tiers",
2025 {
2026 id: uuid("id").primaryKey().defaultRandom(),
2027 maintainerId: uuid("maintainer_id")
2028 .notNull()
2029 .references(() => users.id, { onDelete: "cascade" }),
2030 name: text("name").notNull(),
2031 description: text("description").default("").notNull(),
2032 monthlyCents: integer("monthly_cents").notNull(),
2033 oneTimeAllowed: boolean("one_time_allowed").default(true).notNull(),
2034 isActive: boolean("is_active").default(true).notNull(),
2035 createdAt: timestamp("created_at").defaultNow().notNull(),
2036 },
2037 (table) => [
2038 index("sponsor_tiers_maintainer").on(table.maintainerId, table.isActive),
2039 ]
2040);
2041
2042export type SponsorshipTier = typeof sponsorshipTiers.$inferSelect;
2043
2044export const sponsorships = pgTable(
2045 "sponsorships",
2046 {
2047 id: uuid("id").primaryKey().defaultRandom(),
2048 sponsorId: uuid("sponsor_id")
2049 .notNull()
2050 .references(() => users.id, { onDelete: "cascade" }),
2051 maintainerId: uuid("maintainer_id")
2052 .notNull()
2053 .references(() => users.id, { onDelete: "cascade" }),
2054 tierId: uuid("tier_id"),
2055 amountCents: integer("amount_cents").notNull(),
2056 kind: text("kind").notNull(), // one_time | monthly
2057 note: text("note"),
2058 isPublic: boolean("is_public").default(true).notNull(),
2059 externalRef: text("external_ref"),
2060 cancelledAt: timestamp("cancelled_at"),
2061 createdAt: timestamp("created_at").defaultNow().notNull(),
2062 },
2063 (table) => [
2064 index("sponsorships_maintainer").on(
2065 table.maintainerId,
2066 table.createdAt
2067 ),
2068 index("sponsorships_sponsor").on(table.sponsorId, table.createdAt),
2069 ]
2070);
2071
2072export type Sponsorship = typeof sponsorships.$inferSelect;
4c8f666Claude2073
2074// Block I8 — Code symbol index for xref navigation.
2075export const codeSymbols = pgTable(
2076 "code_symbols",
2077 {
2078 id: uuid("id").primaryKey().defaultRandom(),
2079 repositoryId: uuid("repository_id")
2080 .notNull()
2081 .references(() => repositories.id, { onDelete: "cascade" }),
2082 commitSha: text("commit_sha").notNull(),
2083 name: text("name").notNull(),
2084 kind: text("kind").notNull(), // function | class | interface | type | const | variable
2085 path: text("path").notNull(),
2086 line: integer("line").notNull(),
2087 signature: text("signature"),
2088 createdAt: timestamp("created_at").defaultNow().notNull(),
2089 },
2090 (table) => [
2091 index("code_symbols_repo_name_idx").on(table.repositoryId, table.name),
2092 index("code_symbols_repo_path_idx").on(table.repositoryId, table.path),
2093 ]
2094);
2095
2096export type CodeSymbol = typeof codeSymbols.$inferSelect;
4a0dea1Claude2097
2098// Block I9 — Repository mirroring. One row per mirrored repo + an
2099// append-only log of sync attempts.
2100export const repoMirrors = pgTable("repo_mirrors", {
2101 id: uuid("id").primaryKey().defaultRandom(),
2102 repositoryId: uuid("repository_id")
2103 .notNull()
2104 .unique()
2105 .references(() => repositories.id, { onDelete: "cascade" }),
2106 upstreamUrl: text("upstream_url").notNull(),
2107 intervalMinutes: integer("interval_minutes").default(1440).notNull(),
2108 lastSyncedAt: timestamp("last_synced_at"),
2109 lastStatus: text("last_status"), // "ok" | "error"
2110 lastError: text("last_error"),
2111 isEnabled: boolean("is_enabled").default(true).notNull(),
2112 createdAt: timestamp("created_at").defaultNow().notNull(),
2113 updatedAt: timestamp("updated_at").defaultNow().notNull(),
2114});
2115
2116export type RepoMirror = typeof repoMirrors.$inferSelect;
2117
2118export const repoMirrorRuns = pgTable(
2119 "repo_mirror_runs",
2120 {
2121 id: uuid("id").primaryKey().defaultRandom(),
2122 mirrorId: uuid("mirror_id")
2123 .notNull()
2124 .references(() => repoMirrors.id, { onDelete: "cascade" }),
2125 startedAt: timestamp("started_at").defaultNow().notNull(),
2126 finishedAt: timestamp("finished_at"),
2127 status: text("status").default("running").notNull(),
2128 message: text("message"),
2129 exitCode: integer("exit_code"),
2130 },
2131 (table) => [
2132 index("repo_mirror_runs_mirror_id_idx").on(table.mirrorId, table.startedAt),
2133 ]
2134);
2135
2136export type RepoMirrorRun = typeof repoMirrorRuns.$inferSelect;
edf7c36Claude2137
2138// ----------------------------------------------------------------------------
2139// Block I10 — Enterprise SSO (OIDC)
2140// ----------------------------------------------------------------------------
2141
2142/** Site-wide SSO provider. Singleton row with id = 'default'. */
2143export const ssoConfig = pgTable("sso_config", {
2144 id: text("id").primaryKey(),
2145 enabled: boolean("enabled").default(false).notNull(),
2146 providerName: text("provider_name").default("SSO").notNull(),
2147 issuer: text("issuer"),
2148 authorizationEndpoint: text("authorization_endpoint"),
2149 tokenEndpoint: text("token_endpoint"),
2150 userinfoEndpoint: text("userinfo_endpoint"),
2151 clientId: text("client_id"),
2152 clientSecret: text("client_secret"),
2153 scopes: text("scopes").default("openid profile email").notNull(),
2154 allowedEmailDomains: text("allowed_email_domains"),
2155 autoCreateUsers: boolean("auto_create_users").default(true).notNull(),
2156 createdAt: timestamp("created_at").defaultNow().notNull(),
2157 updatedAt: timestamp("updated_at").defaultNow().notNull(),
2158});
2159
2160export type SsoConfig = typeof ssoConfig.$inferSelect;
2161
2162/** Maps a local user to an IdP `sub` claim. */
2163export const ssoUserLinks = pgTable(
2164 "sso_user_links",
2165 {
2166 id: uuid("id").primaryKey().defaultRandom(),
2167 userId: uuid("user_id")
2168 .notNull()
2169 .references(() => users.id, { onDelete: "cascade" }),
2170 subject: text("subject").notNull().unique(),
2171 emailAtLink: text("email_at_link").notNull(),
2172 linkedAt: timestamp("linked_at").defaultNow().notNull(),
2173 },
2174 (table) => [index("sso_user_links_user_id_idx").on(table.userId)]
2175);
2176
2177export type SsoUserLink = typeof ssoUserLinks.$inferSelect;
8098672Claude2178
2179// ----------------------------------------------------------------------------
2180// Block J1 — Dependency graph
2181// ----------------------------------------------------------------------------
2182
2183/**
2184 * Last known set of dependencies parsed from manifest files. Each reindex
2185 * replaces the prior rows for that repo. One row per (ecosystem, name,
2186 * manifest_path) — same name in multiple manifests is kept.
2187 */
2188export const repoDependencies = pgTable(
2189 "repo_dependencies",
2190 {
2191 id: uuid("id").primaryKey().defaultRandom(),
2192 repositoryId: uuid("repository_id")
2193 .notNull()
2194 .references(() => repositories.id, { onDelete: "cascade" }),
2195 ecosystem: text("ecosystem").notNull(),
2196 name: text("name").notNull(),
2197 versionSpec: text("version_spec"),
2198 manifestPath: text("manifest_path").notNull(),
2199 isDev: boolean("is_dev").default(false).notNull(),
2200 commitSha: text("commit_sha").notNull(),
2201 indexedAt: timestamp("indexed_at").defaultNow().notNull(),
2202 },
2203 (table) => [
2204 index("repo_dependencies_repo_id_idx").on(
2205 table.repositoryId,
2206 table.ecosystem
2207 ),
2208 index("repo_dependencies_name_idx").on(table.name),
2209 ]
2210);
2211
2212export type RepoDependency = typeof repoDependencies.$inferSelect;
f60ccdeClaude2213
2214// ----------------------------------------------------------------------------
2215// Block J2 — Security advisories + alerts
2216// ----------------------------------------------------------------------------
2217
2218/** CVE-style package advisories. Populated via seed + admin import. */
2219export const securityAdvisories = pgTable(
2220 "security_advisories",
2221 {
2222 id: uuid("id").primaryKey().defaultRandom(),
2223 ghsaId: text("ghsa_id").unique(),
2224 cveId: text("cve_id"),
2225 summary: text("summary").notNull(),
2226 severity: text("severity").default("moderate").notNull(),
2227 ecosystem: text("ecosystem").notNull(),
2228 packageName: text("package_name").notNull(),
2229 affectedRange: text("affected_range").notNull(),
2230 fixedVersion: text("fixed_version"),
2231 referenceUrl: text("reference_url"),
2232 publishedAt: timestamp("published_at").defaultNow().notNull(),
2233 },
2234 (table) => [
2235 index("security_advisories_pkg_idx").on(
2236 table.ecosystem,
2237 table.packageName
2238 ),
2239 ]
2240);
2241
2242export type SecurityAdvisory = typeof securityAdvisories.$inferSelect;
2243
2244/** Per-repo match state. One row per (repo, advisory, manifest_path). */
2245export const repoAdvisoryAlerts = pgTable(
2246 "repo_advisory_alerts",
2247 {
2248 id: uuid("id").primaryKey().defaultRandom(),
2249 repositoryId: uuid("repository_id")
2250 .notNull()
2251 .references(() => repositories.id, { onDelete: "cascade" }),
2252 advisoryId: uuid("advisory_id")
2253 .notNull()
2254 .references(() => securityAdvisories.id, { onDelete: "cascade" }),
2255 dependencyName: text("dependency_name").notNull(),
2256 dependencyVersion: text("dependency_version"),
2257 manifestPath: text("manifest_path").notNull(),
2258 status: text("status").default("open").notNull(),
2259 dismissedReason: text("dismissed_reason"),
2260 createdAt: timestamp("created_at").defaultNow().notNull(),
2261 updatedAt: timestamp("updated_at").defaultNow().notNull(),
2262 },
2263 (table) => [
2264 index("repo_advisory_alerts_status_idx").on(
2265 table.repositoryId,
2266 table.status
2267 ),
2268 ]
2269);
2270
2271export type RepoAdvisoryAlert = typeof repoAdvisoryAlerts.$inferSelect;
3951454Claude2272
2273// ----------------------------------------------------------------------------
2274// Block J3 — Commit signature verification (GPG + SSH)
2275// ----------------------------------------------------------------------------
2276
2277/** Per-user GPG/SSH public keys for commit signing. */
2278export const signingKeys = pgTable(
2279 "signing_keys",
2280 {
2281 id: uuid("id").primaryKey().defaultRandom(),
2282 userId: uuid("user_id")
2283 .notNull()
2284 .references(() => users.id, { onDelete: "cascade" }),
2285 keyType: text("key_type").notNull(), // 'gpg' | 'ssh'
2286 title: text("title").notNull(),
2287 fingerprint: text("fingerprint").notNull(),
2288 publicKey: text("public_key").notNull(),
2289 email: text("email"),
2290 expiresAt: timestamp("expires_at"),
2291 lastUsedAt: timestamp("last_used_at"),
2292 createdAt: timestamp("created_at").defaultNow().notNull(),
2293 },
2294 (table) => [
2295 uniqueIndex("signing_keys_fp_unique").on(table.keyType, table.fingerprint),
2296 index("signing_keys_user_idx").on(table.userId),
2297 ]
2298);
2299
2300export type SigningKey = typeof signingKeys.$inferSelect;
2301
2302/**
2303 * Cached verification result for a (repo, commit) pair. Repopulated on demand;
2304 * rows are invalidated implicitly by CASCADE when either side is removed.
2305 */
2306export const commitVerifications = pgTable(
2307 "commit_verifications",
2308 {
2309 id: uuid("id").primaryKey().defaultRandom(),
2310 repositoryId: uuid("repository_id")
2311 .notNull()
2312 .references(() => repositories.id, { onDelete: "cascade" }),
2313 commitSha: text("commit_sha").notNull(),
2314 verified: boolean("verified").default(false).notNull(),
2315 reason: text("reason").notNull(),
2316 signatureType: text("signature_type"),
2317 signerKeyId: uuid("signer_key_id").references(() => signingKeys.id, {
2318 onDelete: "set null",
2319 }),
2320 signerUserId: uuid("signer_user_id").references(() => users.id, {
2321 onDelete: "set null",
2322 }),
2323 signerFingerprint: text("signer_fingerprint"),
2324 verifiedAt: timestamp("verified_at").defaultNow().notNull(),
2325 },
2326 (table) => [
2327 uniqueIndex("commit_verifications_sha_unique").on(
2328 table.repositoryId,
2329 table.commitSha
2330 ),
2331 ]
2332);
2333
2334export type CommitVerification = typeof commitVerifications.$inferSelect;
7aa8b99Claude2335
2336// ----------------------------------------------------------------------------
2337// Block J4 — User following
2338// ----------------------------------------------------------------------------
2339
2340/**
2341 * Directed user→user follow edges. Primary key is the composite
2342 * (follower_id, following_id) at the SQL level; drizzle sees it as a
2343 * regular table with a unique index plus a secondary index on the
2344 * reverse-lookup column.
2345 */
2346export const userFollows = pgTable(
2347 "user_follows",
2348 {
2349 followerId: uuid("follower_id")
2350 .notNull()
2351 .references(() => users.id, { onDelete: "cascade" }),
2352 followingId: uuid("following_id")
2353 .notNull()
2354 .references(() => users.id, { onDelete: "cascade" }),
2355 createdAt: timestamp("created_at").defaultNow().notNull(),
2356 },
2357 (table) => [
2358 uniqueIndex("user_follows_pair_unique").on(
2359 table.followerId,
2360 table.followingId
2361 ),
2362 index("user_follows_following_idx").on(table.followingId),
2363 ]
2364);
2365
2366export type UserFollow = typeof userFollows.$inferSelect;
9ff7128Claude2367
2368// ----------------------------------------------------------------------------
2369// Block J6 — Repository rulesets
2370// ----------------------------------------------------------------------------
2371
2372/**
2373 * A ruleset groups N rules under a named policy at enforcement level active /
2374 * evaluate / disabled. Unique per (repo, name) so a repo can carry multiple
2375 * overlapping rulesets (e.g. "release branches" vs "everywhere").
2376 */
2377export const repoRulesets = pgTable(
2378 "repo_rulesets",
2379 {
2380 id: uuid("id").primaryKey().defaultRandom(),
2381 repositoryId: uuid("repository_id")
2382 .notNull()
2383 .references(() => repositories.id, { onDelete: "cascade" }),
2384 name: text("name").notNull(),
2385 enforcement: text("enforcement").default("active").notNull(),
2386 createdBy: uuid("created_by").references(() => users.id, {
2387 onDelete: "set null",
2388 }),
2389 createdAt: timestamp("created_at").defaultNow().notNull(),
2390 updatedAt: timestamp("updated_at").defaultNow().notNull(),
2391 },
2392 (table) => [
2393 index("repo_rulesets_repo_idx").on(table.repositoryId),
2394 uniqueIndex("repo_rulesets_repo_name_unique").on(
2395 table.repositoryId,
2396 table.name
2397 ),
2398 ]
2399);
2400
2401export type RepoRuleset = typeof repoRulesets.$inferSelect;
2402
2403/** Individual rule — type tag plus JSON params. */
2404export const rulesetRules = pgTable(
2405 "ruleset_rules",
2406 {
2407 id: uuid("id").primaryKey().defaultRandom(),
2408 rulesetId: uuid("ruleset_id")
2409 .notNull()
2410 .references(() => repoRulesets.id, { onDelete: "cascade" }),
2411 ruleType: text("rule_type").notNull(),
2412 params: text("params").default("{}").notNull(),
2413 createdAt: timestamp("created_at").defaultNow().notNull(),
2414 },
2415 (table) => [index("ruleset_rules_set_idx").on(table.rulesetId)]
2416);
2417
2418export type RulesetRule = typeof rulesetRules.$inferSelect;
0cdfd89Claude2419
2420// ---------------------------------------------------------------------------
2421// Block J8 — Commit statuses.
2422// ---------------------------------------------------------------------------
2423
2424/**
2425 * External CI / automation posts per-commit (sha, context) statuses. Upsert
2426 * semantics keyed on (repository, commit_sha, context). State vocabulary:
2427 * pending | success | failure | error. Combined rollup logic lives in
2428 * `src/lib/commit-statuses.ts`.
2429 */
2430export const commitStatuses = pgTable(
2431 "commit_statuses",
2432 {
2433 id: uuid("id").primaryKey().defaultRandom(),
2434 repositoryId: uuid("repository_id")
2435 .notNull()
2436 .references(() => repositories.id, { onDelete: "cascade" }),
2437 commitSha: text("commit_sha").notNull(),
2438 state: text("state").notNull(),
2439 context: text("context").default("default").notNull(),
2440 description: text("description"),
2441 targetUrl: text("target_url"),
2442 creatorId: uuid("creator_id").references(() => users.id, {
2443 onDelete: "set null",
2444 }),
2445 createdAt: timestamp("created_at").defaultNow().notNull(),
2446 updatedAt: timestamp("updated_at").defaultNow().notNull(),
2447 },
2448 (table) => [
2449 uniqueIndex("commit_statuses_repo_sha_context_unique").on(
2450 table.repositoryId,
2451 table.commitSha,
2452 table.context
2453 ),
2454 index("commit_statuses_repo_sha_idx").on(
2455 table.repositoryId,
2456 table.commitSha
2457 ),
2458 ]
2459);
2460
2461export type CommitStatus = typeof commitStatuses.$inferSelect;
23d1a81Claude2462
2463// ---------------------------------------------------------------------------
2464// Collaborators — per-repo role grants (read / write / admin).
2465// ---------------------------------------------------------------------------
2466
2467/**
2468 * A user granted access to a repository beyond ownership. Roles are
2469 * hierarchical — 'admin' implies write, write implies read. `invitedBy` tracks
2470 * who added them (nullable once that user is deleted); `acceptedAt` is null
2471 * until the invitee explicitly accepts. Unique per (repo, user) so a given
2472 * user has at most one role per repo.
2473 */
2474export const repoCollaborators = pgTable(
2475 "repo_collaborators",
2476 {
2477 id: uuid("id").primaryKey().defaultRandom(),
2478 repositoryId: uuid("repository_id")
2479 .notNull()
2480 .references(() => repositories.id, { onDelete: "cascade" }),
2481 userId: uuid("user_id")
2482 .notNull()
2483 .references(() => users.id, { onDelete: "cascade" }),
2484 role: text("role", { enum: ["read", "write", "admin"] })
2485 .notNull()
2486 .default("read"),
2487 invitedBy: uuid("invited_by").references(() => users.id, {
2488 onDelete: "set null",
2489 }),
2490 invitedAt: timestamp("invited_at").defaultNow().notNull(),
2491 acceptedAt: timestamp("accepted_at"),
febd4f0Claude2492 // sha256(plaintext) of the outstanding invite token. Set when the owner
2493 // sends an invite, cleared when the invitee accepts. NULL on older rows
2494 // that were auto-accepted before the email flow existed.
2495 inviteTokenHash: text("invite_token_hash"),
23d1a81Claude2496 },
2497 (table) => [
2498 uniqueIndex("repo_collaborators_repo_user_uq").on(
2499 table.repositoryId,
2500 table.userId
2501 ),
2502 index("repo_collaborators_repo_idx").on(table.repositoryId),
2503 index("repo_collaborators_user_idx").on(table.userId),
2504 ]
2505);
2506
2507export type RepoCollaborator = typeof repoCollaborators.$inferSelect;
abfa9adClaude2508
2509// ---------------------------------------------------------------------------
2510// Workflow engine v2 — Sprint 1 additions (drizzle/0037_workflow_engine_v2.sql)
2511//
2512// Strictly additive to Block C1. The original workflow tables defined above
2513// (workflows / workflow_runs / workflow_jobs / workflow_artifacts) are locked
2514// and must not be altered in-place; the four tables below extend the runner
2515// with secrets, workflow_dispatch inputs, a content-addressable cache, and a
2516// warm-runner worker pool.
2517// ---------------------------------------------------------------------------
2518
2519/**
2520 * Per-repo encrypted secrets exposed to workflow jobs as env vars.
2521 *
2522 * `encrypted_value` is base64 of `iv || authTag || ciphertext` produced by
2523 * AES-256-GCM — the crypto lives in `src/lib/workflow-crypto.ts` (Agent 2);
2524 * the DB only stores opaque ciphertext. `name` is validated against
2525 * `[A-Z_][A-Z0-9_]*` at the write-site, not the DB. Unique per (repo, name).
2526 */
2527export const workflowSecrets = pgTable(
2528 "workflow_secrets",
2529 {
2530 id: uuid("id").primaryKey().defaultRandom(),
2531 repositoryId: uuid("repository_id")
2532 .notNull()
2533 .references(() => repositories.id, { onDelete: "cascade" }),
2534 name: text("name").notNull(),
2535 encryptedValue: text("encrypted_value").notNull(),
2536 createdBy: uuid("created_by").references(() => users.id, {
2537 onDelete: "set null",
2538 }),
2539 createdAt: timestamp("created_at").defaultNow().notNull(),
2540 updatedAt: timestamp("updated_at").defaultNow().notNull(),
2541 },
2542 (table) => [
2543 uniqueIndex("workflow_secrets_repo_name_uq").on(
2544 table.repositoryId,
2545 table.name
2546 ),
2547 index("workflow_secrets_repo_idx").on(table.repositoryId),
2548 ]
2549);
2550
2551export type WorkflowSecret = typeof workflowSecrets.$inferSelect;
2552export type NewWorkflowSecret = typeof workflowSecrets.$inferInsert;
2553
2554/**
2555 * Parameter schema for the `workflow_dispatch` trigger. One row per input
2556 * declared in the workflow YAML. `type` is constrained to the four values
2557 * GitHub Actions supports; `options` is only meaningful for type='choice'
2558 * (a JSON array of allowed strings). `default_value` and `description` are
2559 * optional. Unique per (workflow, name) so two inputs on the same workflow
2560 * can't share an identifier.
2561 */
2562export const workflowDispatchInputs = pgTable(
2563 "workflow_dispatch_inputs",
2564 {
2565 id: uuid("id").primaryKey().defaultRandom(),
2566 workflowId: uuid("workflow_id")
2567 .notNull()
2568 .references(() => workflows.id, { onDelete: "cascade" }),
2569 name: text("name").notNull(),
2570 type: text("type", {
2571 enum: ["string", "boolean", "choice", "number"],
2572 }).notNull(),
2573 required: boolean("required").default(false).notNull(),
2574 defaultValue: text("default_value"),
2575 // JSON array of strings; only populated when type = 'choice'.
2576 options: jsonb("options"),
2577 description: text("description"),
2578 },
2579 (table) => [
2580 uniqueIndex("workflow_dispatch_inputs_wf_name_uq").on(
2581 table.workflowId,
2582 table.name
2583 ),
2584 ]
2585);
2586
2587export type WorkflowDispatchInput =
2588 typeof workflowDispatchInputs.$inferSelect;
2589export type NewWorkflowDispatchInput =
2590 typeof workflowDispatchInputs.$inferInsert;
2591
2592/**
2593 * Content-addressable cache backing the `gluecron/cache@v1` action. Rows are
2594 * keyed by user-chosen `cache_key` within a `scope` — 'repo' for repo-wide
2595 * entries, 'branch' or 'tag' when isolation is desired (with `scope_ref`
2596 * holding the branch/tag name). `content_hash` is the sha256 of `content`
2597 * so jobs can short-circuit redundant writes. `content` is real bytea; the
2598 * 100MB payload cap is enforced at the write-site, not by the DB. LRU
2599 * eviction reads (`repository_id`, `last_accessed_at`).
2600 */
2601export const workflowRunCache = pgTable(
2602 "workflow_run_cache",
2603 {
2604 id: uuid("id").primaryKey().defaultRandom(),
2605 repositoryId: uuid("repository_id")
2606 .notNull()
2607 .references(() => repositories.id, { onDelete: "cascade" }),
2608 cacheKey: text("cache_key").notNull(),
2609 // 'repo' | 'branch' | 'tag' — constraint enforced at the write-site.
2610 scope: text("scope").default("repo").notNull(),
2611 scopeRef: text("scope_ref"),
2612 contentHash: text("content_hash").notNull(),
2613 content: bytea("content").notNull(),
2614 sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(),
2615 createdAt: timestamp("created_at").defaultNow().notNull(),
2616 lastAccessedAt: timestamp("last_accessed_at").defaultNow().notNull(),
2617 },
2618 (table) => [
2619 uniqueIndex("workflow_run_cache_repo_key_scope_uq").on(
2620 table.repositoryId,
2621 table.cacheKey,
2622 table.scope,
2623 table.scopeRef
2624 ),
2625 index("workflow_run_cache_repo_lru_idx").on(
2626 table.repositoryId,
2627 table.lastAccessedAt
2628 ),
2629 ]
2630);
2631
2632export type WorkflowRunCache = typeof workflowRunCache.$inferSelect;
2633export type NewWorkflowRunCache = typeof workflowRunCache.$inferInsert;
2634
2635/**
2636 * Warm-runner worker registry. The scheduler reads idle rows to dispatch
2637 * queued jobs without paying cold-start cost; workers heartbeat here so
2638 * stale entries (>N seconds since `last_heartbeat_at`) can be reaped.
2639 * `worker_id` is a stable string (hostname:pid or similar) and is globally
2640 * unique so a restarted worker naturally replaces its predecessor.
2641 * `current_run_id` is set when status='busy' and cleared on completion.
2642 */
2643export const workflowRunnerPool = pgTable(
2644 "workflow_runner_pool",
2645 {
2646 id: uuid("id").primaryKey().defaultRandom(),
2647 workerId: text("worker_id").notNull().unique(),
2648 status: text("status", {
2649 enum: ["idle", "busy", "draining", "dead"],
2650 }).notNull(),
2651 currentRunId: uuid("current_run_id").references(() => workflowRuns.id, {
2652 onDelete: "set null",
2653 }),
2654 warmedAt: timestamp("warmed_at").defaultNow().notNull(),
2655 lastHeartbeatAt: timestamp("last_heartbeat_at").defaultNow().notNull(),
2656 capacity: integer("capacity").default(1).notNull(),
2657 },
2658 (table) => [index("workflow_runner_pool_status_idx").on(table.status)]
2659);
2660
2661export type WorkflowRunnerPoolEntry =
2662 typeof workflowRunnerPool.$inferSelect;
2663export type NewWorkflowRunnerPoolEntry =
2664 typeof workflowRunnerPool.$inferInsert;
e6bad81Claude2665
2666
2667/**
2668 * Repair Flywheel — every auto-repair attempt is recorded here so future
2669 * failures with the same signature can short-circuit straight to the cached
2670 * patch (Tier 0). After ~5000 entries the cache dominates and AI calls drop.
2671 * Migration: 0039_repair_flywheel.sql
2672 */
2673export const repairFlywheel = pgTable(
2674 "repair_flywheel",
2675 {
2676 id: uuid("id").primaryKey().defaultRandom(),
2677 repositoryId: uuid("repository_id").references(() => repositories.id, {
2678 onDelete: "cascade",
2679 }),
2680 // Fingerprint of the normalised failure text (variables/paths stripped).
2681 failureSignature: text("failure_signature").notNull(),
2682 // Original failure text (capped at write-site, ~4KB).
2683 failureText: text("failure_text").notNull(),
2684 // Mechanical classification or NULL if AI/human-driven.
2685 failureClassification: text("failure_classification"),
2686 // 'cached' | 'mechanical' | 'ai-sonnet' | 'human'
2687 repairTier: text("repair_tier").notNull(),
2688 patchSummary: text("patch_summary").notNull(),
2689 filesChanged: jsonb("files_changed").notNull().default([]),
2690 commitSha: text("commit_sha"),
2691 // 'pending' | 'success' | 'failed' | 'reverted'
2692 outcome: text("outcome").notNull().default("pending"),
2693 appliedAt: timestamp("applied_at").defaultNow().notNull(),
2694 outcomeAt: timestamp("outcome_at"),
2695 parentPatternId: uuid("parent_pattern_id"),
2696 cacheHitCount: integer("cache_hit_count").default(0).notNull(),
2697 isPublicPattern: boolean("is_public_pattern").default(false).notNull(),
2698 createdAt: timestamp("created_at").defaultNow().notNull(),
2699 },
2700 (table) => [
2701 index("repair_flywheel_signature_idx").on(table.failureSignature),
2702 index("repair_flywheel_repo_idx").on(table.repositoryId),
2703 index("repair_flywheel_outcome_idx").on(table.outcome),
2704 index("repair_flywheel_classification_idx").on(table.failureClassification),
2705 index("repair_flywheel_lookup_idx").on(
2706 table.repositoryId,
2707 table.failureSignature,
2708 table.outcome
2709 ),
2710 ]
2711);
2712
2713export type RepairFlywheelEntry = typeof repairFlywheel.$inferSelect;
2714export type NewRepairFlywheelEntry = typeof repairFlywheel.$inferInsert;
2715
534f04aClaude2716/**
2717 * Block M3 — AI pre-merge risk score cache.
2718 *
2719 * One row per (pull_request_id, commit_sha). The score is computed by a
2720 * transparent formula over `signals`; the LLM only writes `ai_summary`.
2721 * Pinning by head SHA means a new push invalidates the cache implicitly
2722 * (the new SHA won't have a row yet → cache miss → recompute).
2723 *
2724 * Migration: 0044_pr_risk_scores.sql
2725 */
2726export const prRiskScores = pgTable(
2727 "pr_risk_scores",
2728 {
2729 id: uuid("id").primaryKey().defaultRandom(),
2730 pullRequestId: uuid("pull_request_id")
2731 .notNull()
2732 .references(() => pullRequests.id, { onDelete: "cascade" }),
2733 commitSha: text("commit_sha").notNull(),
2734 // 0..10 integer score.
2735 score: integer("score").notNull(),
2736 // 'low' | 'medium' | 'high' | 'critical'
2737 band: text("band").notNull(),
2738 // Raw signal map — see PrRiskSignals in src/lib/pr-risk.ts.
2739 signals: jsonb("signals").notNull(),
2740 aiSummary: text("ai_summary"),
2741 generatedAt: timestamp("generated_at", { withTimezone: true })
2742 .defaultNow()
2743 .notNull(),
2744 },
2745 (table) => [
2746 uniqueIndex("pr_risk_scores_pr_sha_uq").on(
2747 table.pullRequestId,
2748 table.commitSha
2749 ),
2750 index("pr_risk_scores_pr_idx").on(table.pullRequestId),
2751 ]
2752);
2753
2754export type PrRiskScoreRow = typeof prRiskScores.$inferSelect;
2755export type NewPrRiskScoreRow = typeof prRiskScores.$inferInsert;
2756
c63b860Claude2757/**
2758 * Block P1 — Password reset tokens.
2759 *
2760 * One row per outstanding reset request. `tokenHash` is a SHA-256 of the
2761 * plaintext token mailed to the user; the plaintext never persists.
2762 * `usedAt` is flipped on consume so a replayed link cannot rotate the
2763 * password twice. `expiresAt` is enforced at consume time (1-hour TTL).
2764 *
2765 * Migration: 0047_password_reset_tokens.sql
2766 */
2767export const passwordResetTokens = pgTable(
2768 "password_reset_tokens",
2769 {
2770 id: uuid("id").primaryKey().defaultRandom(),
2771 userId: uuid("user_id")
2772 .notNull()
2773 .references(() => users.id, { onDelete: "cascade" }),
2774 tokenHash: text("token_hash").notNull().unique(),
2775 expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2776 usedAt: timestamp("used_at", { withTimezone: true }),
2777 requestIp: text("request_ip"),
2778 createdAt: timestamp("created_at", { withTimezone: true })
2779 .defaultNow()
2780 .notNull(),
2781 },
2782 (table) => [
2783 index("idx_password_reset_tokens_user").on(table.userId),
2784 index("idx_password_reset_tokens_expires").on(table.expiresAt),
2785 ]
2786);
2787
2788export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
2789export type NewPasswordResetToken = typeof passwordResetTokens.$inferInsert;
2790
2791/**
2792 * Block P2 — email verification tokens.
2793 *
2794 * SHA-256 hashed at rest. `email` captured at issuance so a verification
2795 * link still resolves the right address even after a profile-email change.
2796 * Migration: drizzle/0048_email_verification.sql
2797 */
2798export const emailVerificationTokens = pgTable(
2799 "email_verification_tokens",
2800 {
2801 id: uuid("id").primaryKey().defaultRandom(),
2802 userId: uuid("user_id")
2803 .notNull()
2804 .references(() => users.id, { onDelete: "cascade" }),
2805 email: text("email").notNull(),
2806 tokenHash: text("token_hash").notNull().unique(),
2807 expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2808 usedAt: timestamp("used_at", { withTimezone: true }),
2809 createdAt: timestamp("created_at", { withTimezone: true })
2810 .defaultNow()
2811 .notNull(),
2812 },
2813 (table) => [index("idx_email_verify_tokens_user").on(table.userId)]
2814);
2815
2816export type EmailVerificationToken = typeof emailVerificationTokens.$inferSelect;
2817export type NewEmailVerificationToken = typeof emailVerificationTokens.$inferInsert;
2818
cd4f63bTest User2819/**
2820 * Block Q2 — Magic-link sign-in tokens.
2821 *
2822 * Structurally identical to passwordResetTokens / emailVerificationTokens:
2823 * a short plaintext token is mailed to the user, only its sha256 hash is
2824 * persisted, the token is single-use (usedAt) and time-limited (expiresAt).
2825 *
2826 * Difference: `userId` is NULLABLE. When a user enters an email that does
2827 * NOT yet have an account, we still mint a row — consume will create the
2828 * account on click (autoCreate=true), using the click itself as proof the
2829 * recipient owns the address. See `src/lib/magic-link.ts`.
2830 *
2831 * Migration: drizzle/0051_magic_link_tokens.sql
2832 */
2833export const magicLinkTokens = pgTable(
2834 "magic_link_tokens",
2835 {
2836 id: uuid("id").primaryKey().defaultRandom(),
2837 email: text("email").notNull(),
2838 userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }),
2839 tokenHash: text("token_hash").notNull().unique(),
2840 expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
2841 usedAt: timestamp("used_at", { withTimezone: true }),
2842 requestIp: text("request_ip"),
2843 createdAt: timestamp("created_at", { withTimezone: true })
2844 .defaultNow()
2845 .notNull(),
2846 },
2847 (table) => [
2848 index("idx_magic_link_tokens_email").on(table.email),
2849 index("idx_magic_link_tokens_user").on(table.userId),
2850 index("idx_magic_link_tokens_expires").on(table.expiresAt),
2851 ]
2852);
2853
2854export type MagicLinkToken = typeof magicLinkTokens.$inferSelect;
2855export type NewMagicLinkToken = typeof magicLinkTokens.$inferInsert;
2856
b1be050CC LABS App2857// ---------------------------------------------------------------------------
2858// BLOCK S4 — Synthetic monitor history.
2859//
2860// Every autopilot tick runs `runSyntheticChecks()` (see
2861// `src/lib/synthetic-monitor.ts`) and inserts one row per check here.
2862// The /admin/status page reads the most-recent row per check_name and
2863// renders the red/green dashboard. On a green->red transition we fire
2864// MONITOR_ALERT_WEBHOOK_URL so the owner sees the site is on fire.
2865// ---------------------------------------------------------------------------
2866export const syntheticChecks = pgTable(
2867 "synthetic_checks",
2868 {
2869 id: uuid("id").primaryKey().defaultRandom(),
2870 checkName: text("check_name").notNull(),
2871 status: text("status").notNull(), // "green" | "red" | "yellow"
2872 statusCode: integer("status_code"),
2873 durationMs: integer("duration_ms").notNull(),
2874 error: text("error"),
2875 checkedAt: timestamp("checked_at", { withTimezone: true })
2876 .defaultNow()
2877 .notNull(),
2878 },
2879 (table) => [
2880 index("idx_synthetic_checks_checked_at").on(table.checkedAt),
2881 index("idx_synthetic_checks_name_checked_at").on(
2882 table.checkName,
2883 table.checkedAt
2884 ),
2885 ]
2886);
2887
2888export type SyntheticCheckRow = typeof syntheticChecks.$inferSelect;
2889export type NewSyntheticCheckRow = typeof syntheticChecks.$inferInsert;
2890