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
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository6 other files it gives its agents.
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
- Very specific: line 6
Read the matching local skill under .agents/skills/ (already in this repo.
- Very specific: line 7
If a companion is missing in another project, install with npx skills (or gh skill install):
- Distinctive: line 23
Before non-trivial changes: read the skill Core Contract and run the Architecture Checklist.
- Distinctive: line 54
Modularity & Reusability: Write code that can be reused.
- Distinctive: line 56
Clean Code: Follow clean code best practices.
- Distinctive: line 57
Interface-First: All external services (database, AI, observability) are accessed through interfaces, not concrete implementations.
- Very specific: line 76
Use PascalCase for component files (e.g., MyComponent.tsx).
- Very specific: line 77
Use camelCase for utility, service, and route files.
- Very specific: line 78
Test files are co-located with source as *.test.ts or *.test.tsx.
- Distinctive: line 84
Place React components in their own files.
- Distinctive: line 87
Avoid mixing unrelated code in the same file.
- Distinctive: line 95
Use Mantine components first: Before creating custom components, check if Mantine provides one.
- Very specific: line 96
Styling props: Use Mantine's built-in props (c, fw, size, variant, etc.) instead of CSS.
- Very specific: line 97
CSS Modules: When custom CSS beyond Mantine is needed, use CSS Modules (Component.module.css).
- Very specific: line 98
Responsive props: Use Mantine's object syntax for mobile-first values: <SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }}> (base is the narrow …
- Distinctive: line 101
Avoid inline styles: Use Mantine props or CSS Modules instead.
- Distinctive: line 103
Dark mode: All components must work in both light and dark color schemes.
- Very specific: line 113
If your team prefers @tabler/icons-react (the Mantine-adjacent set), it is a compatible alternative — but keep one library per project to …
- Distinctive: line 118
Functional Components: Prefer functional components and hooks over class components.
- Very specific: line 119
Type Reuse: Import domain types from @/services/schemas/schemas (the module that defines them).
- Very specific: line 120
Imports: Import the defining file with the @/ alias (@/* → ./src/*).
- Distinctive: line 194
Call mutations from event handlers.
- Very specific: line 262
When adding client tools: export the toolDefinition(...) from tools.ts, pass it to chat() in the chat endpoint for side/prompt-first, and …
- Very specific: line 300
When adding dynamic route segments, add pattern-matching in buildSystemPrompt() so the AI can resolve "this task" to the current id.
- Very specific: line 327
POST handler flow: check adapter → parse { messages, browserContext } → buildSystemPrompt → assemble tools → chat({ …, agentLoopStrategy: …
- Very specific: line 329
When modifying: add server tools to the tools array.
- Very specific: line 357
Convention: Test files co-located as *.test.ts or *.test.tsx.
- Very specific: line 373
Always use latest versions: When adding dependencies, run pnpm add <pkg> without a version suffix so the package manager resolves the …
- Very specific: line 374
Keep dependencies up to date: Run pnpm outdated to check for stale packages and pnpm update to align the lockfile with the latest …
- Very specific: line 375
Major version upgrades are conscious decisions: When pnpm outdated shows a major version bump, upgrade explicitly with pnpm add …
- Distinctive: line 388
Always verify changes with:
- Distinctive: line 401
When creating a new project from this template or migrating an existing one, confirm all of the following before considering the setup …
≈ “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.
Files
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(orgh 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__).
| 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 |
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 withComponent.tsxand optionalComponent.module.cssandComponent.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 insrc/components/. See skill Core Contract #7. Route tree:routeTree.gen.ts.src/middleware: TanStack Start middleware (auth, invalidation). Registered globally insrc/start.ts.src/services/api: Server functions (exported directly fromcreateServerFn) and shared response utilities.src/services/repository: Repository interface, implementations (MongoDB, seed), and the factory.src/services/schemas: Centralized Zod schemas — tools-layer schemas inschemas.ts, repository-layer schemas inrepository.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
PascalCasefor component files (e.g.,MyComponent.tsx). - Use
camelCasefor utility, service, and route files. - Test files are co-located with source as
*.test.tsor*.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 }}>(baseis the narrow viewport; larger keys enhance). - Mobile first (default): Progressive enhancement from small viewports up — not a particular widget. Start from
basestyles and addsm/md/lg. For navigation that overflows a narrow header, MantineAppShell+Burger(hiddenFrom="sm") +NavLink(projectLink) 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 rightDrawer, notAppShell.Aside. A second Netlify site can setPROMPT_CONCEPT=prompt-firstfor the always-visiblePromptBarshell (same routes and loaders). A third setsPROMPT_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 addindex.tsbarrels that only re-export.routes/**/index.tsxstays, because TanStack Start file routing requires that name. - Architecture (schema layers, URL-as-state,
loaderDeps,satisfiesoveras, trust-boundarySchema.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 orvalidateSearchedge, keep schema-inferred types — parse untrusted widget strings with the same schema.parse(), not a tuple.find(). - Recoverable URL State & Avoiding
useState/useEffectOveruse: TreatuseStateanduseEffectoveruse in routes and page components as a code smell. Every user-meaningful page state (filters, search, sort, multi-selectselected, bulk status choicebulkStatus, create/edit modals via nested routes) must be recoverable from the URL path orvalidateSearchparams. 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 akeyprop (key={${task.id}-${task.updatedAt}}) instead of syncing props inuseEffect.
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_NAMEenv 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(seerequireAuth.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_PREFIXESinsrc/middleware/auth.tsskips 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 DataHow 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.
| 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):
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.
| 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:
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).
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. Acceptsto(path) and optionalsearch(query params). Validates paths viamatchUserFacingRoute()($parampatterns fromUserFacingTo). - 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:
ChatDrawerMarkdownLink→ TanStack RouterLinkwithpreload="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 MantineTablecomponents for markdown output — extend the.markdownCSS class instead. - Code blocks:
preandcodeelements are styled in the same CSS Module using--mantine-font-family-monospaceand--mantine-radius-md. No syntax highlighting library is included by default. - Internal links: A custom
MarkdownLinkcomponent detects internal paths (starts with/, not/api/) and renders TanStack RouterLinkwithpreload="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 itsuseChatmessage 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
componentsentry to theMarkdowncomponent inChatDrawer.tsxand extend the.markdownCSS 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.
| 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. KeepNODE_ENVandENValigned 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(includesmatchMedia,ResizeObserver,MutationObservermocks). - Rendering:
renderWithProviders()insrc/test-utils/renderWithRouter.tsxwraps with MantineProvider. - Convention: Test files co-located as
*.test.tsor*.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.tsprovidesauthedPage/authedContextfixtures using unsigned JWTs sent viaextraHTTPHeaders. - 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 outdatedto check for stale packages andpnpm updateto align the lockfile with the latest compatible versions within current ranges. - Major version upgrades are conscious decisions: When
pnpm outdatedshows a major version bump, upgrade explicitly withpnpm add <pkg>@latest, then verify withpnpm lint && pnpm test && pnpm buildbefore 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 buildPost-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:
- Dependencies installed with latest compatible versions (
pnpm install && pnpm update). pnpm format && pnpm lintpasses with zero errors.- At least one unit test exists and
pnpm testpasses. pnpm buildsucceeds 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, sopnpm devis 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
Authorizationheader (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 viaextraHTTPHeaders(e2e/auth.ts). To manually exercise authenticated flows in a real browser, put a small reverse proxy in front of:3000that adds anAuthorization: Bearer <unsigned-jwt>header (unsignedalg:nonetokens are accepted — signatures are not verified). - E2E gotcha —
networkidlehangs in dev mode.pnpm test:e2edefaults to startingpnpm dev(seeplaywright.config.ts,reuseExistingServer: true). The@tanstack/devtools-viteplugin holds an open background connection in dev, so specs that usepage.goto(..., { waitUntil: 'networkidle' })never settle and time out. To get a clean run, start a production server first and let Playwright reuse it:pnpm buildthenREPOSITORY_TYPE=seed PORT=3000 pnpm start, thenpnpm test:e2ein another shell. - Mantine Select is a combobox. E2E helpers in
e2e/helpers.ts(selectMantineOption) targetgetByRole('combobox'), nottextbox. - Harmless noise:
pnpm installprints 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.ymlvalidates on PRs andmain;skills.ymlvalidates published skills (including Waza);commitlint.ymllints Conventional Commits on PRs;release-please.ymlopens/updates Release PRs on push tomain. 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 frompackage.jsonat Netlify build time.
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
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.

