Content Architecture
Core Decision: Database-First Markdown
All business content lives in the database as versioned markdown. This includes themes, advisor intake responses, workshop notes, report drafts, templates, prompts, analysis outputs, and system instructions.
Why database-only:
- RLS — Customer content is isolated per advisor/engagement. Supabase Row Level Security enforces this at the database layer.
- Versioning — Every edit creates a new version. We can diff, roll back, or analyze how content evolved over time.
- Embeddable — Published content is automatically chunked and embedded for AI semantic search via pgvector.
- Universal access — The same content is readable/writable by: the web UI (TipTap editor), React Router API routes, AI agents via REST/MCP, and automated ingestion pipelines.
- Dynamic by nature — Everything in this system evolves constantly via human edits, AI analysis, chatbot interactions, and automated processes.
What Stays on Disk
Only developer/project documentation (/docs folder) lives as files. These help humans and AI coding agents understand how to build the app. They are not business content.
Folders + Tags (Replacing Virtual Paths)
The original design used a path text field for virtual folder paths. This was replaced with a proper folder/tag system:
Folders (content_folders)
- Real database table with
parent_idself-reference for nesting - Scoped:
engagement_id = NULLfor KB folders, set for engagement folders - Unique constraint:
(name, parent_id, engagement_id)prevents duplicates at same level - Delete only allowed when empty (no docs, no sub-folders)
- Notable: "Inbox" folder is the landing zone for auto-ingested content
Tags (content_tags + content_document_tags)
- Flat classification labels — a document can have 0 or more tags
- Replace the old
content_typeenum column - Include both content-type labels and milestone markers:
- Content types: Theme, Data Point, Report Template, Prompt, System Instruction, Internal Note, Intake Notes, Workshop Note, Transcript, Report Draft, Final Report, Questionnaire Response, Summary, Auto-Ingested, Analysis Output
- Milestone markers: MS: Intake, MS: Discovery, MS: Workshop 1, MS: Workshop 2, MS: Workshop 3, MS: Complete
- Colors defined per tag for UI display (purple, indigo, amber, blue, etc.)
Why Folders + Tags?
- Folders answer "where does this live?" — a single organizational location
- Tags answer "what is this?" — cross-cutting classification that works across folders
- Milestone tags link documents to engagement milestones (the "View docs" links in engagement overview filter by milestone tag)
Content Table Design
content_folders
├── id (uuid, PK)
├── name (text)
├── parent_id (uuid, nullable — self-ref for nesting)
├── engagement_id (uuid, nullable — NULL = KB, set = engagement-scoped)
├── created_by (uuid)
├── created_at, updated_at
content_documents
├── id (uuid, PK)
├── title (text)
├── folder_id (uuid, nullable — FK → content_folders.id)
├── engagement_id (uuid, nullable — NULL = KB, set = engagement-scoped)
├── created_by (uuid)
├── created_at, updated_at
content_document_tags
├── document_id (uuid, FK → content_documents.id) ─┐ composite PK
├── tag_id (uuid, FK → content_tags.id) ─┘
content_versions
├── id (uuid, PK)
├── document_id (uuid, FK → content_documents.id)
├── version (integer — auto-incrementing per document)
├── status (enum: draft, published, archived)
├── content (text — the markdown body)
├── change_summary (text, nullable)
├── created_by (uuid)
├── created_at
content_visibility
├── id (uuid, PK)
├── document_id (uuid, FK → content_documents.id)
├── visible_to_client (boolean, default false)
├── published_at (timestamptz)
├── published_by (uuid)
How It Works
- Creating content — Insert
content_documentsrow + firstcontent_versionsrow (version 1, status draft or published). Optionally assign to folder and add tags. - Editing content — Insert new
content_versionsrow with incremented version. Publishing auto-archives previous published version. - Reading current — Join
content_documents→content_versions WHERE status = 'published'. - Browsing history — Query all versions for a document, ordered by version.
- Rolling back — Create a new version with old content (never mutate history).
- Publishing — Set version status to published → triggers auto-embedding for RAG search.
- Filing to engagement — Set
engagement_id, clearfolder_id, optionally assign milestone tag.
RLS Strategy
- KB content (engagement_id IS NULL): Super admins full CRUD; associates read-only; clients no access
- Engagement content (engagement_id IS NOT NULL): Super admins full CRUD; assigned associates CRUD; clients read only published + visible content
- RLS recursion fix:
content_documents ↔ content_visibility ↔ content_versionscircular policies broken with SECURITY DEFINER helper functions (is_doc_visible_to_client,can_view_document,can_access_document_versions,is_doc_published_and_visible)
Markdown Editing in the UI
The TipTap-based editor (app/components/markdown-editor.tsx) provides:
- Rich mode — WYSIWYG editing with toolbar (headings, bold, italic, lists, links, code blocks, blockquotes, tables)
- Raw mode — Plain textarea for direct markdown editing, copy/paste, and agent consumption
- Version history — Sidebar showing all versions with timestamps, change summaries, and restore capability
- Auto-save — Draft versions saved without publishing
- SSR safe — TipTap initialized with
immediatelyRender: falseto avoid hydration issues
TipTap pinning: All tiptap packages pinned to v3.20.1. Version 3.20.3+ ships source-only (no dist/) which breaks Vite builds.
How AI Agents Interact
Built-in Chat Panel
- Slide-out panel in admin layout with Claude Sonnet (default) or GPT-4o
- RAG-powered: queries embed user's message, retrieve top-5 similar document chunks via cosine similarity, inject as context
- Persistent chat history via
chat_messagestable - Agent tools available for searching KB, reading/creating documents, managing engagements
Analysis Tab
- Per-engagement AI analysis with custom prompts
- @-mention popup to reference specific documents as inputs
- Streaming LLM execution via SSE
- Output saved as engagement-scoped content document tagged "Analysis Output"
- Refinement: send current output + instructions back for iteration
REST API (/api/v1/tools)
- Execute any agent tool via HTTP POST
- Used by ChatGPT Actions via OpenAPI spec
- Auth: API key (
Authorization: Bearer epok_sk_...)
MCP Server (/api/mcp)
- Stateless JSON-RPC 2.0 endpoint for Claude desktop/mobile
- Same tool set as REST API
- Auth: same API key system
Auto-Ingestion (/api/v1/ingest)
- External webhook for content ingestion (Otter.ai via Zapier)
- Creates KB documents in Inbox folder, auto-tagged and auto-embedded
Export Pipeline
Documents can be exported from the UI via the Copy & Export menu (app/components/copy-export-menu.tsx):
- Clipboard: Markdown source, plain text (stripped formatting), or rich text (HTML)
- Google Docs: Per-user OAuth connection, creates new Google Doc with markdown → Docs formatting, optional folder picker
- Gamma: Sends content to Gamma's Generate API as document, presentation, or webpage. Uses
X-API-KEYheader withsk-gamma-...key. Client-side polling for async generation completion.