Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.tsBlame765 lines · 1 contributor
fc1817aClaude1import {
2 pgTable,
3 text,
4 timestamp,
5 uuid,
6 boolean,
7 integer,
8 uniqueIndex,
79136bbClaude9 index,
10 serial,
fc1817aClaude11} from "drizzle-orm/pg-core";
12
13export const users = pgTable("users", {
14 id: uuid("id").primaryKey().defaultRandom(),
15 username: text("username").notNull().unique(),
16 email: text("email").notNull().unique(),
17 displayName: text("display_name"),
18 passwordHash: text("password_hash").notNull(),
19 avatarUrl: text("avatar_url"),
20 bio: text("bio"),
24cf2caClaude21 // Email notification preferences (Block A8). Default on; opt-out via /settings.
22 notifyEmailOnMention: boolean("notify_email_on_mention").default(true).notNull(),
23 notifyEmailOnAssign: boolean("notify_email_on_assign").default(true).notNull(),
24 notifyEmailOnGateFail: boolean("notify_email_on_gate_fail").default(true).notNull(),
fc1817aClaude25 createdAt: timestamp("created_at").defaultNow().notNull(),
26 updatedAt: timestamp("updated_at").defaultNow().notNull(),
27});
28
06d5ffeClaude29export const sessions = pgTable("sessions", {
30 id: uuid("id").primaryKey().defaultRandom(),
31 userId: uuid("user_id")
32 .notNull()
33 .references(() => users.id, { onDelete: "cascade" }),
34 token: text("token").notNull().unique(),
35 expiresAt: timestamp("expires_at").notNull(),
36 createdAt: timestamp("created_at").defaultNow().notNull(),
37});
38
fc1817aClaude39export const repositories = pgTable(
40 "repositories",
41 {
42 id: uuid("id").primaryKey().defaultRandom(),
43 name: text("name").notNull(),
44 ownerId: uuid("owner_id")
45 .notNull()
46 .references(() => users.id),
47 description: text("description"),
48 isPrivate: boolean("is_private").default(false).notNull(),
3ef4c9dClaude49 isArchived: boolean("is_archived").default(false).notNull(),
fc1817aClaude50 defaultBranch: text("default_branch").default("main").notNull(),
51 diskPath: text("disk_path").notNull(),
c81ab7aClaude52 forkedFromId: uuid("forked_from_id").references(() => repositories.id, {
53 onDelete: "set null",
54 }),
fc1817aClaude55 createdAt: timestamp("created_at").defaultNow().notNull(),
56 updatedAt: timestamp("updated_at").defaultNow().notNull(),
57 pushedAt: timestamp("pushed_at"),
58 starCount: integer("star_count").default(0).notNull(),
59 forkCount: integer("fork_count").default(0).notNull(),
79136bbClaude60 issueCount: integer("issue_count").default(0).notNull(),
fc1817aClaude61 },
62 (table) => [uniqueIndex("repos_owner_name").on(table.ownerId, table.name)]
63);
64
3ef4c9dClaude65/**
66 * Per-repository gate + auto-repair configuration.
67 * Every new repo is created with all gates ENABLED by default —
68 * the "full green ecosystem" default. Owners can manually opt-out per setting.
69 */
70export const repoSettings = pgTable("repo_settings", {
71 id: uuid("id").primaryKey().defaultRandom(),
72 repositoryId: uuid("repository_id")
73 .notNull()
74 .unique()
75 .references(() => repositories.id, { onDelete: "cascade" }),
76 // Gates
77 gateTestEnabled: boolean("gate_test_enabled").default(true).notNull(),
78 aiReviewEnabled: boolean("ai_review_enabled").default(true).notNull(),
79 secretScanEnabled: boolean("secret_scan_enabled").default(true).notNull(),
80 securityScanEnabled: boolean("security_scan_enabled").default(true).notNull(),
81 dependencyScanEnabled: boolean("dependency_scan_enabled").default(true).notNull(),
82 lintEnabled: boolean("lint_enabled").default(true).notNull(),
83 typeCheckEnabled: boolean("type_check_enabled").default(true).notNull(),
84 testEnabled: boolean("test_enabled").default(true).notNull(),
85 // Auto-repair
86 autoFixEnabled: boolean("auto_fix_enabled").default(true).notNull(),
87 autoMergeResolveEnabled: boolean("auto_merge_resolve_enabled").default(true).notNull(),
88 autoFormatEnabled: boolean("auto_format_enabled").default(true).notNull(),
89 // AI features
90 aiCommitMessagesEnabled: boolean("ai_commit_messages_enabled").default(true).notNull(),
91 aiPrSummaryEnabled: boolean("ai_pr_summary_enabled").default(true).notNull(),
92 aiChangelogEnabled: boolean("ai_changelog_enabled").default(true).notNull(),
93 // Deploy
94 autoDeployEnabled: boolean("auto_deploy_enabled").default(true).notNull(),
95 deployRequireAllGreen: boolean("deploy_require_all_green").default(true).notNull(),
96 createdAt: timestamp("created_at").defaultNow().notNull(),
97 updatedAt: timestamp("updated_at").defaultNow().notNull(),
98});
99
100/**
101 * Branch protection rules — enforced on push and merge.
102 * Every repo's default branch gets a protection rule on creation.
103 */
104export const branchProtection = pgTable(
105 "branch_protection",
106 {
107 id: uuid("id").primaryKey().defaultRandom(),
108 repositoryId: uuid("repository_id")
109 .notNull()
110 .references(() => repositories.id, { onDelete: "cascade" }),
111 pattern: text("pattern").notNull(), // branch name or glob (e.g. "main", "release/*")
112 requirePullRequest: boolean("require_pull_request").default(true).notNull(),
113 requireGreenGates: boolean("require_green_gates").default(true).notNull(),
114 requireAiApproval: boolean("require_ai_approval").default(true).notNull(),
115 requireHumanReview: boolean("require_human_review").default(false).notNull(),
116 requiredApprovals: integer("required_approvals").default(0).notNull(),
117 allowForcePush: boolean("allow_force_push").default(false).notNull(),
118 allowDeletion: boolean("allow_deletion").default(false).notNull(),
119 dismissStaleReviews: boolean("dismiss_stale_reviews").default(true).notNull(),
120 createdAt: timestamp("created_at").defaultNow().notNull(),
121 updatedAt: timestamp("updated_at").defaultNow().notNull(),
122 },
123 (table) => [
124 uniqueIndex("branch_protection_repo_pattern").on(
125 table.repositoryId,
126 table.pattern
127 ),
128 ]
129);
130
131/**
132 * Gate run history. Every push + every PR creates gate_runs entries —
133 * one per configured gate. Serves as the source of truth for "is this green?".
134 */
135export const gateRuns = pgTable(
136 "gate_runs",
137 {
138 id: uuid("id").primaryKey().defaultRandom(),
139 repositoryId: uuid("repository_id")
140 .notNull()
141 .references(() => repositories.id, { onDelete: "cascade" }),
142 pullRequestId: uuid("pull_request_id").references(() => pullRequests.id, {
143 onDelete: "cascade",
144 }),
145 commitSha: text("commit_sha").notNull(),
146 ref: text("ref").notNull(),
147 gateName: text("gate_name").notNull(), // e.g. "GateTest", "AI Review", "Secret Scan", "Type Check"
148 status: text("status").notNull(), // pending, running, passed, failed, skipped, repaired
149 summary: text("summary"),
150 details: text("details"), // JSON: per-check output, affected files, etc
151 repairAttempted: boolean("repair_attempted").default(false).notNull(),
152 repairSucceeded: boolean("repair_succeeded").default(false).notNull(),
153 repairCommitSha: text("repair_commit_sha"),
154 durationMs: integer("duration_ms"),
155 createdAt: timestamp("created_at").defaultNow().notNull(),
156 completedAt: timestamp("completed_at"),
157 },
158 (table) => [
159 index("gate_runs_repo_sha").on(table.repositoryId, table.commitSha),
160 index("gate_runs_pr").on(table.pullRequestId),
161 index("gate_runs_created").on(table.createdAt),
162 ]
163);
164
165/**
166 * In-app notifications. Powered by the activity feed + explicit mentions.
167 */
168export const notifications = pgTable(
169 "notifications",
170 {
171 id: uuid("id").primaryKey().defaultRandom(),
172 userId: uuid("user_id")
173 .notNull()
174 .references(() => users.id, { onDelete: "cascade" }),
175 repositoryId: uuid("repository_id").references(() => repositories.id, {
176 onDelete: "cascade",
177 }),
178 kind: text("kind").notNull(), // mention, review_requested, pr_merged, pr_closed, gate_failed, gate_repaired, ai_review, assigned, security_alert, deploy_success, deploy_failed
179 title: text("title").notNull(),
180 body: text("body"),
181 url: text("url"), // link to the relevant page
182 readAt: timestamp("read_at"),
183 createdAt: timestamp("created_at").defaultNow().notNull(),
184 },
185 (table) => [
186 index("notifications_user_unread").on(table.userId, table.readAt),
187 index("notifications_user_created").on(table.userId, table.createdAt),
188 ]
189);
190
191/**
192 * Releases — named snapshots of a repo at a tag/commit.
193 * AI-generated changelogs bundled in notes field.
194 */
195export const releases = pgTable(
196 "releases",
197 {
198 id: uuid("id").primaryKey().defaultRandom(),
199 repositoryId: uuid("repository_id")
200 .notNull()
201 .references(() => repositories.id, { onDelete: "cascade" }),
202 authorId: uuid("author_id")
203 .notNull()
204 .references(() => users.id),
205 tag: text("tag").notNull(),
206 name: text("name").notNull(),
207 body: text("body"), // AI-generated release notes + changelog
208 targetCommit: text("target_commit").notNull(),
209 isDraft: boolean("is_draft").default(false).notNull(),
210 isPrerelease: boolean("is_prerelease").default(false).notNull(),
211 createdAt: timestamp("created_at").defaultNow().notNull(),
212 publishedAt: timestamp("published_at"),
213 },
214 (table) => [
215 uniqueIndex("releases_repo_tag").on(table.repositoryId, table.tag),
216 ]
217);
218
219/**
220 * Milestones — group issues + PRs toward a shared goal.
221 */
222export const milestones = pgTable(
223 "milestones",
224 {
225 id: uuid("id").primaryKey().defaultRandom(),
226 repositoryId: uuid("repository_id")
227 .notNull()
228 .references(() => repositories.id, { onDelete: "cascade" }),
229 title: text("title").notNull(),
230 description: text("description"),
231 state: text("state").notNull().default("open"), // open, closed
232 dueDate: timestamp("due_date"),
233 createdAt: timestamp("created_at").defaultNow().notNull(),
234 closedAt: timestamp("closed_at"),
235 },
236 (table) => [index("milestones_repo_state").on(table.repositoryId, table.state)]
237);
238
239/**
240 * Reactions on issues, PRs, and comments. Universal target pointer.
241 */
242export const reactions = pgTable(
243 "reactions",
244 {
245 id: uuid("id").primaryKey().defaultRandom(),
246 userId: uuid("user_id")
247 .notNull()
248 .references(() => users.id, { onDelete: "cascade" }),
249 targetType: text("target_type").notNull(), // issue, pr, issue_comment, pr_comment
250 targetId: uuid("target_id").notNull(),
251 emoji: text("emoji").notNull(), // thumbs_up, thumbs_down, rocket, heart, eyes, laugh, hooray, confused
252 createdAt: timestamp("created_at").defaultNow().notNull(),
253 },
254 (table) => [
255 uniqueIndex("reactions_unique").on(
256 table.userId,
257 table.targetType,
258 table.targetId,
259 table.emoji
260 ),
261 index("reactions_target").on(table.targetType, table.targetId),
262 ]
263);
264
265/**
266 * PR reviews (formal approve/request-changes).
267 * Separate from inline comments in pr_comments.
268 */
269export const prReviews = pgTable(
270 "pr_reviews",
271 {
272 id: uuid("id").primaryKey().defaultRandom(),
273 pullRequestId: uuid("pull_request_id")
274 .notNull()
275 .references(() => pullRequests.id, { onDelete: "cascade" }),
276 reviewerId: uuid("reviewer_id")
277 .notNull()
278 .references(() => users.id),
279 state: text("state").notNull(), // approved, changes_requested, commented
280 body: text("body"),
281 isAi: boolean("is_ai").default(false).notNull(),
282 createdAt: timestamp("created_at").defaultNow().notNull(),
283 },
284 (table) => [index("pr_reviews_pr").on(table.pullRequestId)]
285);
286
287/**
288 * Code owners — who owns which paths (auto-request review on PR).
289 * Parsed from a CODEOWNERS file at the root of the default branch.
290 */
291export const codeOwners = pgTable(
292 "code_owners",
293 {
294 id: uuid("id").primaryKey().defaultRandom(),
295 repositoryId: uuid("repository_id")
296 .notNull()
297 .references(() => repositories.id, { onDelete: "cascade" }),
298 pathPattern: text("path_pattern").notNull(),
299 ownerUsernames: text("owner_usernames").notNull(), // comma-separated
300 createdAt: timestamp("created_at").defaultNow().notNull(),
301 },
302 (table) => [index("code_owners_repo").on(table.repositoryId)]
303);
304
305/**
306 * Per-repo AI chat sessions — conversational repo assistant.
307 */
308export const aiChats = pgTable(
309 "ai_chats",
310 {
311 id: uuid("id").primaryKey().defaultRandom(),
312 userId: uuid("user_id")
313 .notNull()
314 .references(() => users.id, { onDelete: "cascade" }),
315 repositoryId: uuid("repository_id").references(() => repositories.id, {
316 onDelete: "cascade",
317 }),
318 title: text("title"),
319 messages: text("messages").notNull().default("[]"), // JSON array of {role, content}
320 createdAt: timestamp("created_at").defaultNow().notNull(),
321 updatedAt: timestamp("updated_at").defaultNow().notNull(),
322 },
323 (table) => [
324 index("ai_chats_user").on(table.userId),
325 index("ai_chats_repo").on(table.repositoryId),
326 ]
327);
328
329/**
330 * Audit log — every sensitive action. Who did what, when, from where.
331 */
332export const auditLog = pgTable(
333 "audit_log",
334 {
335 id: uuid("id").primaryKey().defaultRandom(),
336 userId: uuid("user_id").references(() => users.id, { onDelete: "set null" }),
337 repositoryId: uuid("repository_id").references(() => repositories.id, {
338 onDelete: "set null",
339 }),
340 action: text("action").notNull(), // repo.create, repo.delete, repo.transfer, token.create, token.revoke, merge, force_push, branch_protection.update, deploy, ...
341 targetType: text("target_type"),
342 targetId: text("target_id"),
343 ip: text("ip"),
344 userAgent: text("user_agent"),
345 metadata: text("metadata"), // JSON for extra context
346 createdAt: timestamp("created_at").defaultNow().notNull(),
347 },
348 (table) => [
349 index("audit_log_user").on(table.userId),
350 index("audit_log_repo").on(table.repositoryId),
351 index("audit_log_created").on(table.createdAt),
352 ]
353);
354
355/**
356 * Deployments — tracks every deploy to downstream systems (Crontech, etc).
357 * Each deploy is gated on ALL green gates passing.
358 */
359export const deployments = pgTable(
360 "deployments",
361 {
362 id: uuid("id").primaryKey().defaultRandom(),
363 repositoryId: uuid("repository_id")
364 .notNull()
365 .references(() => repositories.id, { onDelete: "cascade" }),
366 environment: text("environment").notNull().default("production"),
367 commitSha: text("commit_sha").notNull(),
368 ref: text("ref").notNull(),
369 status: text("status").notNull(), // pending, running, success, failed, blocked
370 blockedReason: text("blocked_reason"),
371 target: text("target"), // e.g. "crontech", "fly.io"
372 triggeredBy: uuid("triggered_by").references(() => users.id),
373 createdAt: timestamp("created_at").defaultNow().notNull(),
374 completedAt: timestamp("completed_at"),
375 },
376 (table) => [
377 index("deployments_repo").on(table.repositoryId),
378 index("deployments_created").on(table.createdAt),
379 ]
380);
381
382/**
383 * Rate-limit buckets — in-memory or persisted counter per IP / token / route.
384 */
385export const rateLimitBuckets = pgTable(
386 "rate_limit_buckets",
387 {
388 id: uuid("id").primaryKey().defaultRandom(),
389 bucketKey: text("bucket_key").notNull().unique(), // "ip:1.2.3.4:api" or "token:abc:api"
390 count: integer("count").default(0).notNull(),
391 windowStart: timestamp("window_start").defaultNow().notNull(),
392 expiresAt: timestamp("expires_at").notNull(),
393 },
394 (table) => [index("rate_limit_expires").on(table.expiresAt)]
395);
396
06d5ffeClaude397export const stars = pgTable(
398 "stars",
399 {
400 id: uuid("id").primaryKey().defaultRandom(),
401 userId: uuid("user_id")
402 .notNull()
403 .references(() => users.id, { onDelete: "cascade" }),
404 repositoryId: uuid("repository_id")
405 .notNull()
406 .references(() => repositories.id, { onDelete: "cascade" }),
407 createdAt: timestamp("created_at").defaultNow().notNull(),
408 },
79136bbClaude409 (table) => [
410 uniqueIndex("stars_user_repo").on(table.userId, table.repositoryId),
411 ]
412);
413
414export const issues = pgTable(
415 "issues",
416 {
417 id: uuid("id").primaryKey().defaultRandom(),
418 number: serial("number"),
419 repositoryId: uuid("repository_id")
420 .notNull()
421 .references(() => repositories.id, { onDelete: "cascade" }),
422 authorId: uuid("author_id")
423 .notNull()
424 .references(() => users.id),
425 title: text("title").notNull(),
426 body: text("body"),
427 state: text("state").notNull().default("open"), // open, closed
428 createdAt: timestamp("created_at").defaultNow().notNull(),
429 updatedAt: timestamp("updated_at").defaultNow().notNull(),
430 closedAt: timestamp("closed_at"),
431 },
432 (table) => [
433 index("issues_repo_state").on(table.repositoryId, table.state),
434 index("issues_repo_number").on(table.repositoryId, table.number),
435 ]
436);
437
438export const issueComments = pgTable(
439 "issue_comments",
440 {
441 id: uuid("id").primaryKey().defaultRandom(),
442 issueId: uuid("issue_id")
443 .notNull()
444 .references(() => issues.id, { onDelete: "cascade" }),
445 authorId: uuid("author_id")
446 .notNull()
447 .references(() => users.id),
448 body: text("body").notNull(),
449 createdAt: timestamp("created_at").defaultNow().notNull(),
450 updatedAt: timestamp("updated_at").defaultNow().notNull(),
451 },
452 (table) => [index("comments_issue").on(table.issueId)]
453);
454
455export const labels = pgTable(
456 "labels",
457 {
458 id: uuid("id").primaryKey().defaultRandom(),
459 repositoryId: uuid("repository_id")
460 .notNull()
461 .references(() => repositories.id, { onDelete: "cascade" }),
462 name: text("name").notNull(),
463 color: text("color").notNull().default("#8b949e"),
464 description: text("description"),
465 },
466 (table) => [
467 uniqueIndex("labels_repo_name").on(table.repositoryId, table.name),
468 ]
469);
470
471export const issueLabels = pgTable(
472 "issue_labels",
473 {
474 id: uuid("id").primaryKey().defaultRandom(),
475 issueId: uuid("issue_id")
476 .notNull()
477 .references(() => issues.id, { onDelete: "cascade" }),
478 labelId: uuid("label_id")
479 .notNull()
480 .references(() => labels.id, { onDelete: "cascade" }),
481 },
482 (table) => [
483 uniqueIndex("issue_labels_unique").on(table.issueId, table.labelId),
484 ]
06d5ffeClaude485);
486
0074234Claude487export const pullRequests = pgTable(
488 "pull_requests",
489 {
490 id: uuid("id").primaryKey().defaultRandom(),
491 number: serial("number"),
492 repositoryId: uuid("repository_id")
493 .notNull()
494 .references(() => repositories.id, { onDelete: "cascade" }),
495 authorId: uuid("author_id")
496 .notNull()
497 .references(() => users.id),
498 title: text("title").notNull(),
499 body: text("body"),
500 state: text("state").notNull().default("open"), // open, closed, merged
501 baseBranch: text("base_branch").notNull(),
502 headBranch: text("head_branch").notNull(),
3ef4c9dClaude503 isDraft: boolean("is_draft").default(false).notNull(),
504 mergeStrategy: text("merge_strategy").default("merge").notNull(), // merge, squash, rebase
505 milestoneId: uuid("milestone_id"),
0074234Claude506 mergedAt: timestamp("merged_at"),
507 mergedBy: uuid("merged_by").references(() => users.id),
508 createdAt: timestamp("created_at").defaultNow().notNull(),
509 updatedAt: timestamp("updated_at").defaultNow().notNull(),
510 closedAt: timestamp("closed_at"),
511 },
512 (table) => [
513 index("prs_repo_state").on(table.repositoryId, table.state),
514 index("prs_repo_number").on(table.repositoryId, table.number),
515 ]
516);
517
518export const prComments = pgTable(
519 "pr_comments",
520 {
521 id: uuid("id").primaryKey().defaultRandom(),
522 pullRequestId: uuid("pull_request_id")
523 .notNull()
524 .references(() => pullRequests.id, { onDelete: "cascade" }),
525 authorId: uuid("author_id")
526 .notNull()
527 .references(() => users.id),
528 body: text("body").notNull(),
529 isAiReview: boolean("is_ai_review").default(false).notNull(),
530 filePath: text("file_path"),
531 lineNumber: integer("line_number"),
532 createdAt: timestamp("created_at").defaultNow().notNull(),
533 updatedAt: timestamp("updated_at").defaultNow().notNull(),
534 },
535 (table) => [index("pr_comments_pr").on(table.pullRequestId)]
536);
537
538export const activityFeed = pgTable(
539 "activity_feed",
540 {
541 id: uuid("id").primaryKey().defaultRandom(),
542 repositoryId: uuid("repository_id")
543 .notNull()
544 .references(() => repositories.id, { onDelete: "cascade" }),
545 userId: uuid("user_id").references(() => users.id),
546 action: text("action").notNull(), // push, issue_open, issue_close, pr_open, pr_merge, star, comment
547 targetType: text("target_type"), // issue, pr, commit
548 targetId: text("target_id"),
549 metadata: text("metadata"), // JSON string for extra data
550 createdAt: timestamp("created_at").defaultNow().notNull(),
551 },
552 (table) => [
553 index("activity_repo").on(table.repositoryId),
554 index("activity_user").on(table.userId),
555 ]
556);
557
c81ab7aClaude558export const webhooks = pgTable(
559 "webhooks",
560 {
561 id: uuid("id").primaryKey().defaultRandom(),
562 repositoryId: uuid("repository_id")
563 .notNull()
564 .references(() => repositories.id, { onDelete: "cascade" }),
565 url: text("url").notNull(),
566 secret: text("secret"),
567 events: text("events").notNull().default("push"), // comma-separated: push,issue,pr
568 isActive: boolean("is_active").default(true).notNull(),
569 lastDeliveredAt: timestamp("last_delivered_at"),
570 lastStatus: integer("last_status"),
571 createdAt: timestamp("created_at").defaultNow().notNull(),
572 },
573 (table) => [index("webhooks_repo").on(table.repositoryId)]
574);
575
576export const apiTokens = pgTable("api_tokens", {
577 id: uuid("id").primaryKey().defaultRandom(),
578 userId: uuid("user_id")
579 .notNull()
580 .references(() => users.id, { onDelete: "cascade" }),
581 name: text("name").notNull(),
582 tokenHash: text("token_hash").notNull(),
583 tokenPrefix: text("token_prefix").notNull(), // first 8 chars for display
584 scopes: text("scopes").notNull().default("repo"), // comma-separated
585 lastUsedAt: timestamp("last_used_at"),
586 expiresAt: timestamp("expires_at"),
587 createdAt: timestamp("created_at").defaultNow().notNull(),
588});
589
590export const repoTopics = pgTable(
591 "repo_topics",
592 {
593 id: uuid("id").primaryKey().defaultRandom(),
594 repositoryId: uuid("repository_id")
595 .notNull()
596 .references(() => repositories.id, { onDelete: "cascade" }),
597 topic: text("topic").notNull(),
598 },
599 (table) => [
600 uniqueIndex("repo_topics_unique").on(table.repositoryId, table.topic),
601 index("topics_name").on(table.topic),
602 ]
603);
604
fc1817aClaude605export const sshKeys = pgTable("ssh_keys", {
606 id: uuid("id").primaryKey().defaultRandom(),
607 userId: uuid("user_id")
608 .notNull()
06d5ffeClaude609 .references(() => users.id, { onDelete: "cascade" }),
fc1817aClaude610 title: text("title").notNull(),
611 fingerprint: text("fingerprint").notNull(),
612 publicKey: text("public_key").notNull(),
613 lastUsedAt: timestamp("last_used_at"),
614 createdAt: timestamp("created_at").defaultNow().notNull(),
615});
616
617export type User = typeof users.$inferSelect;
618export type NewUser = typeof users.$inferInsert;
619export type Repository = typeof repositories.$inferSelect;
620export type NewRepository = typeof repositories.$inferInsert;
06d5ffeClaude621export type Session = typeof sessions.$inferSelect;
622export type Star = typeof stars.$inferSelect;
623export type SshKey = typeof sshKeys.$inferSelect;
79136bbClaude624export type Issue = typeof issues.$inferSelect;
625export type IssueComment = typeof issueComments.$inferSelect;
626export type Label = typeof labels.$inferSelect;
0074234Claude627export type PullRequest = typeof pullRequests.$inferSelect;
628export type PrComment = typeof prComments.$inferSelect;
629export type ActivityEntry = typeof activityFeed.$inferSelect;
c81ab7aClaude630export type Webhook = typeof webhooks.$inferSelect;
631export type ApiToken = typeof apiTokens.$inferSelect;
632export type RepoTopic = typeof repoTopics.$inferSelect;
3ef4c9dClaude633export type RepoSettings = typeof repoSettings.$inferSelect;
634export type BranchProtection = typeof branchProtection.$inferSelect;
635export type GateRun = typeof gateRuns.$inferSelect;
636export type Notification = typeof notifications.$inferSelect;
637export type Release = typeof releases.$inferSelect;
638export type Milestone = typeof milestones.$inferSelect;
639export type Reaction = typeof reactions.$inferSelect;
640export type PrReview = typeof prReviews.$inferSelect;
641export type CodeOwner = typeof codeOwners.$inferSelect;
642export type AiChat = typeof aiChats.$inferSelect;
643export type AuditLogEntry = typeof auditLog.$inferSelect;
644export type Deployment = typeof deployments.$inferSelect;
24cf2caClaude645
646/**
647 * Saved replies — per-user canned responses, insertable into any
648 * issue / PR comment textarea. Shortcut name must be unique per user.
649 */
650export const savedReplies = pgTable(
651 "saved_replies",
652 {
653 id: uuid("id").primaryKey().defaultRandom(),
654 userId: uuid("user_id")
655 .notNull()
656 .references(() => users.id, { onDelete: "cascade" }),
657 shortcut: text("shortcut").notNull(),
658 body: text("body").notNull(),
659 createdAt: timestamp("created_at").defaultNow().notNull(),
660 updatedAt: timestamp("updated_at").defaultNow().notNull(),
661 },
662 (table) => [
663 uniqueIndex("saved_replies_user_shortcut").on(table.userId, table.shortcut),
664 ]
665);
666
667export type SavedReply = typeof savedReplies.$inferSelect;
5cc5d95Claude668
669/**
670 * Organizations (Block B1) — multi-user namespaces. Distinct from `users`.
671 * An org has members (with org-level roles) and may contain teams.
672 * Repos can be owned by an org via `repositories.orgId` (added in Block B2).
673 *
674 * Slug is globally unique against itself; collision with a username is
675 * checked at create time in the route handler (no DB-level cross-table
676 * uniqueness in Postgres).
677 */
678export const organizations = pgTable("organizations", {
679 id: uuid("id").primaryKey().defaultRandom(),
680 slug: text("slug").notNull().unique(),
681 name: text("name").notNull(),
682 description: text("description"),
683 avatarUrl: text("avatar_url"),
684 billingEmail: text("billing_email"),
685 createdById: uuid("created_by_id")
686 .notNull()
687 .references(() => users.id, { onDelete: "restrict" }),
688 createdAt: timestamp("created_at").defaultNow().notNull(),
689 updatedAt: timestamp("updated_at").defaultNow().notNull(),
690});
691
692/**
693 * Org membership. Roles: owner (full control, billing), admin (manage
694 * members + teams + repos), member (default; can be added to teams).
695 */
696export const orgMembers = pgTable(
697 "org_members",
698 {
699 id: uuid("id").primaryKey().defaultRandom(),
700 orgId: uuid("org_id")
701 .notNull()
702 .references(() => organizations.id, { onDelete: "cascade" }),
703 userId: uuid("user_id")
704 .notNull()
705 .references(() => users.id, { onDelete: "cascade" }),
706 role: text("role").notNull().default("member"), // owner | admin | member
707 createdAt: timestamp("created_at").defaultNow().notNull(),
708 },
709 (table) => [
710 uniqueIndex("org_members_unique").on(table.orgId, table.userId),
711 index("org_members_user").on(table.userId),
712 ]
713);
714
715/**
716 * Teams within an org. Slug is unique within an org.
717 * `parentTeamId` allows nesting (GitHub-style child teams). Optional.
718 */
719export const teams = pgTable(
720 "teams",
721 {
722 id: uuid("id").primaryKey().defaultRandom(),
723 orgId: uuid("org_id")
724 .notNull()
725 .references(() => organizations.id, { onDelete: "cascade" }),
726 slug: text("slug").notNull(),
727 name: text("name").notNull(),
728 description: text("description"),
729 parentTeamId: uuid("parent_team_id"),
730 createdAt: timestamp("created_at").defaultNow().notNull(),
731 updatedAt: timestamp("updated_at").defaultNow().notNull(),
732 },
733 (table) => [uniqueIndex("teams_org_slug").on(table.orgId, table.slug)]
734);
735
736/**
737 * Team membership. Roles: maintainer (can edit team), member (default).
738 * A user can belong to many teams; team membership requires org membership
739 * but that invariant is enforced at the route layer, not the DB layer.
740 */
741export const teamMembers = pgTable(
742 "team_members",
743 {
744 id: uuid("id").primaryKey().defaultRandom(),
745 teamId: uuid("team_id")
746 .notNull()
747 .references(() => teams.id, { onDelete: "cascade" }),
748 userId: uuid("user_id")
749 .notNull()
750 .references(() => users.id, { onDelete: "cascade" }),
751 role: text("role").notNull().default("member"), // maintainer | member
752 createdAt: timestamp("created_at").defaultNow().notNull(),
753 },
754 (table) => [
755 uniqueIndex("team_members_unique").on(table.teamId, table.userId),
756 index("team_members_user").on(table.userId),
757 ]
758);
759
760export type Organization = typeof organizations.$inferSelect;
761export type OrgMember = typeof orgMembers.$inferSelect;
762export type Team = typeof teams.$inferSelect;
763export type TeamMember = typeof teamMembers.$inferSelect;
764export type OrgRole = "owner" | "admin" | "member";
765export type TeamRole = "maintainer" | "member";