This file provides context for Claude Code when working on the web UI codebase.
The tldw Web UI is a Next.js application that serves as a thin wrapper around shared UI code from packages/ui/src/. It provides:
- Browser-based access to tldw features (chat, media, RAG, etc.)
- JWT authentication for multi-user mode
- Browser API shims for code shared with the extension
This app is part of a bun workspace monorepo that shares UI code with the browser extension.
Key directories:
packages/ui/src/— Shared components, hooks, services, routes, storestldw-frontend/— This directory: Next.js wrapper, shims, web-only codeextension/— WXT browser extension (separate build)
For feature parity guidelines, see: ../DEVELOPMENT.md
tldw-frontend/
├── pages/ # Next.js pages (thin wrappers around shared routes)
├── extension/
│ └── shims/ # Browser API compatibility shims
│ ├── wxt-browser.ts # localStorage-based browser.* shim
│ └── react-router-dom.tsx # Next.js router shim for react-router-dom
├── hooks/ # Web-only hooks (auth/config are shared — see @/services/tldw/TldwAuth)
├── lib/ # Web-only utilities
│ ├── api.ts # Fetch wrapper with auth
│ └── auth.ts # Token management
├── components/ # Web-only components (rare)
├── types/ # Web-specific TypeScript types
└── styles/ # Global styles, Tailwind imports
Shared code (imported from @/):
packages/ui/src/components/— React componentspackages/ui/src/hooks/— React hookspackages/ui/src/services/— API calls, business logicpackages/ui/src/store/— Zustand state storespackages/ui/src/routes/— Page-level components
| Alias | Resolves To | Usage |
|---|---|---|
@/ |
packages/ui/src/ |
Shared components, hooks, services |
~/ |
packages/ui/src/ |
Alternative shared path |
@tldw/ui |
packages/ui/src/ |
Explicit shared package import |
@web/* |
tldw-frontend/* |
Web-only modules |
// Shared UI (from packages/ui/src/)
import { MyComponent } from "@/components/MyComponent"
import { useMyHook } from "@/hooks/use-my-hook"
import { myService } from "@/services/my-service"
import { tldwAuth } from "@/services/tldw/TldwAuth" // shared auth (live path)
// Web-only (from tldw-frontend/)
import { api } from "@web/lib/api"
// Browser APIs (automatically shimmed)
import { browser } from "wxt/browser" // Uses localStorage shim
import { Link, useNavigate } from "react-router-dom" // Uses Next.js router shimAll pages must disable SSR because shared code uses browser APIs:
// pages/my-feature.tsx
import dynamic from "next/dynamic"
export default dynamic(() => import("@/routes/my-feature-page"), { ssr: false })Why: Shared code uses IndexedDB (Dexie), browser storage APIs, and window which don't exist during SSR.
The extension/shims/ directory provides compatibility layers:
wxt-browser.ts — Maps browser.storage to localStorage:
// Shared code calls:
await browser.storage.local.set({ key: "value" })
// Shim translates to:
localStorage.setItem("key", JSON.stringify("value"))react-router-dom.tsx — Maps react-router to Next.js router:
// Shared code uses:
import { Link, useNavigate } from "react-router-dom"
// Shim provides:
// <Link to="/path"> → <NextLink href="/path">
// useNavigate().push() → useRouter().push()Auth state lives in the shared stack (@/services/tldw/TldwAuth + the @/store/connection
store), not a web-only hook. Pages stay thin wrappers; auth is resolved inside the shared route
component:
// pages/protected-page.tsx
import dynamic from "next/dynamic"
// Auth is resolved inside the shared route via tldwAuth / the connection store.
export default dynamic(() => import("@/routes/protected-route"), { ssr: false })When behavior must differ:
// packages/ui/src/utils/platform.ts
export const isExtension = typeof chrome !== "undefined" && chrome.runtime?.id
// Usage in shared code:
if (isExtension) {
// Extension-specific behavior
} else {
// Web-specific behavior
}# From apps/tldw-frontend/
# Development (use port 8080 for CORS compatibility)
npm run dev -- -p 8080
# or
bun run dev -- -p 8080
# Type checking
npm run lint
bun run --cwd ../packages/ui tsc --noEmit
# Tests
npm run test # Unit tests (Vitest)
npm run test:integration # Full integration tests
npm run smoke # API connectivity check
# Production build
npm run buildCopy .env.local.example to .env.local:
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_API_URL |
Backend URL | http://127.0.0.1:8000 |
NEXT_PUBLIC_API_VERSION |
API version | v1 |
NEXT_PUBLIC_X_API_KEY |
Single-user API key | — |
NEXT_PUBLIC_API_BEARER |
Bearer token for chat | — |
# Unit tests
npm run test
# Integration tests (starts backend if needed)
npm run test:integration
# Smoke test (API connectivity)
NEXT_PUBLIC_API_URL=http://127.0.0.1:8000 \
NEXT_PUBLIC_X_API_KEY=your_key \
npm run smokeWrong:
// pages/my-page.tsx
import MyRoute from "@/routes/my-route"
export default MyRoute // Breaks SSR!Right:
import dynamic from "next/dynamic"
export default dynamic(() => import("@/routes/my-route"), { ssr: false })Wrong:
// packages/ui/src/components/MyComponent.tsx
import { api } from "@web/lib/api" // Breaks extension!Right: Keep web-only imports in tldw-frontend/pages/ wrappers only.
Wrong:
chrome.storage.local.get("key") // No shim for chrome.*Right:
import { browser } from "wxt/browser" // Uses shim
browser.storage.local.get("key")next— React framework with SSR/SSG@tanstack/react-query— Server state managementzustand— Client state managementtailwindcss— Utility CSSantd— UI component libraryaxios— HTTP client
- ../DEVELOPMENT.md — Feature parity workflows
- ../extension/CLAUDE.md — Extension development guide
- README.md — Setup and deployment details