import { sql } from "drizzle-orm"; import { pgTable, text, timestamp, integer, jsonb, uniqueIndex, index, primaryKey, } from "drizzle-orm/pg-core"; // Users are mirrored from Clerk on first sign-in. // id = Clerk user id (e.g., "user_2abc..."), email is the canonical Clerk email. export const users = pgTable( "users", { id: text("id").primaryKey(), email: text("email").notNull(), displayName: text("display_name"), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }) .defaultNow() .notNull(), }, (t) => ({ emailIdx: uniqueIndex("users_email_idx").on(t.email), }), ); // One per user. Tracks the user's unified agent's container stack + Git repo. // Per changes.md §2A: per-user Gitea containers removed; central Gitea shared. // Per changes.md §5: ONE actor per user manages the full orchestration layer. export const userStacks = pgTable( "user_stacks", { userId: text("user_id") .primaryKey() .references(() => users.id, { onDelete: "cascade" }), status: text("status", { enum: ["provisioning", "running", "stopped", "error"], }) .notNull() .default("provisioning"), // Central Gitea (shared org-wide, changes.md §2A). giteaRepoName: text("gitea_repo_name"), giteaRepoOwner: text("gitea_repo_owner"), // Per-user OpenCode container (from shared image, changes.md §3). opencodeContainerId: text("opencode_container_id"), opencodeContainerName: text("opencode_container_name"), opencodeHost: text("opencode_host"), opencodePort: integer("opencode_port"), opencodePassword: text("opencode_password"), workspacePath: text("workspace_path"), // Version tracking for image rollouts (changes.md §9). imageVersion: text("image_version"), migrationVersion: text("migration_version"), promptVersion: text("prompt_version"), lastError: text("last_error"), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }) .defaultNow() .notNull(), }, (t) => ({ statusIdx: index("user_stacks_status_idx").on(t.status), }), ); // Per changes.md §5: ONE unified actor per user (no separate grow/sub actors). // The actor manages: infra state, git state, runtime comms, migrations, API orchestration. export const actors = pgTable( "actors", { actorId: text("actor_id").notNull(), userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), kind: text("kind", { enum: ["user"] }).notNull().default("user"), status: text("status", { enum: ["idle", "running", "done", "error"], }) .notNull() .default("idle"), lastActivityAt: timestamp("last_activity_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), }, (t) => ({ pk: primaryKey({ columns: [t.userId, t.actorId] }), kindIdx: index("actors_user_kind_idx").on(t.userId, t.kind), }), ); // Per-user repo registry (in addition to the primary memory repo). export const repos = pgTable( "repos", { id: text("id").primaryKey(), // `${userId}:${name}` userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), name: text("name").notNull(), role: text("role", { enum: ["memory", "project"] }).notNull(), giteaOwner: text("gitea_owner").notNull(), giteaName: text("gitea_name").notNull(), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), }, (t) => ({ userRoleIdx: index("repos_user_role_idx").on(t.userId, t.role), }), ); // OpenCode sessions opened by sub-agents. export const opencodeSessions = pgTable( "opencode_sessions", { id: text("id").primaryKey(), // OpenCode session id from POST /session userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), actorId: text("actor_id"), title: text("title"), parentId: text("parent_id"), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), }, (t) => ({ userIdx: index("opencode_sessions_user_idx").on(t.userId), }), ); // Audit/event log — small append-only stream used for debugging + the // frontend's "task progress timeline" until we move it to Rivet streams only. export const events = pgTable( "events", { id: text("id") .primaryKey() .default(sql`gen_random_uuid()::text`), userId: text("user_id").notNull(), actorId: text("actor_id"), type: text("type").notNull(), payload: jsonb("payload").$type>(), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), }, (t) => ({ userIdx: index("events_user_idx").on(t.userId, t.createdAt), }), ); export type User = typeof users.$inferSelect; export type NewUser = typeof users.$inferInsert; export type UserStack = typeof userStacks.$inferSelect; export type NewUserStack = typeof userStacks.$inferInsert; export type ActorRow = typeof actors.$inferSelect; export type RepoRow = typeof repos.$inferSelect; export type OpencodeSessionRow = typeof opencodeSessions.$inferSelect;