← Back to App

Epok Advisor Docs

Tech Stack

Overview

Epok Advisor is a full-stack React application deployed on Netlify. The server handles data operations via React Router loaders/actions and standalone API routes. All business content lives in the database as versioned markdown. AI features run through Claude and OpenAI APIs with RAG-powered retrieval via pgvector embeddings.

Core Technologies

LayerTechnologyPurpose
FrameworkReact Router 7 (Netlify template)Full-stack React with SSR, file-based routing, loaders/actions
StylingTailwind CSS + @tailwindcss/typographyUtility-first CSS with prose styling for rendered markdown
BrandingCustom "epok" color scale (#74b668)Green accent color defined in tailwind.config.ts (epok-50 through epok-950)
EditorTipTap (pinned to v3.20.1)WYSIWYG markdown editing with rich/raw toggle
DatabaseSupabase (Postgres 17) + pgvectorManaged Postgres with auth, RLS, and vector search
ORMDrizzleType-safe SQL ORM with drizzle-kit push for schema management
DB Driverpostgres.jsDirect connection via IPv4 (not pooler)
AuthSupabase AuthGoogle OAuth + magic link, @supabase/ssr for cookie-based sessions
AIAnthropic Claude + OpenAILLM chat/analysis (Claude Sonnet default, GPT-4o), embeddings (text-embedding-3-small)
EmailResendTransactional email for invites, notifications, questionnaire requests
DeploymentNetlifyHosting, serverless functions, edge functions, auto-deploy from GitHub

External Services

ServicePurposeAuth Method
SupabaseDatabase, auth, RLSService role key (server), anon key (browser)
AnthropicClaude Sonnet for chat, analysis, agentAPI key
OpenAIGPT-4o chat, text-embedding-3-smallAPI key
ResendEmail deliveryAPI key
GoogleOAuth for Docs export, Drive folder pickerOAuth 2.0 per-user (tokens stored in profiles)
GammaDocument/presentation exportAPI key (sk-gamma-..., X-API-KEY header)

Architecture Principles

  • Loaders + Actions — React Router loaders fetch data server-side; actions handle mutations. No separate API framework (Hono was removed early on).
  • Standalone API routes — Routes prefixed api.* handle non-UI operations: chat streaming, analysis execution, external API endpoints, exports, OAuth, MCP.
  • Database-first content — All business content (themes, notes, reports, prompts) lives as versioned markdown in Postgres. Folders provide organization, tags provide classification.
  • RAG pipeline — Documents are chunked and embedded on publish. Chat and analysis queries retrieve relevant chunks via cosine similarity before prompting the LLM.
  • External API layer — Agent tools are defined once and exposed via REST API (ChatGPT), MCP (Claude), and the built-in chat panel simultaneously.

Environment Variables

Defined in .env (gitignored), with .env.example committed:

VariableRequiredPurpose
SUPABASE_URLYesSupabase project URL
SUPABASE_ANON_KEYYesSupabase anonymous key (browser client)
SUPABASE_SERVICE_ROLE_KEYYesSupabase service role key (server operations)
DATABASE_URLYesDirect Postgres connection string
ANTHROPIC_API_KEYYesClaude API access
OPENAI_API_KEYYesGPT-4o + embeddings
RESEND_API_KEYYesEmail delivery
GAMMA_API_KEYNoGamma export (sk-gamma-... format)
GOOGLE_CLIENT_IDNoGoogle OAuth for Docs export
GOOGLE_CLIENT_SECRETNoGoogle OAuth for Docs export
GOOGLE_REDIRECT_URINoGoogle OAuth callback URL
GOOGLE_DOCS_TEMPLATE_IDNoOptional Google Docs template
GOOGLE_DOCS_FOLDER_IDNoOptional default Google Drive folder

File Structure

app/
├── components/          # Shared UI components
│   ├── chat-panel.tsx         # AI chat slide-out panel
│   ├── copy-export-menu.tsx   # Export dropdown (copy, Gamma, Google Docs)
│   ├── markdown-editor.tsx    # TipTap WYSIWYG editor
│   ├── prompt-editor.tsx      # @-mention prompt textarea for analysis
│   ├── refine-modal.tsx       # AI refinement dialog
│   └── ...
├── db/
│   └── schema/          # Drizzle table definitions (one file per table)
├── lib/                 # Server utilities
│   ├── ai.server.ts           # Streaming LLM, RAG retrieval, embedding pipeline
│   ├── agent.server.ts        # Agent orchestration and tool execution
│   ├── agent-tools.server.ts  # Tool definitions (search_kb, read_document, etc.)
│   ├── api-auth.server.ts     # API key validation middleware
│   ├── email.server.ts        # Resend email helpers
│   ├── google-docs.server.ts  # Google Docs/Drive API integration
│   ├── require-auth.server.ts # Route auth guard
│   ├── supabase.server.ts     # Server Supabase client
│   └── supabase.client.ts     # Browser Supabase client
├── routes/              # File-based routing (dot-delimited flat files)
│   ├── admin.tsx              # Admin layout shell with sidebar + chat panel
│   ├── admin.engagements.*    # Engagement CRUD, detail tabs
│   ├── admin.knowledge-base.* # KB folder/document management
│   ├── admin.questionnaires.* # Questionnaire template builder
│   ├── api.chat.ts            # SSE streaming chat endpoint
│   ├── api.analysis.*         # Analysis CRUD + streaming execution
│   ├── api.export.*           # Gamma + Google Docs export
│   ├── api.v1.*               # External REST API (tools, ingest, openapi)
│   ├── api.mcp.ts             # MCP JSON-RPC endpoint
│   ├── api.oauth.*            # OAuth 2.1 authorization server
│   └── api.keys.ts            # API key management
├── root.tsx
├── routes.ts
└── app.css
docs/                    # Project documentation (this folder)
netlify/
└── edge-functions/
    └── cors.ts          # CORS preflight handler for API/OAuth
tests/                   # Playwright E2E tests

Routing Convention

Uses @react-router/fs-routes with dot-delimited flat file routing:

  • admin.engagements.$id.documents.tsx/admin/engagements/:id/documents
  • api.v1.tools.$name.ts/api/v1/tools/:name
  • Layout routes: admin.tsx wraps all admin.* routes

Testing

ToolPurpose
PlaywrightEnd-to-end browser testing

Running Tests

npm test                  # Run all tests
npm run test:ui           # Interactive UI
npx playwright test tests/engagement-tabs.spec.ts  # Specific file

Auth Setup

Tests authenticate automatically using Supabase's admin API:

  1. A setup project (tests/auth.setup.ts) runs before all test suites
  2. It generates a magic link via supabase.auth.admin.generateLink, verifies it with verifyOtp, and injects the session cookies into the browser context
  3. The authenticated state is saved to tests/.auth/user.json (gitignored) and reused by all test projects
  4. Tests run as ben.unsworth@epokadvice.com (super_admin)

Test Structure

tests/
├── auth.setup.ts               # Auth bootstrap (runs first)
├── home.spec.ts                 # Public pages
├── engagement-tabs.spec.ts      # Engagement detail: tabs, sidebar persistence
├── engagement-documents.spec.ts # Documents tab: create engagement doc → KB editor
└── questionnaires.spec.ts       # Template builder, seeded templates, response flow

Writing New Tests

  • All authenticated tests use the chromium project which depends on setup
  • Use page.getByRole() and page.getByText() for robust selectors
  • For engagement tests, use a.block[href^="/admin/engagements/"] to target engagement cards (avoids matching sidebar nav links)
  • Use test.skip() with a guard when data may not exist (e.g., no engagements in DB)

Config

  • playwright.config.ts — projects: setupchromium, webServer reuses running dev server
  • Browsers: Chromium only (add Firefox/WebKit projects as needed)
  • Traces captured on first retry for debugging (npx playwright show-trace)

Key Decisions

  • Why React Router 7? — Full-stack framework with SSR, loaders/actions, and file-based routing. Netlify template provides seamless deployment.
  • Why Drizzle? — Type-safe, SQL-first ORM that doesn't hide the database. Works well with Supabase Postgres. Schema push for rapid iteration.
  • Why TipTap? — Extensible rich text editor with markdown serialization. Pinned to v3.20.1 (v3.20.3+ ships source-only, breaking Vite builds).
  • Why Markdown for content? — Human-readable, version-controllable, and trivially parseable by AI. No CMS overhead.
  • Why pgvector? — Native Postgres vector search avoids external vector DB. HNSW index for fast approximate nearest neighbor queries.
  • Why Playwright? — Cross-browser E2E testing with excellent async handling, auto-waiting, and built-in auth state management via storage state files.
  • Why direct DB connection? — IPv4 add-on on Supabase allows direct postgres.js connection from Netlify functions, avoiding pooler complexity.