nexus/.planning/phases/25-file-system/25-RESEARCH.md
Nexus Dev d72c065fc7 docs(25-file-system): create phase plan — 4 plans in 3 waves
Plans cover FILE-01 through FILE-06: DB schema + shared types (wave 1),
server file service + routes and UI file upload (wave 2, parallel),
file preview components + full wiring (wave 3).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:48 +00:00

5.3 KiB

Phase 25: File System — Codebase Research

Gathered: 2026-04-01 Status: Complete

Existing Patterns

Storage System

  • StorageService in server/src/storage/service.ts handles file persistence via a provider abstraction (local disk or S3)
  • LocalDiskProvider in server/src/storage/local-disk-provider.ts writes files to {instanceRoot}/data/storage/
  • Object keys follow {companyId}/{namespace}/{year}/{month}/{day}/{uuid}-{stem}{ext} pattern
  • Storage service computes SHA-256 hash, validates inputs, sanitizes filenames
  • PutFileInput requires: companyId, namespace, originalFilename, contentType, body (Buffer)
  • PutFileResult returns: provider, objectKey, contentType, byteSize, sha256, originalFilename

Asset Upload (existing reference)

  • server/src/routes/assets.ts uses multer v2 with memory storage for single file upload
  • Pattern: multer.memoryStorage() + limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 }
  • Manual runSingleFileUpload wrapper around multer's middleware
  • After upload: calls storage.putFile(), then assetService.create() to write DB row
  • Content served via GET /api/assets/:assetId/content which pipes storage.getObject() stream
  • MAX_ATTACHMENT_BYTES defaults to 10MB (from server/src/attachment-types.ts)
  • Allowed content types configurable via PAPERCLIP_ALLOWED_ATTACHMENT_TYPES env var

Database Schema

  • Drizzle ORM with PostgreSQL, migrations in packages/db/src/migrations/ (currently at 0052)
  • Schema files in packages/db/src/schema/, exported from index.ts
  • Convention: pgTable() with uuid("id").primaryKey().defaultRandom(), timestamps with timezone
  • Index callbacks use object syntax: (table) => ({ indexName: index("idx_name").on(table.col) })
  • Chat tables: chat_conversations, chat_messages, chat_message_bookmarks
  • chat_messages columns: id, conversationId, role, content, agentId, messageType, createdAt, updatedAt

Chat Routes

  • server/src/routes/chat.tschatRoutes(db: Db) returns Express Router
  • Mounted at /api in server/src/app.ts line 160: api.use(chatRoutes(db))
  • Currently does NOT receive storageService — only db
  • Uses assertBoard(req) + assertCompanyAccess(req, companyId) for auth
  • Uses validators from @paperclipai/shared (zod schemas)
  • Message creation via svc.addMessage(conversationId, { role, content, agentId })

Chat Service

  • server/src/services/chat.tschatService(db) returns object with methods
  • Methods: listConversations, createConversation, getConversation, updateConversation, addMessage, editMessage, truncateMessagesAfter, addSystemMessage, streamEcho, searchMessages, toggleBookmark, getBookmarks, branchConversation, listBranches, exportConversation

Shared Types

  • packages/shared/src/types/chat.ts — ChatConversation, ChatMessage, ChatConversationListResponse, etc.
  • ChatMessage has: id, conversationId, role, content, agentId, messageType, createdAt, updatedAt
  • NO file/attachment fields exist on ChatMessage yet

UI Components

  • ChatInput.tsx — textarea with slash commands and @mentions; onSend(content: string) callback (text only)
  • ChatMessage.tsx — dispatches to specialized components by messageType (spec_card, handoff, etc.)
  • ChatPanel.tsx — orchestrates conversation selection, message list, input, streaming, bookmarks
  • chatApi in ui/src/api/chat.ts — API client using fetch-based api helper

Migration Pattern

  • SQL files named NNNN_descriptive_name.sql
  • Journal in packages/db/src/migrations/meta/_journal.json
  • Snapshots in packages/db/src/migrations/meta/NNNN_snapshot.json
  • Next migration index: 0053

Key Design Decisions (Claude's Discretion)

  1. Reuse existing StorageService — Files will be stored via the same StorageService (local disk or S3) used by assets. This means files live in {instanceRoot}/data/storage/ under company-namespaced object keys, NOT in a separate files/ git repo. The git versioning requirement (FILE-09, FILE-10) is out of scope for this batch of requirements (FILE-01 through FILE-06 only).

  2. New chat_files table — A new Drizzle schema table tracking file metadata: id, companyId, conversationId, messageId, filename, originalFilename, mimeType, sizeBytes, objectKey (StorageService key), sha256, source (user_upload | agent_generated), createdAt.

  3. New chat_file_references table — Enables a file to be referenced from multiple messages/conversations without re-uploading the binary.

  4. Extend chat routes — Add upload endpoint to chat routes (POST /api/conversations/:id/files) rather than creating a separate file routes module. This keeps the chat domain cohesive.

  5. Extend ChatMessage type — Add optional files array to ChatMessage response (populated via join or separate query). Messages with files get messageType: "file_attachment" or files are included as metadata alongside normal messages.

  6. ChatInput gets file drop zone — Add drag-and-drop, paste, and button-based file selection to ChatInput. Files are uploaded immediately on drop/paste, and the pending file IDs are sent with the message.

  7. ChatFilePreview component — Renders inline image previews, syntax-highlighted code previews (reusing existing rehype-highlight from Phase 21), and download buttons for other file types.