agentleFS
carlosvincarlosvin/tanstack-fullstack-ai-template/AGENTS.md

tanstack-fullstack-ai-template

AGENTS.md3 starsChanged 7 days agoMITView on GitHub

This repo publishes Agent Skills as .agents/skills/<name>/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): This document is the default agent and contributor guide for projects built from this template.…

  • Reads credentials
  • Installs packages

The file

Show its rules and text

We split this AGENTS.md into the rules it gives an agent

32 rules. About 0 are common, 13 are distinctive, and 19 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈13

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 19

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read the matching local skill under .agents/skills/ (already in this repo.

    line 6
  2. Very specific:

    If a companion is missing in another project, install with npx skills (or gh skill install):

    line 7
  3. Distinctive:

    Before non-trivial changes: read the skill Core Contract and run the Architecture Checklist.

    line 23
  4. Distinctive:

    Modularity & Reusability: Write code that can be reused.

    line 54
  5. Distinctive:

    Clean Code: Follow clean code best practices.

    line 56
  6. Distinctive:

    Interface-First: All external services (database, AI, observability) are accessed through interfaces, not concrete implementations.

    line 57
  7. Very specific:

    Use PascalCase for component files (e.g., MyComponent.tsx).

    line 76
  8. Very specific:

    Use camelCase for utility, service, and route files.

    line 77
  9. Very specific:

    Test files are co-located with source as *.test.ts or *.test.tsx.

    line 78
  10. Distinctive:

    Place React components in their own files.

    line 84
  11. Distinctive:

    Avoid mixing unrelated code in the same file.

    line 87
  12. Distinctive:

    Use Mantine components first: Before creating custom components, check if Mantine provides one.

    line 95
  13. Very specific:

    Styling props: Use Mantine's built-in props (c, fw, size, variant, etc.) instead of CSS.

    line 96
  14. Very specific:

    CSS Modules: When custom CSS beyond Mantine is needed, use CSS Modules (Component.module.css).

    line 97
  15. Very specific:

    Responsive props: Use Mantine's object syntax for mobile-first values: <SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}> (base is the narrow …

    line 98
  16. Distinctive:

    Avoid inline styles: Use Mantine props or CSS Modules instead.

    line 101
  17. Distinctive:

    Dark mode: All components must work in both light and dark color schemes.

    line 103
  18. Very specific:

    If your team prefers @tabler/icons-react (the Mantine-adjacent set), it is a compatible alternative — but keep one library per project to …

    line 113
  19. Distinctive:

    Functional Components: Prefer functional components and hooks over class components.

    line 118
  20. Very specific:

    Type Reuse: Import domain types from @/services/schemas/schemas (the module that defines them).

    line 119
  21. Very specific:

    Imports: Import the defining file with the @/ alias (@/* → ./src/*).

    line 120
  22. Distinctive:

    Call mutations from event handlers.

    line 194
  23. Very specific:

    When adding client tools: export the toolDefinition(...) from tools.ts, pass it to chat() in the chat endpoint for side/prompt-first, and …

    line 262
  24. Very specific:

    When adding dynamic route segments, add pattern-matching in buildSystemPrompt() so the AI can resolve "this task" to the current id.

    line 300
  25. Very specific:

    POST handler flow: check adapter → parse { messages, browserContext } → buildSystemPrompt → assemble tools → chat({ …, agentLoopStrategy: …

    line 327
  26. Very specific:

    When modifying: add server tools to the tools array.

    line 329
  27. Very specific:

    Convention: Test files co-located as *.test.ts or *.test.tsx.

    line 357
  28. Very specific:

    Always use latest versions: When adding dependencies, run pnpm add <pkg> without a version suffix so the package manager resolves the …

    line 373
  29. Very specific:

    Keep dependencies up to date: Run pnpm outdated to check for stale packages and pnpm update to align the lockfile with the latest …

    line 374
  30. Very specific:

    Major version upgrades are conscious decisions: When pnpm outdated shows a major version bump, upgrade explicitly with pnpm add …

    line 375
  31. Distinctive:

    Always verify changes with:

    line 388
  32. Distinctive:

    When creating a new project from this template or migrating an existing one, confirm all of the following before considering the setup …

    line 401

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

Skill loading (Agent Skills)

This repo publishes Agent Skills as .agents/skills/<name>/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):
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 <id>
  • 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 (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. Language-agnostic repository ownership — collection repositories, indexes, stale-data cleanup, and resource lifetime — lives in the repository-architecture skill. Shared prompt UX — mobile first, assistant markdown, and the two prompt concepts — lives in the promptable-ux skill. The fully agentic shell lives in the agentic-ux skill. 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), request-context via middleware next({ context }) + chaining (#8, refined in #12), skill v1.19 Request Context rules, handbook dedupe (#9), Cursor Cloud setup (#10).

Observability & env (#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__).

PhaseSkill contractStatus / files
0 — Observability & envCentralized env parse once; pino; Sentry; webServerEnv + shellSessionDone — src/env/webEnv.server.ts, src/services/schemas/shellSession.ts, instrument.*.mts, webEnvMiddleware, getBrowserShellSession
1 — Schema boundariesOutbound Schema.parse() (repo → tools); router defaults bundle; Link with search: trueDone — serverFns.ts, taskMappers.ts, router.tsx (defaultNotFoundComponent), src/components/Link/Link.tsx
2 — Auth & writesAuth ticket + TraceabilityContext (skill allows stock user/userProfile until enriched)Done — accessTicket from identity + getUserProfile; TraceabilityContext on create/update
3 — Data loading & AIParent loader dedup; getAIAvailability() gates chat UI; distinct-values toolsDone — __root.tsx, task routes, Header, AppLayout, ChatDrawer, getDistinctValues
4 — HardeningcreateServerOnlyFn; PUBLIC_ROUTES; router introspection for nav manifestDone — PUBLIC_ROUTES, *.server.ts + import protection, createServerOnlyFn on DB/repo factories; buildAppNavigation(router.routesById)
5 — DeploySide + prompt-first + agentic demos (same repo, PROMPT_CONCEPT per Netlify site)Side: 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 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 (.github/workflows/release-please.yml + release-please-config.json + .release-please-manifest.json). Use Conventional Commits 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 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 as the primary UI framework.

  • Use Mantine components first: Before creating custom components, check if Mantine provides one.
  • 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: <SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}> (base is the narrow viewport; larger keys enhance).
  • Mobile first (default): Progressive enhancement from small viewports up — 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, or the fully agentic shell in the agentic-ux skill) 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: 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 as the default icon library. Lucide icons are tree-shakeable and visually consistent across the template. Import individual icons by name:

import { CheckCircle, Trash2 } from 'lucide-react'

If your team prefers @tabler/icons-react (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 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

PiecePath
Global middleware registrationsrc/start.ts
JWT + profile loadsrc/middleware/auth.ts
Mutation auth gatesrc/middleware/requireAuth.ts
Post-mutation invalidationsrc/middleware/invalidate.ts
Guard helperssrc/utils/auth.ts — requireAuth, requireGroup
JWT parsingsrc/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)

// 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 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. 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.

ConcernWhere
RPC entry pointssrc/services/api/serverFns.ts — all createServerFn exports
UI mutation errorssrc/services/api/processResponse.ts
AI tool errorssrc/services/ai/serverTool.ts — createSafeServerTool, safeToolHandler
Composition rootsrc/services/repository/getRepository.server.ts — ensureRepository()
Request injectionsrc/middleware/repository.ts — context.repository
Import protectionvite.config.ts → tanstackStart({ importProtection })

Calling convention: getTasks({ data: filter }) — pass { data: … } to server functions.

Mutations (template pattern):

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 Core Contract #11–14 and Implementation Flow #4, #7. Prompt concept: promptable-ux skill. 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.

FileRole
src/services/ai/adapter.tsProvider implementation (Gemini → OpenAI/Gateway → Azure)
src/services/ai/tools.tsServer + client tool definitions
src/services/ai/serverTool.tscreateSafeServerTool / safeToolHandler
src/services/ai/navigationManifest.tsAI route matching + prompt section; buildAppNavigation(router) from routesById
src/routes/api/chat.tsSSE endpoint, BASE_SYSTEM_PROMPT, buildSystemPrompt
src/components/PromptChat/PromptChatContext.tsxSide + prompt-first: useChat, client navigate / invalidateRouter, browserContext
src/components/AgenticChat/AgenticChatContext.tsxAgentic: useChat without route client tools
src/components/AgenticShell/AgenticThread.tsxShared markdown + A2UI thread (AgenticMessageItems) used by all three shells
src/components/ChatDrawer/ChatDrawer.tsxSide 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:

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:

ProviderAdapter packageRequired env var(s)Notes
OpenRouter (recommended for greenfield)@tanstack/ai-openrouterOPENROUTER_API_KEY300+ models with a single API key
Google Gemini@tanstack/ai-geminiGEMINI_API_KEYFirst match in this template’s adapter.ts
OpenAI / Netlify AI Gateway@tanstack/ai-openaiOPENAI_API_KEY (or gateway placeholders)Second match; gateway via OPENAI_BASE_URL / Netlify vars
Anthropic@tanstack/ai-anthropicANTHROPIC_API_KEYClaude series
Azure OpenAI@tanstack/ai-openaiAZURE_OPENAI_*Third match in this template
Ollama (local)@tanstack/ai-ollamaOLLAMA_BASE_URLLocal models, no API key needed
Groq@tanstack/ai-groqGROQ_API_KEYFast inference
xAI Grok@tanstack/ai-grokGROK_API_KEYGrok 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).

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 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):

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<typeof options>. 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 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 + 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 <table> 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 <a target="_blank" rel="noopener noreferrer">.
  • 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 (landed via PR #6). This section: file map and usage in this repo.

PiecePath
Interfacesrc/services/observability/types.ts
Sentry / no-opsentry.ts, noop.ts; factory getObservability.ts
Server bootstrapinstrument.env.mts → instrument.shared.mts → instrument.server.mts (emitted .mjs in .output/server)
Env schemassrc/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
Pinosrc/utils/logger.ts, src/utils/serverLogger.ts
Public env middlewaresrc/middleware/webEnv.ts
Public env + app meta for clientgetBrowserShellSession in serverFns.ts + root loader (not window.__ENV__)
Typed request contextwebEnvMiddleware 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 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 <pkg> 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 <pkg>@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 (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 Special Patterns and Core Contract #7–8. Mobile first and the two prompt concepts (Promptable UI (side), Prompt-first): promptable-ux skill. The fully agentic shell: agentic-ux skill. 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:

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 <unsigned-jwt> 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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.