Last Updated: 2026-03-26
ThinkHaven - Decision accelerator for structured AI sessions
- Tech Stack: Next.js 15.5, React 19, TypeScript, Supabase, Stripe, Anthropic Claude
- Architecture: Monorepo with Next.js app in
apps/web/ - Deployment: Vercel project
thinkhaven(https://thinkhaven.co) - Documented Solutions:
docs/solutions/contains searchable solution notes for past bugs, architecture patterns, conventions, and workflow issues, organized by category with YAML frontmatter (module,problem_type,tags). Relevant when implementing or debugging in documented areas.
All commands run from apps/web/:
npm run dev # Dev server (localhost:3000, Turbopack)
npm run build # Production build
npm run lint # ESLint
npm test # Unit tests (Vitest, watch mode)
npm run test:run # Unit tests (once)
npm run test:e2e # E2E tests (Playwright, 7 smoke tests)
npm run test:prod # Smoke tests against production (15 tests)Migrations: apps/web/supabase/migrations/ (001 → 017, sequential, never skip)
Protected (/app/* - requires auth):
/app- Dashboard/app/new- New session/app/session/[id]- Active session workspace/app/account- Account settings
Public: / (landing), /try (guest, 10 msg limit), /demo, /assessment
Legacy redirects: /dashboard → /app, /bmad → /app/new, /workspace/[id] → /app/session/[id], /account → /app/account
See .env.example. Required: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, ANTHROPIC_API_KEY, STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_APP_URL. Optional: OPENROUTER_API_KEY (server-only fallback for guest synthesis when Anthropic is unavailable; OPENROUTER_MODEL and AI_PROVIDER=openrouter tune it — streaming/tool chat paths are not covered).
Model selection is per-workload via lib/ai/model-config.ts (modelFor(workload)), NOT a single global. Cost-preservation posture: defaults are synthesis/board/chat→claude-sonnet-4-6, util→claude-haiku-4-5 — NO frontier model (Fable 5 / Opus 4.8) is ever a default; they are opt-in only via ANTHROPIC_MODEL_{SYNTHESIS,BOARD,CHAT,UTIL}. ANTHROPIC_MODEL is a global kill-switch (forces every workload onto one model). Synthesis falls back to OpenRouter on Anthropic 402/429/5xx. Frontier models (Fable 5, Opus 4.8/4.7) 400 on temperature — use samplingFor(model, t) to build the sampling slice, never hardcode temperature in a messages.create() call. Reasoning effort: synthesis/board send output_config.effort: "high" via effortConfigFor(model, workload) (model-gated — stripped on models that don't support effort); tune with ANTHROPIC_EFFORT_{SYNTHESIS,BOARD} (low|medium|high|max).
- Unit tests:
**/*.test.{ts,tsx}, setup intests/setup.ts - E2E:
tests/e2e/smoke/health.spec.ts- 7 public route smoke tests, all passing in CI - Prod:
tests/e2e/smoke/beta-checklist.spec.ts- 9 production verification tests (npm run test:prod) - Config:
vitest.config.ts,playwright.config.ts,playwright.prod.config.ts(production) - Unit suite is fully green (61 files / 528 tests pass, 2 files + 41 tests intentionally skipped). Keep it that way — failures are regressions now, not baseline noise.
next.config.ts- Next.js configtailwind.config.cjs- Tailwind (CommonJS format)eslint.config.mjs- Linting rules
- Middleware active (
middleware.ts) - Validates JWTs viasupabase.auth.getUser()and refreshes tokens. Runs in Edge Runtime; avoid Node-only APIs in middleware context. - Credit deduction - ALWAYS use
deduct_credit_transaction()for atomicity, never manual UPDATE - File-system routing - Every route needs a
page.tsxfile - Migration order - Sequential (001 → 017), never skip
- Stripe webhooks - Verify signatures with
stripe-service.ts.constructWebhookEvent() - Tldraw v4 - Use
getSnapshot(store)/loadSnapshot(store, data), NOT instance methods - Agentic tool loop - Max 5 rounds per message (
MAX_TOOL_ROUNDSin/api/chat/stream/route.ts) - Tool results - Use
ToolExecutor.formatResultsForClaude()for Claude's expectedtool_resultformat - Session ops - Use
session-primitives.tsfunctions, not direct Supabase calls - Supabase server client -
createClient()returns null when env vars missing; callers must null-check - Next.js env files - Only reads
.env,.env.local,.env.production(NOT.env.test) - SSG safety -
lib/supabase/client.tsexports a no-op Proxy for SSG; use for client components - Monorepo git paths - Always use absolute paths or run git commands from repo root, not
apps/web/ - Claude Code Review CI -
anthropics/claude-code-action@v1has known SDK crash (issue #911).continue-on-error: trueis set. Job may show failed but workflow passes. - Never use
text-secondaryfor text - shadcn maps--secondaryto parchment (a background color), producing invisible text on cream/parchment. Usetext-muted-foreground(slate-blue) for secondary text, ortext-ink-lightfor warm secondary text. - Hooks must re-throw or return errors - If a hook catches an error and only sets state, callers can't detect failure. Always re-throw after setting error state, or return a success/failure indicator. Silent swallowing creates impossible UI states.
- IDOR checks on every session-scoped handler - When adding ownership verification to API routes, audit ALL handlers in the file, not just the ones that triggered the fix. Partial coverage is worse than none (false sense of security).
- React ErrorBoundaries don't catch event handlers - Errors in
onClick,onSubmit, etc. are not caught by ErrorBoundary. Wrap event handler bodies in try/catch or use error state for recovery. - Design system source of truth - The actual design system lives in
globals.css(CSS variables, component classes) andtailwind.config.cjs(Tailwind tokens). Do NOT trustdocs/design/MASTER.mdif it reappears — it's auto-generated by ui-ux-pro-max with wrong colors/fonts. - Rate limiting - Use
RateLimiter.createLimitResponse(resetTime)fromlib/security/rate-limiter.tsfor 429 responses. Never hand-roll the response. Admin bypass check (isAdminEmail) must run BEFORE rate limit check so admins aren't blocked. - Lazy module-level init for env vars - Never use IIFEs or top-level
const x = fn()that reads env vars at import time. Vercel builds don't have runtime env vars, causing build crashes. Use lazy getter functions instead (seegetAppUrl()instripe-service.ts). - Branded error pages exist -
app/error.tsx(runtime errors) andapp/not-found.tsx(404s) are in place. Use them as patterns for sub-route-group error handling. Both use the design system (cream/terracotta/ink). - In-memory rate limiter is per-instance -
RateLimiteruses a static Map, so state isn't shared across Vercel serverless instances. Acceptable for beta; needs Redis/database-backed solution at scale. - Client components cannot import server-only modules -
'use client'files cannot transitively import modules that usenext/headersor other server-only APIs.session-primitives.tsimportssupabase/server.ts, so client components must not import from it. Shared types/constants go in a separate client-safe file (e.g.,pathway-labels.ts). This broke a Vercel build. - Heavy libraries must be dynamically imported - Mermaid (~250KB gzip), Excalidraw (~500KB), and similar large packages must use
next/dynamicwithssr: false. Never import at module scope in components rendered on every page. Gate behind user action or lazy-load on first use. - Sanitize dangerouslySetInnerHTML from third-party libs - Any
dangerouslySetInnerHTMLwith output from mermaid, markdown renderers, or similar must be sanitized with DOMPurify. Mermaid'ssecurityLevel: 'strict'has had bypasses (CVE-2023-20052). Defense in depth. - Radix Dialog for all new modals - Hand-rolled modals lack focus trap, Escape handling, and aria-modal.
@radix-ui/react-dialogis installed. Use it for all new modals. Existing hand-rolled modals (SignupPromptModal, ExportDialog, BranchDialog) are tracked for migration.
- Vercel: Auto-deploys on push to main
- Manual:
cd apps/web && vercel --prod - Env vars: Set via Vercel dashboard (Settings -> Environment Variables)
- URL: https://thinkhaven.co