From e2e3ac6e86cb0d51fe4049503cb3258ec0c264e3 Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:03:07 -0500 Subject: [PATCH] feat: ditch triplit, move to postgresql + drizzle orm --- AGENTS.md | 80 +- app/app.vue | 9 +- app/components/Attachment/Display.vue | 7 +- app/components/Attachment/Preview.vue | 2 +- app/components/ChatInput.vue | 27 +- app/components/Dialog/QuickSwitcher.vue | 6 +- app/components/Dialog/Settings.vue | 6 +- app/components/Dialog/Textbox.vue | 4 +- app/components/Dropdown.vue | 16 +- app/components/FileSelector.vue | 36 +- app/components/Logo/Cerebras.vue | 7 +- app/components/Logo/ClosedRouter.vue | 35 + app/components/Logo/Ollama.vue | 4 +- app/components/Logo/Vllm.vue | 24 + app/components/Markdown/ShikiHighlight.vue | 4 +- app/components/Message/Agent/Reasoning.vue | 15 +- app/components/Message/Agent/Text.vue | 7 +- app/components/Message/Agent/Tool/Debug.vue | 5 +- app/components/Message/Agent/Tool/index.vue | 9 +- app/components/Message/Agent/index.vue | 4 +- .../Message/TokenDetailsDropdown.vue | 12 +- app/components/Message/User.vue | 9 +- app/components/Message/index.vue | 122 +- app/components/ModelInfo.vue | 42 +- app/components/ModelSelector.vue | 37 +- app/components/RowVirtualizerDynamic.vue | 50 +- app/components/RowVirtualizerFixed.vue | 2 +- app/components/Settings/AIServiceProvider.vue | 228 +-- .../Settings/AppearanceSettings.vue | 14 +- app/components/Settings/ModelItem.vue | 2 +- app/components/Settings/ProviderSettings.vue | 35 +- app/components/Settings/ProviderSidebar.vue | 10 +- app/components/Settings/SystemAssistants.vue | 5 +- app/components/Sidenav/Header.vue | 11 +- app/components/Sidenav/HeaderAgent.vue | 8 +- app/components/Sidenav/Item.vue | 38 +- app/components/Sidenav/NavAgent.vue | 99 +- app/components/Sidenav/NavHome.vue | 64 +- app/components/Sidenav/index.vue | 6 +- app/components/ThemeSwitcher.vue | 8 +- app/components/Tooltip.vue | 8 +- app/components/TooltipProvider.vue | 19 +- app/composables/useAgents.ts | 254 +++- app/composables/useAuth.ts | 41 +- app/composables/useChat.ts | 760 +++++----- app/composables/useModels.ts | 190 ++- app/composables/useTokenDropdown.ts | 5 +- app/composables/useTopic.ts | 101 ++ app/composables/useUserEvents.ts | 190 +++ app/composables/useUserSettings.ts | 277 ++-- app/layouts/auth.vue | 20 +- app/layouts/default.vue | 9 +- app/pages/agent/[id]/index.vue | 41 +- app/pages/agent/[id]/profile.vue | 65 +- app/pages/agent/[id]/topic/[topicId].vue | 280 +--- app/pages/auth/login.vue | 5 +- app/pages/auth/register.vue | 5 +- app/pages/index.vue | 72 +- app/plugins/auth.client.ts | 10 +- app/plugins/auth.server.ts | 10 - app/plugins/global-data.ts | 13 - app/types/model.ts | 22 +- app/utils/model-mapping.ts | 7 + app/utils/settings.ts | 19 - app/utils/sort.ts | 2 + bun.lock | 761 ++++++---- docker-compose.yml | 15 + drizzle.config.ts | 10 + drizzle/migrations/0000_flimsy_firebrand.sql | 2 + drizzle/relations.ts | 95 ++ drizzle/schema.ts | 298 ++++ generate-tokens.ts | 38 - lib/auth.ts | 19 +- nuxt.config.ts | 30 +- package.json | 152 +- patches/@triplit%2Fclient@1.0.50.patch | 169 --- patches/@triplit%2Fdb@1.1.10.patch | 167 --- scripts/migrate-triplit-to-pg.ts | 205 +++ server/api/agent/[id]/index.delete.ts | 34 + server/api/agent/[id]/index.patch.ts | 66 + server/api/agent/index.post.ts | 44 + server/api/agents.get.ts | 37 + server/api/auto-rename/[topicId].post.ts | 142 -- server/api/auto-rename/cancel.post.ts | 42 - server/api/chat/cancel/[generationId].post.ts | 26 - server/api/chat/generate.post.ts | 919 ------------ server/api/events.get.ts | 47 + server/api/file/[id[.delete.ts | 23 + server/api/file/index.post.ts | 47 + .../api/messages/[messageId]/index.delete.ts | 146 ++ .../api/messages/[messageId]/index.patch.ts | 30 + server/api/model/[modelId]/index.delete.ts | 26 + server/api/model/[modelId]/index.patch.ts | 95 ++ server/api/model/index.post.ts | 86 ++ .../api/provider/[providerId]/index.patch.ts | 68 + .../provider/[providerId]/models.delete.ts | 28 + .../api/provider/[providerId]/models.patch.ts | 38 + .../api/provider/[providerId]/models.post.ts | 272 +++- server/api/providers.get.ts | 39 + server/api/settings/index.get.ts | 34 + server/api/settings/index.patch.ts | 87 ++ .../[topicId]/auto-rename/cancel.post.ts | 44 + .../topic/[topicId]/auto-rename/index.post.ts | 261 ++++ .../chat/cancel/[generationId].post.ts | 75 + server/api/topic/[topicId]/chat/index.post.ts | 1263 +++++++++++++++++ server/api/topic/[topicId]/index.delete.ts | 21 + server/api/topic/[topicId]/index.get.ts | 95 ++ server/api/topic/[topicId]/index.patch.ts | 65 + server/api/topic/[topicId]/message.post.ts | 76 + server/api/topic/index.post.ts | 45 + server/api/topics.get.ts | 36 + server/lib/db.ts | 12 + server/lib/triplit.ts | 8 - server/utils/ai-provider.ts | 88 +- server/utils/cerebras.ts | 84 -- server/utils/events.ts | 161 +++ server/utils/renames.ts | 30 +- triplit/auth-schema.ts | 90 -- triplit/schema.ts | 490 ------- uno.config.ts | 19 +- utils/message.ts | 219 +++ 121 files changed, 6680 insertions(+), 4373 deletions(-) create mode 100644 app/components/Logo/ClosedRouter.vue create mode 100644 app/components/Logo/Vllm.vue create mode 100644 app/composables/useTopic.ts create mode 100644 app/composables/useUserEvents.ts delete mode 100644 app/plugins/global-data.ts delete mode 100644 app/utils/settings.ts create mode 100644 docker-compose.yml create mode 100644 drizzle.config.ts create mode 100644 drizzle/migrations/0000_flimsy_firebrand.sql create mode 100644 drizzle/relations.ts create mode 100644 drizzle/schema.ts delete mode 100644 generate-tokens.ts delete mode 100644 patches/@triplit%2Fclient@1.0.50.patch delete mode 100644 patches/@triplit%2Fdb@1.1.10.patch create mode 100644 scripts/migrate-triplit-to-pg.ts create mode 100644 server/api/agent/[id]/index.delete.ts create mode 100644 server/api/agent/[id]/index.patch.ts create mode 100644 server/api/agent/index.post.ts create mode 100644 server/api/agents.get.ts delete mode 100644 server/api/auto-rename/[topicId].post.ts delete mode 100644 server/api/auto-rename/cancel.post.ts delete mode 100644 server/api/chat/cancel/[generationId].post.ts delete mode 100644 server/api/chat/generate.post.ts create mode 100644 server/api/events.get.ts create mode 100644 server/api/file/[id[.delete.ts create mode 100644 server/api/file/index.post.ts create mode 100644 server/api/messages/[messageId]/index.delete.ts create mode 100644 server/api/messages/[messageId]/index.patch.ts create mode 100644 server/api/model/[modelId]/index.delete.ts create mode 100644 server/api/model/[modelId]/index.patch.ts create mode 100644 server/api/model/index.post.ts create mode 100644 server/api/provider/[providerId]/index.patch.ts create mode 100644 server/api/provider/[providerId]/models.delete.ts create mode 100644 server/api/provider/[providerId]/models.patch.ts create mode 100644 server/api/providers.get.ts create mode 100644 server/api/settings/index.get.ts create mode 100644 server/api/settings/index.patch.ts create mode 100644 server/api/topic/[topicId]/auto-rename/cancel.post.ts create mode 100644 server/api/topic/[topicId]/auto-rename/index.post.ts create mode 100644 server/api/topic/[topicId]/chat/cancel/[generationId].post.ts create mode 100644 server/api/topic/[topicId]/chat/index.post.ts create mode 100644 server/api/topic/[topicId]/index.delete.ts create mode 100644 server/api/topic/[topicId]/index.get.ts create mode 100644 server/api/topic/[topicId]/index.patch.ts create mode 100644 server/api/topic/[topicId]/message.post.ts create mode 100644 server/api/topic/index.post.ts create mode 100644 server/api/topics.get.ts create mode 100644 server/lib/db.ts delete mode 100644 server/lib/triplit.ts delete mode 100644 server/utils/cerebras.ts create mode 100644 server/utils/events.ts delete mode 100644 triplit/auth-schema.ts delete mode 100644 triplit/schema.ts create mode 100644 utils/message.ts diff --git a/AGENTS.md b/AGENTS.md index bb3e21e..00a99ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Veridian AGENTS.md -**Nuxt 4 AI chat platform with Triplit database + better-auth** +**Nuxt 4 AI chat platform with Drizzle + PostgreSQL + better-auth** ## Architecture Overview @@ -21,9 +21,10 @@ server/ # Server-side API routes ├── chat/ # Generation, cancel endpoints └── provider/ # Model fetching from providers -triplit/ # Database -├── schema.ts # Full schema with auth + app collections -└── auth-schema.ts # Auth collections (users, sessions, accounts) +drizzle/ # Database (migrated from Triplit) +├── schema.ts # Full schema with auth + app tables +├── relations.ts # Table relationships +└── migrations/ # SQL migration files lib/ # Shared utilities ├── auth.ts # Better-auth server config @@ -39,7 +40,9 @@ database commands. ### Database Commands -- `bunx triplit schema push` - Push schema changes to database +- `bunx drizzle-kit push` - Push schema changes to database +- `bunx drizzle-kit generate` - Generate migration from schema changes +- `bunx drizzle-kit migrate` - Run pending migrations ## Code Style @@ -57,15 +60,12 @@ database commands. Group imports in this order (alphabetical within groups): 1. Type imports (`types/`) 2. Dependency imports (npm packages) -3. Triplit imports (`#triplit/`, `@triplit/`) -4. Vue/Nuxt imports (`vue`, `#app`, `~~/`, `~/`, `@/`) -5. Local imports (`lib/`, `server/`) +3. Vue/Nuxt imports (`vue`, `#app`, `~~/`, `~/`, `@/`) +4. Local imports (`lib/`, `server/`) ```typescript // Example import order import type { Result, Ok, Err } from '~~/types/result'; -import type { Entity } from '@triplit/client'; -import type schema from '#triplit/schema'; import type { Foo } from 'vue'; import { ref, computed } from 'vue'; import { useFeature } from '~/composables/useFeature'; @@ -128,23 +128,28 @@ export default defineEventHandler(async (event) => { }); ``` -## Database Query Pattern (Triplit) +## Database Query Pattern (Drizzle) ```typescript -// Client queries use useQuery() with auto-includes -useQuery('collection', triplit, triplit.query('collection').Include('relation')); +// Server-side queries use Drizzle ORM +import { db } from '~~/server/db'; +import { agents } from '~~/drizzle/schema'; -// Server-side -import { httpClient } from '~~/server/lib/triplit'; -await httpClient.fetchOne(httpClient.query('collection').Where('id', '=', providerId)); +// Query with relations +const result = await db.query.agents.findMany({ + where: eq(agents.userId, userId), + with: { topics: true, messages: true }, +}); + +// Insert with conflict handling +await db.insert(agents).values(data).onConflictDoNothing(); ``` ## Authentication Flow 1. **Login**: Client uses `authClient.signIn.email()` → derives encryption key from password 2. **Session**: Better-auth creates session token → stored in cookie -3. **Triplit**: Session token passed to `triplit.startSession(token)` for DB access -4. **API Keys**: Encrypted client-side with AES-GCM (key derived from password + userId) +3. **API Keys**: Encrypted client-side with AES-GCM (key derived from password + userId) **Key files**: `lib/auth.ts`, `lib/auth-client.ts`, `app/plugins/auth.*.ts`, `app/middleware/auth.global.ts` @@ -159,10 +164,8 @@ await httpClient.fetchOne(httpClient.query('collection').Where('id', '=', provid | Variable | Purpose | |----------|---------| -| `TRIPLIT_SERVICE_TOKEN` | Admin DB access | -| `NUXT_TRIPLIT_ANON_TOKEN` | Anonymous DB access | +| `DATABASE_URL` | PostgreSQL connection string | | `BETTER_AUTH_SECRET` | Auth encryption | -| `NUXT_PUBLIC_TRIPLIT_URL` | DB server URL | ## Key Composables @@ -198,15 +201,40 @@ await httpClient.fetchOne(httpClient.query('collection').Where('id', '=', provid ## Known Issues (from BUGS.md) -1. Triplit occasionally makes duplicate connections (race condition) -2. Sidebar hover animation occasionally glitches on agent routes -3. Theme switcher + sidebar interaction bug +1. Sidebar hover animation occasionally glitches on agent routes +2. Theme switcher + sidebar interaction bug + +## Critical Architecture Notes + +### Chat Endpoint God File +- `server/api/topic/[topicId]/chat/index.post.ts` is 1263 lines handling route validation, tool definitions, streaming orchestration, DB transactions, and event emission +- Files that change together: this file + `server/utils/events.ts` + `server/utils/generations.ts` +- Tool definitions (bash, python, file access) should be extracted to separate module before extending + +### Security: Code Execution Tools Have No Sandboxing +- `bashTool` uses `exec()` and `pythonTool` uses `python3 -c` with user-controlled input — full RCE risk +- Do NOT extend these tools without adding: timeouts, path restrictions, resource limits +- Location: `server/api/topic/[topicId]/chat/index.post.ts` ~lines 261-455 + +### Crash Risk: `todo()` Function +- `todo()` literally throws `new Error('TODO')` on unhandled token types in chat handler +- Handle gracefully before production deployment +- Location: `server/api/topic/[topicId]/chat/index.post.ts` ~line 256 + +### Module-Scoped Mutable State Limitations +- `useDialog.ts` has module-scoped `actionCallback` — not SSR-safe, overwritten on concurrent dialogs +- `pendingGenerations` in `server/utils/generations.ts` and in-memory event channels in `server/utils/events.ts` won't work in multi-process deployments +- Fine for single-instance dev, but architectural ceiling for scaling + +### Pagination Params Validated But Unused +- `agents.get.ts` validates `page`/`limit` query params but never applies them to the query +- All agent queries return everything regardless of pagination params ## Development Notes - **Never run dev server as agent** - just complete tasks and signal done -- Always run `bunx triplit schema push` after schema changes +- Always run `bunx drizzle-kit push` after schema changes - Use `protectRoute()` in all API routes for auth - All UI state that persists → use cookies (`useCookie()`) -- Real-time data → Triplit subscriptions via `useQuery()` +- Real-time data → SSE events via `server/utils/events.ts` - API keys → encrypted client-side before DB storage diff --git a/app/app.vue b/app/app.vue index 5bbadc2..1c44b80 100644 --- a/app/app.vue +++ b/app/app.vue @@ -2,10 +2,11 @@ import '~/assets/css/reset.css'; import '~/assets/css/base.css'; -const { accent, neutral, hinting } = useUserSettings(); - -// by default, disable hinting -if (Number.isNaN(Number(hinting.value))) hinting.value = '0'; +const { user } = useAuth(); +const { accent, neutral, hinting, refresh: refreshSettings } = await useUserSettings(); +watch(user, () => { + refreshSettings(); +}) watchEffect(() => { useHead({ diff --git a/app/components/Attachment/Display.vue b/app/components/Attachment/Display.vue index 8a8da56..9fa5d8d 100644 --- a/app/components/Attachment/Display.vue +++ b/app/components/Attachment/Display.vue @@ -32,7 +32,7 @@ const fileExtension = computed(() => { diff --git a/app/components/Attachment/Preview.vue b/app/components/Attachment/Preview.vue index 1b11de8..b2902fb 100644 --- a/app/components/Attachment/Preview.vue +++ b/app/components/Attachment/Preview.vue @@ -21,7 +21,7 @@ const handleDelete = async () => { \ No newline at end of file diff --git a/app/pages/agent/[id]/topic/[topicId].vue b/app/pages/agent/[id]/topic/[topicId].vue index 22429b7..f99e4c4 100644 --- a/app/pages/agent/[id]/topic/[topicId].vue +++ b/app/pages/agent/[id]/topic/[topicId].vue @@ -1,131 +1,22 @@