TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1/**2 * Workspace tables added by the 2026-09 "AI operating system" upgrade:3 * projects (workspaces), project files (reusable context library), prompt library with variables,4 * user model labels, custom OpenAI-compatible endpoints and Arena votes/criteria.5 *6 * Kept in a separate file so parallel feature work never edits `schema.ts` concurrently.7 * `drizzle.config.ts` globs `schema*.ts`; `src/db/index.ts` re-exports everything.8 */9import { pgTable, text, timestamp, boolean, integer, jsonb, index, uniqueIndex, primaryKey } from "drizzle-orm/pg-core";10import { sql } from "drizzle-orm";11import { users } from "./schema";1213const ts = (name: string) => timestamp(name, { withTimezone: true, mode: "date" });14const now = () => sql`now()`;1516// ---------------------------------------------------------------------------17// Projects / workspaces18// ---------------------------------------------------------------------------19export const projects = pgTable(20 "projects",21 {22 id: text("id").primaryKey(),23 userId: text("user_id")24 .notNull()25 .references(() => users.id, { onDelete: "cascade" }),26 name: text("name").notNull(),27 description: text("description"),28 /** Emoji or lucide icon name. */29 icon: text("icon"),30 color: text("color"),31 /** Default system instructions prepended to every chat in the project. */32 instructions: text("instructions"),33 /** Preferred model keys (first = default for new chats). */34 preferredModelKeys: jsonb("preferred_model_keys").$type<string[]>().notNull().default([]),35 /** Default generation settings for new chats. */36 defaultSettings: jsonb("default_settings").$type<Record<string, unknown>>().notNull().default({}),37 /** Free-form markdown notes. */38 notes: text("notes"),39 archived: boolean("archived").notNull().default(false),40 sortOrder: integer("sort_order").notNull().default(0),41 createdAt: ts("created_at").notNull().default(now()),42 updatedAt: ts("updated_at").notNull().default(now()),43 },44 (t) => [index("projects_user_idx").on(t.userId)],45);4647/** Reusable files saved to a project (or to the user's global library when projectId is null). */48export const projectFiles = pgTable(49 "project_files",50 {51 id: text("id").primaryKey(),52 userId: text("user_id")53 .notNull()54 .references(() => users.id, { onDelete: "cascade" }),55 projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),56 kind: text("kind").notNull(), // image | pdf | text | code | csv | json | document57 name: text("name").notNull(),58 mimeType: text("mime_type").notNull(),59 sizeBytes: integer("size_bytes").notNull(),60 /** Base64 payload (same storage strategy as message_attachments). */61 dataBase64: text("data_base64").notNull(),62 width: integer("width"),63 height: integer("height"),64 /** Heuristic token estimate computed at upload. */65 estimatedTokens: integer("estimated_tokens"),66 /** Optional user description used when injecting into prompts. */67 description: text("description"),68 createdAt: ts("created_at").notNull().default(now()),69 },70 (t) => [index("project_files_user_idx").on(t.userId), index("project_files_project_idx").on(t.projectId)],71);7273// ---------------------------------------------------------------------------74// Prompt library (templates with {{variables}})75// ---------------------------------------------------------------------------76export const prompts = pgTable(77 "prompts",78 {79 id: text("id").primaryKey(),80 userId: text("user_id")81 .notNull()82 .references(() => users.id, { onDelete: "cascade" }),83 projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),84 /** system | user | template | structured */85 kind: text("kind").notNull().default("user"),86 name: text("name").notNull(),87 description: text("description"),88 /** Body with `{{variable}}` placeholders. */89 content: text("content").notNull(),90 /** Declared variables (parsed from content, editable defaults). */91 variables: jsonb("variables").$type<{ name: string; label?: string; default?: string; required?: boolean; options?: string[] }[]>().notNull().default([]),92 /** For `structured`: the JSON schema to request. */93 schema: jsonb("schema").$type<Record<string, unknown> | null>(),94 folder: text("folder"),95 tags: jsonb("tags").$type<string[]>().notNull().default([]),96 favorite: boolean("favorite").notNull().default(false),97 defaultModelKey: text("default_model_key"),98 uses: integer("uses").notNull().default(0),99 lastUsedAt: ts("last_used_at"),100 createdAt: ts("created_at").notNull().default(now()),101 updatedAt: ts("updated_at").notNull().default(now()),102 },103 (t) => [index("prompts_user_idx").on(t.userId), index("prompts_user_folder_idx").on(t.userId, t.folder)],104);105106// ---------------------------------------------------------------------------107// Model intelligence — user labels & parameter presets scoped to capability sets108// ---------------------------------------------------------------------------109export const userModelLabels = pgTable(110 "user_model_labels",111 {112 userId: text("user_id")113 .notNull()114 .references(() => users.id, { onDelete: "cascade" }),115 modelKey: text("model_key").notNull(),116 label: text("label").notNull(),117 updatedAt: ts("updated_at").notNull().default(now()),118 },119 (t) => [primaryKey({ columns: [t.userId, t.modelKey] })],120);121122// ---------------------------------------------------------------------------123// Custom OpenAI-compatible endpoints (Ollama, LM Studio, vLLM, llama.cpp, MLX…)124// ---------------------------------------------------------------------------125export const customEndpoints = pgTable(126 "custom_endpoints",127 {128 id: text("id").primaryKey(),129 userId: text("user_id")130 .notNull()131 .references(() => users.id, { onDelete: "cascade" }),132 name: text("name").notNull(),133 /** Base URL up to and including `/v1` (e.g. http://localhost:11434/v1). */134 baseUrl: text("base_url").notNull(),135 /** Optional API key, AES-256-GCM envelope like provider_connections.encrypted_key. */136 encryptedKey: text("encrypted_key"),137 keyHint: text("key_hint"),138 /** Extra headers (values encrypted as a single JSON envelope). */139 encryptedHeaders: text("encrypted_headers"),140 /** Relative path used for discovery, default `/models`. Empty = manual models only. */141 modelsPath: text("models_path").notNull().default("/models"),142 /** Manually declared models when discovery is unavailable. */143 manualModels: jsonb("manual_models").$type<{ id: string; displayName?: string; contextTokens?: number; vision?: boolean; tools?: boolean; reasoning?: boolean }[]>().notNull().default([]),144 status: text("status").notNull().default("unverified"), // unverified | valid | invalid | error145 lastValidatedAt: ts("last_validated_at"),146 lastValidationError: text("last_validation_error"),147 lastLatencyMs: integer("last_latency_ms"),148 modelsAvailable: integer("models_available"),149 /** Snapshot of the last successful discovery (`GET {baseUrl}{modelsPath}`), so /api/models never calls the endpoint. */150 discoveredModels: jsonb("discovered_models").$type<{ id: string; ownedBy?: string; created?: number }[]>().notNull().default([]),151 discoveredAt: ts("discovered_at"),152 createdAt: ts("created_at").notNull().default(now()),153 updatedAt: ts("updated_at").notNull().default(now()),154 },155 (t) => [index("custom_endpoints_user_idx").on(t.userId)],156);157158// ---------------------------------------------------------------------------159// Arena — votes per criterion (ratings on arena_responses stay for compatibility)160// ---------------------------------------------------------------------------161export const arenaVotes = pgTable(162 "arena_votes",163 {164 id: text("id").primaryKey(),165 userId: text("user_id")166 .notNull()167 .references(() => users.id, { onDelete: "cascade" }),168 sessionId: text("session_id").notNull(),169 responseId: text("response_id").notNull(),170 modelKey: text("model_key").notNull(),171 /** best | accurate | writing | coding | value | fastest | custom:<slug> */172 criterion: text("criterion").notNull(),173 /** Task category the prompt was classified into (coding | research | writing | reasoning | general). */174 category: text("category"),175 createdAt: ts("created_at").notNull().default(now()),176 },177 (t) => [uniqueIndex("arena_votes_session_criterion_uq").on(t.sessionId, t.criterion), index("arena_votes_user_model_idx").on(t.userId, t.modelKey)],178);179180// ---------------------------------------------------------------------------181// Public share links for Arena sessions182// ---------------------------------------------------------------------------183export const sharedArenaSessions = pgTable(184 "shared_arena_sessions",185 {186 id: text("id").primaryKey(),187 sessionId: text("session_id").notNull(),188 userId: text("user_id")189 .notNull()190 .references(() => users.id, { onDelete: "cascade" }),191 snapshot: jsonb("snapshot").$type<Record<string, unknown>>().notNull(),192 isPublic: boolean("is_public").notNull().default(true),193 viewCount: integer("view_count").notNull().default(0),194 createdAt: ts("created_at").notNull().default(now()),195 revokedAt: ts("revoked_at"),196 },197 (t) => [index("shared_arena_session_idx").on(t.sessionId)],198);199200export type Project = typeof projects.$inferSelect;201export type ProjectFile = typeof projectFiles.$inferSelect;202export type Prompt = typeof prompts.$inferSelect;203export type CustomEndpoint = typeof customEndpoints.$inferSelect;204export type ArenaVote = typeof arenaVotes.$inferSelect;205