## Skill loading (Agent Skills) This repo publishes **[Agent Skills](https://agentskills.io)** as `.agents/skills//SKILL.md`. That is the **only** skill format — do not load TanStack Intent package skills or a parallel YAML skill schema for this template. Before substantial work: - Read the matching local skill under `.agents/skills/` (already in this repo; no extra install). - If a companion is missing in another project, install with `npx skills` (or `gh skill install`): ```bash npx skills add carlosvin/tanstack-fullstack-ai-template --list npx skills add carlosvin/tanstack-fullstack-ai-template npx skills add carlosvin/tanstack-fullstack-ai-template --skill ``` - Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional companion skills only when the task spans those concerns. # Agent Instructions This document is the default agent and contributor guide for projects built from this template. It covers project structure, conventions, tooling, and operational detail. The **architectural contract** lives in the [TanStack Promptable Fullstack App Template skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) ([agentskills.io](https://agentskills.io) `SKILL.md`; companions via `pnpm skills:check`, spec via `pnpm skills:waza`). Opinionated package choices for *this* template (Zod, Mantine, MongoDB, pino, Sentry, …) live in the [reference-tech-stack skill](.agents/skills/reference-tech-stack/SKILL.md). Language-agnostic repository ownership — collection repositories, indexes, stale-data cleanup, and resource lifetime — lives in the [repository-architecture skill](.agents/skills/repository-architecture/SKILL.md). Shared prompt UX — mobile first, assistant markdown, and the two prompt concepts — lives in the [promptable-ux skill](.agents/skills/promptable-ux/SKILL.md). The fully agentic shell lives in the [agentic-ux skill](.agents/skills/agentic-ux/SKILL.md). When it is not clear which user experience to implement, the architecture skill **Choose a UX** asks which of the three and waits. **This handbook** covers project layout, UI kit, auth wiring, AI adapter setup, observability recipes, and validation commands — not alternate architecture. Before non-trivial changes: read the skill **Core Contract** and run the **Architecture Checklist**. ## Skill alignment roadmap This template is the reference app for the skill. **Already landed on `main`:** server-only boundaries ([#7](https://github.com/carlosvin/tanstack-fullstack-ai-template/pull/7)), request-context via middleware `next({ context })` + chaining ([#8](https://github.com/carlosvin/tanstack-fullstack-ai-template/pull/8), refined in [#12](https://github.com/carlosvin/tanstack-fullstack-ai-template/pull/12)), skill v1.19 Request Context rules, handbook dedupe ([#9](https://github.com/carlosvin/tanstack-fullstack-ai-template/pull/9)), Cursor Cloud setup ([#10](https://github.com/carlosvin/tanstack-fullstack-ai-template/pull/10)). **Observability & env ([#6](https://github.com/carlosvin/tanstack-fullstack-ai-template/pull/6)):** companion skill `observability-and-env` — Zod env schemas (`src/env/webEnv.server.ts`: `webServerEnv` + `shellSession`; client-safe `src/services/schemas/shellSession.ts`), pino, `instrument.*.mts` bootstrap, middleware-injected context, `getBrowserShellSession` via root loader (no `window.__ENV__`). | Phase | Skill contract | Status / files | |-------|----------------|----------------| | **0 — Observability & env** | Centralized env parse once; pino; Sentry; `webServerEnv` + `shellSession` | **Done** — `src/env/webEnv.server.ts`, `src/services/schemas/shellSession.ts`, `instrument.*.mts`, `webEnvMiddleware`, `getBrowserShellSession` | | **1 — Schema boundaries** | Outbound `Schema.parse()` (repo → tools); router defaults bundle; `Link` with `search: true` | **Done** — `serverFns.ts`, `taskMappers.ts`, `router.tsx` (`defaultNotFoundComponent`), `src/components/Link/Link.tsx` | | **2 — Auth & writes** | Auth ticket + `TraceabilityContext` (skill allows stock `user`/`userProfile` until enriched) | **Done** — `accessTicket` from identity + `getUserProfile`; `TraceabilityContext` on create/update | | **3 — Data loading & AI** | Parent loader dedup; `getAIAvailability()` gates chat UI; distinct-values tools | **Done** — `__root.tsx`, task routes, `Header`, `AppLayout`, `ChatDrawer`, `getDistinctValues` | | **4 — Hardening** | `createServerOnlyFn`; `PUBLIC_ROUTES`; router introspection for nav manifest | **Done** — `PUBLIC_ROUTES`, `*.server.ts` + import protection, `createServerOnlyFn` on DB/repo factories; `buildAppNavigation(router.routesById)` | | **5 — Deploy** | Side + prompt-first + agentic demos (same repo, `PROMPT_CONCEPT` per Netlify site) | Side: [fullstack-promptable-app-example.netlify.app](https://fullstack-promptable-app-example.netlify.app) | **CI/CD (Netlify-native):** GitHub Actions validates PRs and `main` (`.github/workflows/ci.yml`: lint, test, build; `.github/workflows/skills.yml`: skill drift, static skill evals, and [Waza](https://microsoft.github.io/waza/) readiness). Netlify Git integration handles all deploys — **deploy previews** on pull requests and **production** on merge to `main` (`netlify.toml` → `pnpm build`, publish `dist`). With `NETLIFY=true` (set automatically on Netlify), `@netlify/vite-plugin-tanstack-start` writes static assets to `dist/` and SSR to `.netlify/`; local non-Netlify builds still emit `.output/public`. No `NETLIFY_AUTH_TOKEN` secrets in GitHub. In Netlify: production branch `main`, deploy previews on, branch deploys off. **Versioning (SemVer):** App version lives in `package.json` and is exposed via `shellSession.app.version`. Releases are automated with [Release Please](https://github.com/googleapis/release-please) (`.github/workflows/release-please.yml` + `release-please-config.json` + `.release-please-manifest.json`). Use [Conventional Commits](https://www.conventionalcommits.org/) on every commit (`feat:`, `fix:`, `BREAKING CHANGE` / `type!:` for major). PR commits are linted by `.github/workflows/commitlint.yml`. On push to `main`, Release Please opens/updates a **Release PR** that bumps `package.json`, updates `CHANGELOG.md`, and — when merged — tags `vX.Y.Z` and creates a GitHub Release. Do **not** hand-edit the version in feature PRs. Deploys stay on Netlify; the UI shows the version from the commit Netlify builds (typically the merged Release PR). No npm publish (`private: true`). If Release Please PRs skip CI (GITHUB_TOKEN limitation), configure a PAT/GitHub App token as `RELEASE_PLEASE_TOKEN` and pass it to the action. **Phase 5 checklist:** `pnpm format && pnpm lint && pnpm test && pnpm build` → merge → Netlify auto-deploy from `main` → smoke-test **all** demos. **Netlify env** (post-#6): `SENTRY_DSN`, `ENV`, `LOG_LEVEL`, `REPOSITORY_TYPE=seed`, `OPENAI_API_KEY` (or chosen provider) — not `VITE_SENTRY_DSN`. **Prompt demos:** default site uses `PROMPT_CONCEPT=side` (or unset); link a second Netlify site with `PROMPT_CONCEPT=prompt-first` and a third with `PROMPT_CONCEPT=agentic`, each with a distinct `DISPLAY_NAME` (see README). See the skill **Implementation Flow** (and its **Per-entity 3-UX checklist** + **Tri-modal UX architecture**) for the per-entity file checklist when adding domain entities across `side`, `prompt-first`, and `agentic`. **Example-preview workflow (all prompt demos):** every change to the example app shell or a skill contract must load the matching skill(s) first, update `.agents/skills/*/SKILL.md` when the contract changes, and land via PR so **all** Netlify sites build deploy previews (side drawer + prompt-first + agentic). Smoke-test all preview URLs before merge — a UI change is not done when only one preview is checked. ## 1. General Principles - **SOLID, DRY, YAGNI, KISS**: Adhere to these fundamental software design principles. - **Readability & Clarity**: Code must be easy to read and understand. - **Modularity & Reusability**: Write code that can be reused. - **Pragmatic Programming**: Be practical and realistic in your approach. - **Clean Code**: Follow clean code best practices. - **Interface-First**: All external services (database, AI, observability) are accessed through interfaces, not concrete implementations. ## 2. File and Project Structure - `src/components`: React components. Each component in its own folder with `Component.tsx` and optional `Component.module.css` and `Component.test.tsx`. Page-level components (e.g. `DashboardPage`, `TasksPage`) also live here so they can be tested independently. - `src/routes`: TanStack Router file-based routes — thin route config only; page UI in `src/components/`. See skill **Core Contract** #7. Route tree: `routeTree.gen.ts`. - `src/middleware`: TanStack Start middleware (auth, invalidation). Registered globally in `src/start.ts`. - `src/services/api`: Server functions (exported directly from `createServerFn`) and shared response utilities. - `src/services/repository`: Repository interface, implementations (MongoDB, seed), and the factory. - `src/services/schemas`: Centralized Zod schemas — tools-layer schemas in `schemas.ts`, repository-layer schemas in `repository.ts`. See the [skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) for the full three-layer model. - `src/services/ai`: AI adapter interface, implementation, and tool definitions. - `src/services/observability`: Observability interface with Sentry and no-op implementations. - `src/services/db`: Database client singleton. - `src/utils`: Generic helper functions (JWT, HTTP errors, auth guards). - `src/constants`: Shared constant arrays and enums. - `src/test-utils`: Vitest helpers. ### File Naming - Use `PascalCase` for component files (e.g., `MyComponent.tsx`). - Use `camelCase` for utility, service, and route files. - Test files are co-located with source as `*.test.ts` or `*.test.tsx`. Page components receive loader data as props and render in Vitest with `renderWithProviders()` without a running router. Shared display helpers belong in `src/utils/`. Examples: `src/routes/index.tsx`, `src/routes/tasks/index.tsx`. ### Code Organization - Place React components in their own files. - Group related utility functions in a single file. - Functions and methods should have a single responsibility. - Avoid mixing unrelated code in the same file. ## 3. Styling and UI Components ### Mantine UI Framework This project uses [Mantine](https://mantine.dev/) as the primary UI framework. - **Use Mantine components first**: Before creating custom components, check if Mantine provides one. Agentic views follow the same order in the [agentic-ux skill](.agents/skills/agentic-ux/SKILL.md): a kit component before a custom composite, and a chart when the view needs to explain numbers. - **Styling props**: Use Mantine's built-in props (`c`, `fw`, `size`, `variant`, etc.) instead of CSS. - **CSS Modules**: When custom CSS beyond Mantine is needed, use CSS Modules (`Component.module.css`). - **Responsive props**: Use Mantine's object syntax for mobile-first values: `` (`base` is the narrow viewport; larger keys enhance). - **Mobile first (default):** [Progressive enhancement from small viewports up](https://developer.mozilla.org/en-US/docs/Glossary/Mobile_First) — not a particular widget. Start from `base` styles and add `sm` / `md` / `lg`. For navigation that overflows a narrow header, Mantine `AppShell` + `Burger` (`hiddenFrom="sm"`) + `NavLink` (project `Link`) is the kit recipe. **Ask the developer** which user experience to implement when it is not clear (side or prompt-first in the [promptable-ux skill](.agents/skills/promptable-ux/SKILL.md), or the fully agentic shell in the [agentic-ux skill](.agents/skills/agentic-ux/SKILL.md)) and wait for an answer. Both route-based concepts stay mobile first. The **default deployment of this repo** declares **Promptable UI (side)** from the [promptable-ux skill](.agents/skills/promptable-ux/SKILL.md): the prompt is a right `Drawer`, not `AppShell.Aside`. A **second Netlify site** can set `PROMPT_CONCEPT=prompt-first` for the always-visible `PromptBar` shell (same routes and loaders). A **third** sets `PROMPT_CONCEPT=agentic`. One concept per site — do not enable both shells in one deployment. - **Theming**: Customize the theme in `__root.tsx`. Use Mantine CSS variables (`--mantine-color-*`) in CSS Modules. - **Avoid inline styles**: Use Mantine props or CSS Modules instead. - **Avoid `!important`**: Minimize its use. - **Dark mode**: All components must work in both light and dark color schemes. ### Icons This project uses [`lucide-react`](https://lucide.dev/) as the default icon library. Lucide icons are tree-shakeable and visually consistent across the template. Import individual icons by name: ```tsx import { CheckCircle, Trash2 } from 'lucide-react' ``` If your team prefers [`@tabler/icons-react`](https://tabler.io/icons) (the Mantine-adjacent set), it is a compatible alternative — but keep one library per project to avoid visual inconsistency. ## 4. TypeScript and React - **Server Components by Default**: Rely on Server Components; use `"use client"` only for hooks or browser APIs. Keep client components leaf-level. - **Functional Components**: Prefer functional components and hooks over class components. - **Type Reuse**: Import domain types from `@/services/schemas/schemas` (the module that defines them). Do not redefine existing types. - **Imports**: Import the defining file with the `@/` alias (`@/*` → `./src/*`). Do not add `index.ts` barrels that only re-export. `routes/**/index.tsx` stays, because TanStack Start file routing requires that name. - **Architecture** (schema layers, URL-as-state, `loaderDeps`, `satisfies` over `as`, trust-boundary `Schema.parse` / `validateSearch`): follow the [skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) **Core Contract** #3–5, #7–8, **URL-Recoverable State & Prop-Driven Components**, and **Schema Boundaries**. Tools-layer schemas: `src/services/schemas/schemas.ts`; repository-layer: `src/services/schemas/repository.ts`. After data crosses a repository or `validateSearch` edge, keep schema-inferred types — parse untrusted widget strings with the same schema `.parse()`, not a tuple `.find()`. - **Recoverable URL State & Avoiding `useState` / `useEffect` Overuse**: Treat `useState` and `useEffect` overuse in routes and page components as a code smell. Every user-meaningful page state (filters, search, sort, multi-select `selected`, bulk status choice `bulkStatus`, create/edit modals via nested routes) must be recoverable from the URL path or `validateSearch` params. Pass loader data, URL search state, and callbacks as props to isolated, stateless page components (`TasksPage`, `DashboardPage`, `TaskDetailPage`) so they can be tested purely via props. Reset uncontrolled forms with a `key` prop (`key={`${task.id}-${task.updatedAt}`}`) instead of syncing props in `useEffect`. ## 5. Middleware and Auth **Skill contract:** repository-backed auth ticket, server-enforced guards, `TraceabilityContext` on writes — see skill **Request Context** and **Interface Contracts**. This template implements the contract: `authMiddleware` builds an `accessTicket` from the JWT identity + repository profile (roadmap phase 2). ### Files and env | Piece | Path | |-------|------| | Global middleware registration | `src/start.ts` | | JWT + profile load | `src/middleware/auth.ts` | | Mutation auth gate | `src/middleware/requireAuth.ts` | | Post-mutation invalidation | `src/middleware/invalidate.ts` | | Guard helpers | `src/utils/auth.ts` — `requireAuth`, `requireGroup` | | JWT parsing | `src/utils/jwt.server.ts` | - **JWT header:** `AUTH_HEADER_NAME` env var (default: `Authorization`). - **Mutations only:** POST server functions chain `requireAuthMiddleware`; GET queries are unauthenticated unless the handler adds auth middleware. - **Custom authorization:** Chain composable middleware off `authMiddleware` (see `requireAuth.ts`). ### Auth Context (current) ```tsx // src/middleware/auth.ts interface AuthContext { accessTicket: AccessTicket } ``` `requireAuthMiddleware` chains `authMiddleware` so `context.accessTicket` is typed in mutation handlers. ### Optional patterns (skill) - **Public-route allowlist** — implemented: `PUBLIC_ROUTE_PREFIXES` in `src/middleware/auth.ts` skips the repository profile load and mints a synthetic anonymous ticket for health/static paths. - **Pre-auth 308 redirects** — legacy URL compatibility before `authMiddleware` (skill **Implementation Flow** #5; not yet in this template). ## 6. Server Functions and Data Access Full-stack TanStack Start — no separate backend API. Architecture (layering, isomorphic loaders, tool coverage, boundary parsing): [skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) **Core Contract** #6, #16 and **Server execution boundaries**. ``` Route Loader → serverFns.ts → Repository → Database / Seed Data ``` How to split collection repositories, own indexes, and dispose connections: [repository-architecture skill](.agents/skills/repository-architecture/SKILL.md). `ensureRepository()` in `getRepository.server.ts` is the composition root: it connects, then constructs `MongoRepository` with `new`. `repositoryMiddleware` injects that instance as `context.repository`. Handlers do not await a repository factory. `MongoRepository` delegates to `MongoTaskRepository` and `MongoUserRepository`, which bind their collections and declare indexes. This app has no refresh-window sweep, so disposal does not delete documents. | Concern | Where | |---------|--------| | RPC entry points | `src/services/api/serverFns.ts` — all `createServerFn` exports | | UI mutation errors | `src/services/api/processResponse.ts` | | AI tool errors | `src/services/ai/serverTool.ts` — `createSafeServerTool`, `safeToolHandler` | | Composition root | `src/services/repository/getRepository.server.ts` — `ensureRepository()` | | Request injection | `src/middleware/repository.ts` — `context.repository` | | Import protection | `vite.config.ts` → `tanstackStart({ importProtection })` | **Calling convention:** `getTasks({ data: filter })` — pass `{ data: … }` to server functions. **Mutations (template pattern):** ```tsx export const myMutation = createServerFn({ method: 'POST' }) .middleware([requireAuthMiddleware, invalidateMiddleware]) .inputValidator(MyInputSchema) .handler(async ({ data, context }) => { const trace = createWriteTrace(context.accessTicket.identity.email) return context.repository.doSomething(data, trace) }) const result = await processResponse(() => myMutation({ data: input })) ``` Call mutations from event handlers; show toast feedback on success/failure. ## 7. Routing (TanStack Router) Routes live in `src/routes/`; tree is auto-generated in `routeTree.gen.ts`. Router config: `src/router.tsx`. **Architecture** (`validateSearch`, `loaderDeps`, parent layout loaders, router defaults bundle, project `Link` wrapper): skill **Core Contract** #7–9, #15. **Operational tip:** preserve search params with the functional navigate form — `navigate({ to: '/path', search: prev => ({ ...prev, newParam: 'value' }) })` — to avoid unnecessary re-renders from reading `useSearch()` only to spread into `navigate`. ## 8. AI Chat and Tools **Architecture** (tool coverage, `browserContext`, bounded agent loop, prompt structure, virtual fields): [skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) **Core Contract** #11–14 and **Implementation Flow** #4, #7. **Prompt concept:** [promptable-ux skill](.agents/skills/promptable-ux/SKILL.md). This reference app implements **Promptable UI (side)** — the prompt is hidden until opened. **Prompt-first** is the other concept in that skill; do not add a second always-visible prompt beside this drawer. **This section:** adapter setup, file paths, chat UI wiring, navigation manifest maintenance. | File | Role | |------|------| | `src/services/ai/adapter.ts` | Provider implementation (Gemini → OpenAI/Gateway → Azure) | | `src/services/ai/tools.ts` | Server + client tool definitions | | `src/services/ai/serverTool.ts` | `createSafeServerTool` / `safeToolHandler` | | `src/services/ai/navigationManifest.ts` | AI route matching + prompt section; `buildAppNavigation(router)` from `routesById` | | `src/routes/api/chat.ts` | SSE endpoint, `BASE_SYSTEM_PROMPT`, `buildSystemPrompt` | | `src/components/PromptChat/PromptChatContext.tsx` | Side + prompt-first: `useChat`, client `navigate` / `invalidateRouter`, `browserContext` | | `src/components/AgenticChat/AgenticChatContext.tsx` | Agentic: `useChat` without route client tools | | `src/components/AgenticShell/AgenticThread.tsx` | Shared markdown + A2UI thread (`AgenticMessageItems`) used by all three shells | | `src/components/ChatDrawer/ChatDrawer.tsx` | Side chrome only (drawer wrapping shared `ChatThread` / `ChatComposer`) | **Promptable gating (skill #12):** root loader calls `getAIAvailability()`; `AppLayout` / `Header` mount chat UI only when configured (no disabled placeholder). ### AI Adapter Interface The AI provider is behind the `AIAdapterService` interface defined in `src/services/ai/types.ts`: ```tsx interface AIAdapterService { isConfigured(): boolean getMissingConfigMessage(): string | null getAdapter(): unknown | null } ``` **When scaffolding a new project or when the LLM preference is not clear, ask the user which provider they want.** TanStack AI has adapter packages for multiple providers: | Provider | Adapter package | Required env var(s) | Notes | |----------|----------------|---------------------|-------| | OpenRouter (recommended for greenfield) | `@tanstack/ai-openrouter` | `OPENROUTER_API_KEY` | 300+ models with a single API key | | Google Gemini | `@tanstack/ai-gemini` | `GEMINI_API_KEY` | First match in this template’s `adapter.ts` | | OpenAI / Netlify AI Gateway | `@tanstack/ai-openai` | `OPENAI_API_KEY` (or gateway placeholders) | Second match; gateway via `OPENAI_BASE_URL` / Netlify vars | | Anthropic | `@tanstack/ai-anthropic` | `ANTHROPIC_API_KEY` | Claude series | | Azure OpenAI | `@tanstack/ai-openai` | `AZURE_OPENAI_*` | Third match in this template | | Ollama (local) | `@tanstack/ai-ollama` | `OLLAMA_BASE_URL` | Local models, no API key needed | | Groq | `@tanstack/ai-groq` | `GROQ_API_KEY` | Fast inference | | xAI Grok | `@tanstack/ai-grok` | `GROK_API_KEY` | Grok series | Install only the chosen adapter package(s), implement `AIAdapterService` in `src/services/ai/adapter.ts`, and configure the matching env vars. This reference app’s `adapter.ts` selects the first configured provider: **Gemini → OpenAI / Netlify AI Gateway → Azure OpenAI**. Set `GEMINI_API_KEY` (or `OPENAI_API_KEY` / Azure vars) to enable AI chat. Optionally set `GEMINI_MODEL`, `OPENAI_MODEL` (default: `gpt-4o`), and `OPENAI_BASE_URL` for proxies or compatible APIs. ### Server Tools Definitions in `src/services/ai/tools.ts` — each calls the same `serverFns.ts` exports as routes. When adding repository methods, add matching `toolDefinition` + `createSafeServerTool` entries (skill **Core Contract** #11). Task list views (`showTasksView`) take a `presentation` of `cards`, `table`, `kanban`, `timeline`, or `chart`. `chart` draws totals and breakdown charts from the same task payload. ### Client Tools Client tools execute in the browser and are defined in `src/services/ai/tools.ts` (definition-only, no `.server()` call) with `.client()` implementations in [PromptChatContext](src/components/PromptChat/PromptChatContext.tsx) using `clientTools()` from `@tanstack/ai-client` (side + prompt-first only; agentic omits them): - **navigate**: Triggers `router.navigate()` in the browser. Accepts `to` (path) and optional `search` (query params). Validates paths via `matchUserFacingRoute()` (`$param` patterns from `UserFacingTo`). - **invalidateRouter**: Calls `router.invalidate()` to refresh page data. The AI calls this after mutation tools so the user sees updated data. When adding client tools: export the `toolDefinition(...)` from `tools.ts`, pass it to `chat()` in the chat endpoint for side/prompt-first, and add a `.client()` implementation in `PromptChatContext.tsx` via `clientTools()`. ### Chat Client Setup The chat UI uses `useChat` from `@tanstack/ai-react` and `fetchServerSentEvents` from `@tanstack/ai-client` to connect to the `/api/chat` SSE endpoint. Client tools are wired with `clientTools()` and passed to `useChat` — they execute automatically when the AI calls them (no `onToolCall` callback needed): ```tsx import { useChat, fetchServerSentEvents } from '@tanstack/ai-react' import { clientTools, createChatClientOptions } from '@tanstack/ai-client' const navigateClient = navigateToolDef.client((args) => { router.navigate({ to: args.to, search: args.search ?? undefined }) return { success: true } }) const tools = clientTools(navigateClient, invalidateClient) const { messages, sendMessage, isLoading } = useChat( createChatClientOptions({ connection: fetchServerSentEvents('/api/chat'), tools, body: { browserContext }, }), ) ``` `createChatClientOptions()` enables full type inference for messages via `InferChatMessages`. The `body` field passes additional data (like `browserContext`) alongside messages to the server endpoint. ### App navigation and links When routes or `validateSearch` schemas change, set `staticData.description` on the route and `.describe()` on search fields. `buildAppNavigation(router)` derives the AI prompt from `router.routesById`. Path matching uses exhaustive `UserFacingTo` `$param` patterns in `navigationManifest.ts` (typecheck fails if a new user-facing `to` is missing). Markdown link examples in the prompt are generated from the derived route list. - Internal links in assistant messages: `ChatDrawer` `MarkdownLink` → TanStack Router `Link` with `preload="intent"`. - AI uses markdown links in replies; **navigate** client tool for programmatic navigation. ### AI System Prompt Context `buildSystemPrompt()` in `src/routes/api/chat.ts` merges `BASE_SYSTEM_PROMPT`, navigation manifest, and dynamic blocks (Current User, Browser Context, Current Location). Clients send `browserContext` from `PromptChatContext` / `AgenticChatContext` (agentic sets `surface: 'agentic'` and omits path fields); server validates with `BrowserContextSchema` in `schemas.ts`. When adding dynamic route segments, add pattern-matching in `buildSystemPrompt()` so the AI can resolve "this task" to the current id. ### Chat Drawer Convention This is the **Promptable UI (side)** recipe. The AI chat is a Mantine [`Drawer`](https://mantine.dev/core/drawer/) on the **right** (`position="right"`, `size="lg"`, full viewport below `sm`). It stays closed until the user opens it from the header. The drawer mounts at the root layout (`AppLayout`) so messages persist across route navigation. `useDisclosure` from `@mantine/hooks` controls open/close. A bottom panel is the same concept; this app uses the side. **Prompt-first** does not use this drawer. ### Chat Drawer Rendering Assistant messages are rendered with [`react-markdown`](https://github.com/remarkjs/react-markdown) + [`remark-gfm`](https://github.com/remarkjs/remark-gfm) inside a `div` with the `.markdown` CSS Module class from `PromptChat.module.css` (shared by `ChatDrawer` and `PromptBar`). - **Tables**: GFM tables render as native `` elements styled in the CSS Module with Mantine CSS variables (`--mantine-color-default-border`, `--mantine-color-default-hover`, `--mantine-font-size-xs`). Do **not** use Mantine `Table` components for markdown output — extend the `.markdown` CSS class instead. - **Code blocks**: `pre` and `code` elements are styled in the same CSS Module using `--mantine-font-family-monospace` and `--mantine-radius-md`. No syntax highlighting library is included by default. - **Internal links**: A custom `MarkdownLink` component detects internal paths (starts with `/`, not `/api/`) and renders TanStack Router `Link` with `preload="intent"` and query param parsing. Clicking navigates the app via client-side routing without closing the drawer or losing chat history — the drawer is mounted at the root layout level (`AppLayout`) so its `useChat` message state survives route changes. External links render as ``. - **Adding markdown features**: To support new markdown elements (e.g. syntax highlighting, custom block renderers), add a `components` entry to the `Markdown` component in `ChatDrawer.tsx` and extend the `.markdown` CSS Module rules using Mantine CSS variables for theme consistency. ### System Prompt Generation For new projects, ask the user about domain entities and permissions, then tailor `BASE_SYSTEM_PROMPT` in `src/routes/api/chat.ts` (do not copy the template task prompt verbatim). Section structure: skill **Implementation Flow** #7. ### Chat Endpoint (`src/routes/api/chat.ts`) The chat endpoint is a TanStack Start file-based route at `/api/chat` with two server handlers. It is the central wiring point that connects the AI adapter, system prompt, tools, and auth context. **File structure:** `BASE_SYSTEM_PROMPT` (domain-specific static prompt) → `buildSystemPrompt()` (composes base + manifest + dynamic context) → GET handler (`{ available }`) + POST handler (SSE stream). **GET handler** — `{ available: boolean }` from `getAIAdapterService().isConfigured()`. **POST handler flow:** check adapter → parse `{ messages, browserContext }` → `buildSystemPrompt` → assemble tools → `chat({ …, agentLoopStrategy: maxIterations(N) })` → SSE via `toServerSentEventsResponse`. Set `maxIterations` explicitly (skill **Core Contract** #13). When modifying: add server tools to the `tools` array; add client tool defs and wire `.client()` in `PromptChatContext` for side/prompt-first; update `BASE_SYSTEM_PROMPT` and `navigationManifest.ts` when domain or routes change. ## 9. Observability **Architecture & setup:** [observability-and-env skill](.agents/skills/observability-and-env/SKILL.md) (landed via PR #6). **This section:** file map and usage in this repo. | Piece | Path | |-------|------| | Interface | `src/services/observability/types.ts` | | Sentry / no-op | `sentry.ts`, `noop.ts`; factory `getObservability.ts` | | Server bootstrap | `instrument.env.mts` → `instrument.shared.mts` → `instrument.server.mts` (emitted `.mjs` in `.output/server`) | | Env schemas | `src/env/webEnv.server.ts` (server-only; `importProtection` denies `**/env/**` in the client graph); client-safe `src/services/schemas/runtimeEnv.ts`, `src/services/schemas/shellSession.ts` | | Pino | `src/utils/logger.ts`, `src/utils/serverLogger.ts` | | Public env middleware | `src/middleware/webEnv.ts` | | Public env + app meta for client | `getBrowserShellSession` in `serverFns.ts` + root loader (not `window.__ENV__`) | | Typed request context | `webEnvMiddleware` via `next({ context })`; chain middleware on consumers | - **Usage:** `getObservability().startSpan('name', fn)` in server handlers; `createServerLogger('module')` for structured logs. - **Env vars:** `SENTRY_DSN`, `ENV`, `LOG_LEVEL` — see `.env.example`. Keep `NODE_ENV` and `ENV` aligned in production so Sentry tags match runtime behavior. ## 10. Testing ### Unit Tests (Vitest) - **Framework**: Vitest with jsdom environment. - **Libraries**: `@testing-library/react`, `@testing-library/dom`. - **Setup**: Global setup in `src/test-utils/setupTests.ts` (includes `matchMedia`, `ResizeObserver`, `MutationObserver` mocks). - **Rendering**: `renderWithProviders()` in `src/test-utils/renderWithRouter.tsx` wraps with MantineProvider. - **Convention**: Test files co-located as `*.test.ts` or `*.test.tsx`. ### E2E Tests (Playwright) - **Framework**: Playwright with Chromium. - **Config**: `playwright.config.ts` — single worker, serial CRUD tests, `REPOSITORY_TYPE=seed`. - **Auth fixture**: `e2e/auth.ts` provides `authedPage` / `authedContext` fixtures using unsigned JWTs sent via `extraHTTPHeaders`. - **Convention**: Spec files in `e2e/` as `*.spec.ts`. - **Running**: `pnpm test:e2e` (reuses existing dev server or starts one with seed data). ## 11. Linting and Formatting This project uses [Biome](https://biomejs.dev/) as the default linter and formatter — **not** ESLint or Prettier. The configuration lives in `biome.json` with `recommended` rules and minimal overrides. When adding new rules, prefer Biome's built-in `recommended` set and keep custom overrides to a minimum. ## 12. Dependency Management - **Always use latest versions**: When adding dependencies, run `pnpm add ` without a version suffix so the package manager resolves the newest release. Never pin exact versions unless there is a known incompatibility. - **Keep dependencies up to date**: Run `pnpm outdated` to check for stale packages and `pnpm update` to align the lockfile with the latest compatible versions within current ranges. - **Major version upgrades are conscious decisions**: When `pnpm outdated` shows a major version bump, upgrade explicitly with `pnpm add @latest`, then verify with `pnpm lint && pnpm test && pnpm build` before committing. - **After any dependency change**, run the full validation checklist (section 15) to catch regressions. ## 13. Public Runtime Config Handled by the [observability-and-env skill](.agents/skills/observability-and-env/SKILL.md) (PR #6): startup-validated `webServerEnv` / `webPublicEnv` / `appMeta` on middleware context; browser-safe projection via `getBrowserShellSession` in the root loader — **not** `window.__ENV__` or `import.meta.env` for deployment-specific values. ## 14. Special Patterns Bulk edit (URL multi-select), overlay repository, distinct-values tools, help surface, controlled components, debounced free-text search (uncontrolled input + `useDebouncedCallback`): [architecture skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) **Special Patterns** and **Core Contract** #7–8. [Mobile first](https://developer.mozilla.org/en-US/docs/Glossary/Mobile_First) and the two prompt concepts (**Promptable UI (side)**, **Prompt-first**): [promptable-ux skill](.agents/skills/promptable-ux/SKILL.md). The fully agentic shell: [agentic-ux skill](.agents/skills/agentic-ux/SKILL.md). This app uses the side concept. Mantine recipe in §3. **Ask the developer** which user experience to implement when it is not clear, and wait. ## 15. Validate Changes Always verify changes with: ```bash pnpm format # Auto-fix formatting (Biome) pnpm lint # Check for lint errors and type errors (Biome + tsc) pnpm test # Run unit tests (Vitest) pnpm test:e2e # Run E2E tests (Playwright, requires dev server or lets Playwright start one) pnpm skills:waza # Waza skill readiness + spec coverage + mock evals (needs `waza` CLI) pnpm build # Verify production build ``` ### Post-Setup / Migration Verification When creating a new project from this template or migrating an existing one, confirm all of the following before considering the setup complete: 1. Dependencies installed with latest compatible versions (`pnpm install && pnpm update`). 2. `pnpm format && pnpm lint` passes with zero errors. 3. At least one unit test exists and `pnpm test` passes. 4. `pnpm build` succeeds with zero errors. ## Cursor Cloud specific instructions Package manager is **pnpm**; the standard commands live in `package.json` and section 15 above (`pnpm dev`, `pnpm lint`, `pnpm test`, `pnpm build`, `pnpm test:e2e`). Node 22 works fine even though the `Dockerfile` pins Node 24 (there is no `engines` field). The app runs on **port 3000**. - **No external services required.** With no `MONGODB_URI`, the app uses the in-memory **seed** repository, so `pnpm dev` is fully functional on its own. MongoDB, an AI provider (Gemini/Azure OpenAI), and Sentry are all optional — the app degrades gracefully when their env vars are unset. - **Auth is header-only.** The auth middleware reads a JWT from the `Authorization` header (`src/middleware/auth.ts`); there is no cookie/session login. A plain browser is therefore anonymous and cannot mutate (the "Add task" button and edit/delete icons are hidden). E2E tests inject an unsigned JWT via `extraHTTPHeaders` (`e2e/auth.ts`). To manually exercise authenticated flows in a real browser, put a small reverse proxy in front of `:3000` that adds an `Authorization: Bearer ` header (unsigned `alg:none` tokens are accepted — signatures are not verified). - **E2E gotcha — `networkidle` hangs in dev mode.** `pnpm test:e2e` defaults to starting `pnpm dev` (see `playwright.config.ts`, `reuseExistingServer: true`). The `@tanstack/devtools-vite` plugin holds an open background connection in dev, so specs that use `page.goto(..., { waitUntil: 'networkidle' })` never settle and time out. To get a clean run, start a **production** server first and let Playwright reuse it: `pnpm build` then `REPOSITORY_TYPE=seed PORT=3000 pnpm start`, then `pnpm test:e2e` in another shell. - **Mantine Select is a combobox.** E2E helpers in `e2e/helpers.ts` (`selectMantineOption`) target `getByRole('combobox')`, not `textbox`. - **Harmless noise:** `pnpm install` prints an "Ignored build scripts" warning (esbuild/swc/etc.) — build/dev/test all work anyway. `pnpm test` (Vitest) prints a "close timed out" / "module is not defined" message during teardown after all tests pass. - **Deploys are Netlify-only.** PR previews and production deploys come from Netlify Git integration, not GitHub Actions. GHA `ci.yml` validates on PRs and `main`; `skills.yml` validates published skills (including Waza); `commitlint.yml` lints Conventional Commits on PRs; `release-please.yml` opens/updates Release PRs on push to `main`. Netlify publishes. Confirm deploy previews are enabled in the Netlify site settings. - **SemVer releases:** Conventional Commits → Release Please Release PR → merge tags `vX.Y.Z`. Version in the running app comes from `package.json` at Netlify build time.