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
| Layer | Technology | Purpose |
|---|---|---|
| Framework | React Router 7 (Netlify template) | Full-stack React with SSR, file-based routing, loaders/actions |
| Styling | Tailwind CSS + @tailwindcss/typography | Utility-first CSS with prose styling for rendered markdown |
| Branding | Custom "epok" color scale (#74b668) | Green accent color defined in tailwind.config.ts (epok-50 through epok-950) |
| Editor | TipTap (pinned to v3.20.1) | WYSIWYG markdown editing with rich/raw toggle |
| Database | Supabase (Postgres 17) + pgvector | Managed Postgres with auth, RLS, and vector search |
| ORM | Drizzle | Type-safe SQL ORM with drizzle-kit push for schema management |
| DB Driver | postgres.js | Direct connection via IPv4 (not pooler) |
| Auth | Supabase Auth | Google OAuth + magic link, @supabase/ssr for cookie-based sessions |
| AI | Anthropic Claude + OpenAI | LLM chat/analysis (Claude Sonnet default, GPT-4o), embeddings (text-embedding-3-small) |
| Resend | Transactional email for invites, notifications, questionnaire requests | |
| Deployment | Netlify | Hosting, serverless functions, edge functions, auto-deploy from GitHub |
External Services
| Service | Purpose | Auth Method |
|---|---|---|
| Supabase | Database, auth, RLS | Service role key (server), anon key (browser) |
| Anthropic | Claude Sonnet for chat, analysis, agent | API key |
| OpenAI | GPT-4o chat, text-embedding-3-small | API key |
| Resend | Email delivery | API key |
| OAuth for Docs export, Drive folder picker | OAuth 2.0 per-user (tokens stored in profiles) | |
| Gamma | Document/presentation export | API 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:
| Variable | Required | Purpose |
|---|---|---|
SUPABASE_URL | Yes | Supabase project URL |
SUPABASE_ANON_KEY | Yes | Supabase anonymous key (browser client) |
SUPABASE_SERVICE_ROLE_KEY | Yes | Supabase service role key (server operations) |
DATABASE_URL | Yes | Direct Postgres connection string |
ANTHROPIC_API_KEY | Yes | Claude API access |
OPENAI_API_KEY | Yes | GPT-4o + embeddings |
RESEND_API_KEY | Yes | Email delivery |
GAMMA_API_KEY | No | Gamma export (sk-gamma-... format) |
GOOGLE_CLIENT_ID | No | Google OAuth for Docs export |
GOOGLE_CLIENT_SECRET | No | Google OAuth for Docs export |
GOOGLE_REDIRECT_URI | No | Google OAuth callback URL |
GOOGLE_DOCS_TEMPLATE_ID | No | Optional Google Docs template |
GOOGLE_DOCS_FOLDER_ID | No | Optional 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/documentsapi.v1.tools.$name.ts→/api/v1/tools/:name- Layout routes:
admin.tsxwraps alladmin.*routes
Testing
| Tool | Purpose |
|---|---|
| Playwright | End-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:
- A setup project (
tests/auth.setup.ts) runs before all test suites - It generates a magic link via
supabase.auth.admin.generateLink, verifies it withverifyOtp, and injects the session cookies into the browser context - The authenticated state is saved to
tests/.auth/user.json(gitignored) and reused by all test projects - 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
chromiumproject which depends onsetup - Use
page.getByRole()andpage.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:setup→chromium, 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.