Files
stack/packages/db/src/schema.ts
jason.woltje 39ccba95d0
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
feat: mosaic gateway CLI daemon management + admin token auth (#369)
2026-04-04 18:03:12 +00:00

584 lines
23 KiB
TypeScript

/**
* Unified schema file — all tables defined here.
* drizzle-kit reads this file directly (avoids CJS/ESM extension issues).
*/
import {
pgTable,
text,
timestamp,
boolean,
uuid,
jsonb,
index,
uniqueIndex,
real,
integer,
customType,
} from 'drizzle-orm/pg-core';
// ─── Auth (BetterAuth-compatible) ────────────────────────────────────────────
export const users = pgTable('users', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull().default(false),
image: text('image'),
role: text('role').notNull().default('member'),
banned: boolean('banned').default(false),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const sessions = pgTable(
'sessions',
{
id: text('id').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
token: text('token').notNull().unique(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
// Auth hot path: look up all sessions for a user (BetterAuth session list).
index('sessions_user_id_idx').on(t.userId),
// Session expiry cleanup queries.
index('sessions_expires_at_idx').on(t.expiresAt),
],
);
export const accounts = pgTable(
'accounts',
{
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
// BetterAuth looks up accounts by (provider_id, account_id) on OAuth callback.
index('accounts_provider_account_idx').on(t.providerId, t.accountId),
// Also used in session validation to find linked accounts for a user.
index('accounts_user_id_idx').on(t.userId),
],
);
export const verifications = pgTable('verifications', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
// ─── Admin API Tokens ───────────────────────────────────────────────────────
export const adminTokens = pgTable(
'admin_tokens',
{
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
tokenHash: text('token_hash').notNull(),
label: text('label').notNull(),
scope: text('scope').notNull().default('admin'),
expiresAt: timestamp('expires_at', { withTimezone: true }),
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('admin_tokens_user_id_idx').on(t.userId),
uniqueIndex('admin_tokens_hash_idx').on(t.tokenHash),
],
);
// ─── Teams ───────────────────────────────────────────────────────────────────
// Declared before projects because projects references teams.
export const teams = pgTable('teams', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
ownerId: text('owner_id')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
managerId: text('manager_id')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const teamMembers = pgTable(
'team_members',
{
id: uuid('id').primaryKey().defaultRandom(),
teamId: uuid('team_id')
.notNull()
.references(() => teams.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
role: text('role', { enum: ['manager', 'member'] })
.notNull()
.default('member'),
invitedBy: text('invited_by').references(() => users.id, { onDelete: 'set null' }),
joinedAt: timestamp('joined_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
uniq: uniqueIndex('team_members_team_user_idx').on(t.teamId, t.userId),
}),
);
// ─── Brain ───────────────────────────────────────────────────────────────────
// Declared before Chat because conversations references projects.
export const projects = pgTable('projects', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
description: text('description'),
status: text('status', { enum: ['active', 'paused', 'completed', 'archived'] })
.notNull()
.default('active'),
ownerId: text('owner_id').references(() => users.id, { onDelete: 'set null' }),
teamId: uuid('team_id').references(() => teams.id, { onDelete: 'cascade' }),
ownerType: text('owner_type', { enum: ['user', 'team'] })
.notNull()
.default('user'),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const missions = pgTable(
'missions',
{
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
description: text('description'),
status: text('status', { enum: ['planning', 'active', 'paused', 'completed', 'failed'] })
.notNull()
.default('planning'),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
phase: text('phase'),
milestones: jsonb('milestones').$type<Record<string, unknown>[]>(),
config: jsonb('config'),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('missions_project_id_idx').on(t.projectId),
index('missions_user_id_idx').on(t.userId),
],
);
export const tasks = pgTable(
'tasks',
{
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
description: text('description'),
status: text('status', {
enum: ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'],
})
.notNull()
.default('not-started'),
priority: text('priority', { enum: ['critical', 'high', 'medium', 'low'] })
.notNull()
.default('medium'),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
missionId: uuid('mission_id').references(() => missions.id, { onDelete: 'set null' }),
assignee: text('assignee'),
tags: jsonb('tags').$type<string[]>(),
dueDate: timestamp('due_date', { withTimezone: true }),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('tasks_project_id_idx').on(t.projectId),
index('tasks_mission_id_idx').on(t.missionId),
index('tasks_status_idx').on(t.status),
],
);
// ─── Coord Mission Tasks ─────────────────────────────────────────────────────
// Join table tracking coord-managed tasks within a mission.
// Scoped to userId for multi-tenant RBAC isolation.
export const missionTasks = pgTable(
'mission_tasks',
{
id: uuid('id').primaryKey().defaultRandom(),
missionId: uuid('mission_id')
.notNull()
.references(() => missions.id, { onDelete: 'cascade' }),
taskId: uuid('task_id').references(() => tasks.id, { onDelete: 'set null' }),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
status: text('status', {
enum: ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'],
})
.notNull()
.default('not-started'),
description: text('description'),
notes: text('notes'),
pr: text('pr'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('mission_tasks_mission_id_idx').on(t.missionId),
index('mission_tasks_task_id_idx').on(t.taskId),
index('mission_tasks_user_id_idx').on(t.userId),
index('mission_tasks_status_idx').on(t.status),
],
);
export const events = pgTable(
'events',
{
id: uuid('id').primaryKey().defaultRandom(),
type: text('type').notNull(),
title: text('title').notNull(),
description: text('description'),
date: timestamp('date', { withTimezone: true }).notNull().defaultNow(),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('events_type_idx').on(t.type), index('events_date_idx').on(t.date)],
);
export const agents = pgTable(
'agents',
{
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
provider: text('provider').notNull(),
model: text('model').notNull(),
status: text('status', { enum: ['idle', 'active', 'error', 'offline'] })
.notNull()
.default('idle'),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
ownerId: text('owner_id').references(() => users.id, { onDelete: 'set null' }),
systemPrompt: text('system_prompt'),
allowedTools: jsonb('allowed_tools').$type<string[]>(),
skills: jsonb('skills').$type<string[]>(),
isSystem: boolean('is_system').notNull().default(false),
config: jsonb('config'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('agents_project_id_idx').on(t.projectId),
index('agents_owner_id_idx').on(t.ownerId),
index('agents_is_system_idx').on(t.isSystem),
],
);
export const tickets = pgTable(
'tickets',
{
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
description: text('description'),
status: text('status', { enum: ['open', 'in-progress', 'resolved', 'closed'] })
.notNull()
.default('open'),
priority: text('priority', { enum: ['critical', 'high', 'medium', 'low'] })
.notNull()
.default('medium'),
source: text('source'),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('tickets_status_idx').on(t.status)],
);
export const appreciations = pgTable('appreciations', {
id: uuid('id').primaryKey().defaultRandom(),
fromUser: text('from_user'),
toUser: text('to_user'),
message: text('message').notNull(),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
// ─── Chat ────────────────────────────────────────────────────────────────────
export const conversations = pgTable(
'conversations',
{
id: uuid('id').primaryKey().defaultRandom(),
title: text('title'),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
projectId: uuid('project_id').references(() => projects.id, { onDelete: 'set null' }),
agentId: uuid('agent_id').references(() => agents.id, { onDelete: 'set null' }),
/** M5-004: Agent session ID bound to this conversation. Nullable — set when a session is created. */
sessionId: text('session_id'),
archived: boolean('archived').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
// Compound index for the most common query: conversations for a user filtered by archived.
index('conversations_user_archived_idx').on(t.userId, t.archived),
index('conversations_project_id_idx').on(t.projectId),
index('conversations_agent_id_idx').on(t.agentId),
],
);
export const messages = pgTable(
'messages',
{
id: uuid('id').primaryKey().defaultRandom(),
conversationId: uuid('conversation_id')
.notNull()
.references(() => conversations.id, { onDelete: 'cascade' }),
role: text('role', { enum: ['user', 'assistant', 'system'] }).notNull(),
content: text('content').notNull(),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('messages_conversation_id_idx').on(t.conversationId)],
);
// ─── pgvector custom type ───────────────────────────────────────────────────
const vector = customType<{ data: number[]; driverParam: string; config: { dimensions: number } }>({
dataType(config) {
return `vector(${config?.dimensions ?? 1536})`;
},
fromDriver(value: unknown): number[] {
const str = value as string;
return str
.slice(1, -1)
.split(',')
.map((v) => Number(v));
},
toDriver(value: number[]): string {
return `[${value.join(',')}]`;
},
});
// ─── Memory ─────────────────────────────────────────────────────────────────
export const preferences = pgTable(
'preferences',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
key: text('key').notNull(),
value: jsonb('value').notNull(),
category: text('category', {
enum: ['communication', 'coding', 'workflow', 'appearance', 'general'],
})
.notNull()
.default('general'),
source: text('source'),
mutable: boolean('mutable').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('preferences_user_id_idx').on(t.userId),
// Unique constraint enables single-round-trip INSERT … ON CONFLICT DO UPDATE.
uniqueIndex('preferences_user_key_idx').on(t.userId, t.key),
],
);
export const insights = pgTable(
'insights',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
content: text('content').notNull(),
embedding: vector('embedding', { dimensions: 1536 }),
source: text('source', {
enum: ['agent', 'user', 'summarization', 'system'],
})
.notNull()
.default('agent'),
category: text('category', {
enum: ['decision', 'learning', 'preference', 'fact', 'pattern', 'general'],
})
.notNull()
.default('general'),
relevanceScore: real('relevance_score').notNull().default(1.0),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
decayedAt: timestamp('decayed_at', { withTimezone: true }),
},
(t) => [
index('insights_user_id_idx').on(t.userId),
index('insights_category_idx').on(t.category),
index('insights_relevance_idx').on(t.relevanceScore),
],
);
// ─── Agent Logs ─────────────────────────────────────────────────────────────
export const agentLogs = pgTable(
'agent_logs',
{
id: uuid('id').primaryKey().defaultRandom(),
sessionId: text('session_id').notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
level: text('level', { enum: ['debug', 'info', 'warn', 'error'] })
.notNull()
.default('info'),
category: text('category', {
enum: ['decision', 'tool_use', 'learning', 'error', 'general'],
})
.notNull()
.default('general'),
content: text('content').notNull(),
metadata: jsonb('metadata'),
tier: text('tier', { enum: ['hot', 'warm', 'cold'] })
.notNull()
.default('hot'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
summarizedAt: timestamp('summarized_at', { withTimezone: true }),
archivedAt: timestamp('archived_at', { withTimezone: true }),
},
(t) => [
// Compound index for session log queries (most common: session + tier filter).
index('agent_logs_session_tier_idx').on(t.sessionId, t.tier),
index('agent_logs_user_id_idx').on(t.userId),
// Used by summarization cron to find hot logs older than a cutoff.
index('agent_logs_tier_created_at_idx').on(t.tier, t.createdAt),
],
);
// ─── Skills ─────────────────────────────────────────────────────────────────
export const skills = pgTable(
'skills',
{
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull().unique(),
description: text('description'),
version: text('version'),
source: text('source', { enum: ['builtin', 'community', 'custom'] })
.notNull()
.default('custom'),
config: jsonb('config'),
enabled: boolean('enabled').notNull().default(true),
installedBy: text('installed_by').references(() => users.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('skills_enabled_idx').on(t.enabled)],
);
// ─── Routing Rules ──────────────────────────────────────────────────────────
export const routingRules = pgTable(
'routing_rules',
{
id: uuid('id').primaryKey().defaultRandom(),
/** Human-readable rule name */
name: text('name').notNull(),
/** Lower number = higher priority; unique per scope */
priority: integer('priority').notNull(),
/** 'system' rules apply globally; 'user' rules are scoped to a specific user */
scope: text('scope', { enum: ['system', 'user'] })
.notNull()
.default('system'),
/** Null for system-scoped rules; FK to users.id for user-scoped rules */
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
/** Array of condition objects that must all match for the rule to fire */
conditions: jsonb('conditions').notNull().$type<Record<string, unknown>[]>(),
/** Routing action to take when all conditions are satisfied */
action: jsonb('action').notNull().$type<Record<string, unknown>>(),
/** Whether this rule is active */
enabled: boolean('enabled').notNull().default(true),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
// Lookup by scope + priority for ordered rule evaluation
index('routing_rules_scope_priority_idx').on(t.scope, t.priority),
// User-scoped rules lookup
index('routing_rules_user_id_idx').on(t.userId),
// Filter enabled rules efficiently
index('routing_rules_enabled_idx').on(t.enabled),
],
);
// ─── Provider Credentials ────────────────────────────────────────────────────
export const providerCredentials = pgTable(
'provider_credentials',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
provider: text('provider').notNull(),
credentialType: text('credential_type', { enum: ['api_key', 'oauth_token'] }).notNull(),
encryptedValue: text('encrypted_value').notNull(),
refreshToken: text('refresh_token'),
expiresAt: timestamp('expires_at', { withTimezone: true }),
metadata: jsonb('metadata'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
// Unique constraint: one credential entry per user per provider
uniqueIndex('provider_credentials_user_provider_idx').on(t.userId, t.provider),
index('provider_credentials_user_id_idx').on(t.userId),
],
);
// ─── Summarization Jobs ─────────────────────────────────────────────────────
export const summarizationJobs = pgTable(
'summarization_jobs',
{
id: uuid('id').primaryKey().defaultRandom(),
status: text('status', { enum: ['pending', 'running', 'completed', 'failed'] })
.notNull()
.default('pending'),
logsProcessed: integer('logs_processed').notNull().default(0),
insightsCreated: integer('insights_created').notNull().default(0),
errorMessage: text('error_message'),
startedAt: timestamp('started_at', { withTimezone: true }),
completedAt: timestamp('completed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('summarization_jobs_status_idx').on(t.status)],
);