Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: tanstack-fullstack-pattern
name: tanstack-promptable-fullstack-app-template
description: 'Use when scaffolding a new TanStack Start project, adding domain
entities to the fullstack template, or implementing the interface-first
repository pattern with AI-promptable tools, or nested layout routes duplicate
Expand Down Expand Up @@ -36,7 +36,7 @@ An interface-first fullstack architecture built on TanStack Start. The pattern d
8. URL-as-state: filters, tabs, selections live in URL search params via `validateSearch` (Zod/ArkType). Use `loaderDeps` to feed validated search into loaders.
9. Middleware chain: auth is global middleware, invalidation runs on mutation server functions.
10. Mutation pattern: POST server functions chain `.middleware([requireAuthMiddleware, invalidateMiddleware])`, return domain data or throw `HttpError`. Callers normalize errors: UI via `processResponse()`, AI tools via `safeToolHandler()` / `createSafeServerTool()`.
11. Query pattern: GET server functions throw on failure for centralized error handling.
11. Query pattern: GET server functions throw on failure for centralized error handling. When possible, use static server functions for performance improvements.
12. Maximize AI tool coverage: expose **every** repository method (reads and writes) via `createSafeServerTool()` so failures return `{ error, code }`. If a server function exists, it gets a tool.
13. Router capabilities as AI client tools: expose `router.navigate()` and `router.invalidate()` as client tools via `toolDefinition()`.
14. AI system prompt context: `buildSystemPrompt()` injects three context blocks into every AI chat request. (a) **Current User** — name, email, role from the auth middleware context (server-side; no client round-trip needed). (b) **Browser Context** — timezone, locale, and current date/time from `browserContext` sent by `ChatDrawer` (client-side). (c) **Current Location** — pathname, search params, and full URL from `browserContext`; route patterns (e.g. `/tasks/$taskId`) are matched to resolve dynamic segments. The navigation manifest mirrors `routeTree.gen.ts` and lists each route's search params. When adding routes, update `navigationManifest.ts` and add pattern-matching in `buildSystemPrompt()` for new dynamic segments.
Expand All @@ -56,11 +56,12 @@ An interface-first fullstack architecture built on TanStack Start. The pattern d
29. Explicit agent loop depth: configure `agentLoopStrategy: maxIterations(N)` explicitly on the `chat()` call (default N=10). This caps the number of consecutive tool-calling iterations the AI can run before returning a final answer, which bounds latency, cost, and infinite-loop risk. Tune N only after measuring; do not rely on the framework default.
30. Public runtime config bridge: expose non-secret runtime config (Sentry DSN, environment name, feature flags) via a GET server function `getPublicEnv()` and inline the result as `window.__ENV__` in the root `RootDocument` using a small `<script>` tag emitted before client JS runs. Escape `<` in the inlined JSON to avoid breaking the HTML parser. Never rely on `import.meta.env` alone for values that must differ across runtime environments built from the same bundle. See AGENTS.md section "Public Runtime Config" for the template.
31. Router UX defaults bundle: in `src/router.tsx`, configure `defaultStaleTime` (long for read-heavy dashboards, short for mutation-heavy apps), `defaultPreload: 'intent'`, `defaultPreloadStaleTime: 0` (always-fresh preloads), `scrollRestoration: true` (with a `getScrollRestorationKey` when needed), and a `notFoundComponent` on the root route. These defaults are a single coherent bundle — do not ship a router config without them.
32. Link wrapper preserves search: export a project-local `Link` component (e.g. `src/components/Link/Link.tsx`) that wraps TanStack Router's `Link` with `search: true` as the default. Use this wrapper for every internal link so filters, tabs, and other URL state never silently drop on navigation. Reserve raw `<a>` for external URLs.
32. Link wrapper preserves search & styling: export a project-local `Link` component (e.g. `src/components/Link/Link.tsx`) that wraps TanStack Router's `Link` with `search: true` as the default. Use this wrapper for every internal link so filters, tabs, and other URL state never silently drop on navigation. Ensure that links are still rendered using the chosen component library's corresponding element (e.g., using `createLink` or the `component` prop) so they inherit the correct styling. Reserve raw `<a>` for external URLs.
33. Parent layout routes deduplicate subtree work: when several child routes under the same URL prefix need the same redirect, synchronous guard, or expensive read, implement it **once** on the **parent** layout route. Use the parent's `beforeLoad` for navigation gates and synchronous checks; use the parent's `loader` (still via server functions) for shared data that every descendant needs. Child loaders fetch **only** data specific to that segment. Descendants read parent loader output with `getRouteApi('/parent/path').useLoaderData()` or `useLoaderData({ from: '/parent/path' })` — do not copy the parent's `beforeLoad` or re-fetch the parent's loader payload in each child. Structure files to match TanStack Router's layout conventions (e.g. `routes/foo/route.tsx` wrapping `routes/foo/*`).
34. Sentry user context + feedback: when Sentry is enabled, bind the signed-in user via `Sentry.setUser({ email, username })` from the shell component as soon as the identity loads (no extra round-trip).
35. Single markdown artifact for help + AI + suggested prompts: maintain one `docs/help.md` imported with `?raw`. Back three surfaces from it: (a) a `/help` route that renders it with `react-markdown`; (b) an AI tool that returns the content so the assistant can answer "how do I..." questions; (c) a parser that extracts `- [ ]` / `- [x]` lines as the chat's recommended-question list. Zero duplication between docs, assistant, and suggested prompts.
36. Distinct-value filter discovery: for every enum-ish field, the repository exposes a `getDistinctValues(field)` method that flows through a GET server function into a read-only AI tool (`getDistinctStatuses`, `getDistinctCategories`, …). The AI calls these to ground filter values in real data instead of guessing. The root loader can preload the same lists for UI filter bars so UI and AI share one vocabulary.
37. Reduce `"use client"` usage: TanStack Start supports full SSR and React Server Components. Rely on Server Components by default. Only use `"use client"` when state (`useState`), effects (`useEffect`), or browser APIs are strictly necessary. Keep client components small and at the leaf level.

## Schema Boundaries

Expand Down Expand Up @@ -169,6 +170,10 @@ A TanStack Start file-based route at `/api/chat` with two handlers:

`buildSystemPrompt()` composes `BASE_SYSTEM_PROMPT` (rule 25) + navigation manifest + dynamic context (rule 14). When modifying: add tools to the array, update `BASE_SYSTEM_PROMPT` when the data model changes, add pattern-matching for new dynamic route segments. See AGENTS.md section 8 "System Prompt Generation" for the six-section prompt template.

## Dynamic Route Schema Extraction for AI

To help the AI navigate accurately using client tools, dynamically extract your TanStack Router configuration from `router.flatRoutes`. Map this array to extract `fullPath`, `staticData.description` (for page context), path params (from segments starting with `$`), and search params (by inspecting `validateSearch` keys). Passing this structured map in the system prompt allows the AI to discover available routes and required parameters without hallucinating URLs.

## Verification

Testing setup (Vitest, Playwright, auth fixtures) and the full validation checklist are in [AGENTS.md](https://github.com/carlosvin/tanstack-fullstack-ai-template/blob/main/AGENTS.md) sections 10 and 12. Quick smoke test: `pnpm format && pnpm lint && pnpm test && pnpm build`.
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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 **deep architectural contract** -- rigid rules, three schema layers, cross-layer mapping, interface contracts, migration workflow, and the TanStack CLI docs tip -- lives in the [TanStack Fullstack Pattern skill](.agents/skills/tanstack-fullstack-pattern/SKILL.md) (generated from `skills/src/*.skill.yaml`; regenerate with `pnpm skills:build`).
The **deep architectural contract** -- rigid rules, three schema layers, cross-layer mapping, interface contracts, migration workflow, and the TanStack CLI docs tip -- lives in the [TanStack Promptable Fullstack App Template skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) (generated from `skills/src/*.skill.yaml`; regenerate with `pnpm skills:build`).

## 1. General Principles

Expand All @@ -20,7 +20,7 @@ The **deep architectural contract** -- rigid rules, three schema layers, cross-l
- `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-fullstack-pattern/SKILL.md) for the full three-layer model.
- `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.
Expand Down Expand Up @@ -90,9 +90,10 @@ If your team prefers [`@tabler/icons-react`](https://tabler.io/icons) (the Manti

## 4. TypeScript and React

- **Server Components by Default**: TanStack Start supports React Server Components. Rely on Server Components by default. Reduce the usage of `"use client"` directives. Only use `"use client"` at the top of files when React hooks (`useState`, `useEffect`) or browser APIs are strictly necessary. Keep client components as small and leaf-level as possible.
- **Functional Components**: Prefer functional components and hooks over class components.
- **Type Safety**: Use TypeScript features like interfaces, types, and generics effectively.
- **Zod-First Types**: All domain types are defined as Zod schemas and TypeScript types are inferred via `z.infer<>`. Tools-layer schemas live in `src/services/schemas/schemas.ts` (with `.describe()` on every field for AI JSON Schema); repository-layer schemas live in `repository.ts`. See the [skill](.agents/skills/tanstack-fullstack-pattern/SKILL.md) for the three schema layers, `loaderDeps`, and cross-layer `Schema.parse()` mapping.
- **Zod-First Types**: All domain types are defined as Zod schemas and TypeScript types are inferred via `z.infer<>`. Tools-layer schemas live in `src/services/schemas/schemas.ts` (with `.describe()` on every field for AI JSON Schema); repository-layer schemas live in `repository.ts`. See the [skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) for the three schema layers, `loaderDeps`, and cross-layer `Schema.parse()` mapping.
- **Type Reuse**: Import types from `src/types`. Do not redefine existing types.
- **URL-as-State**: Page state (filters, selections, active tabs, modal open/close) **must** live in URL search params, not `useState`. This makes state shareable via URL, survives refresh, and enables deep-linking. Use `validateSearch` on routes with Zod schemas to define and validate search params.
- **Correct**: `const { filter } = Route.useSearch()` — state comes from the URL.
Expand Down Expand Up @@ -160,7 +161,7 @@ When a URL scheme changes (renaming a route segment, collapsing multiple paths i

## 6. Server Functions and Data Access

This is a full-stack TanStack Start application. There is no separate backend API. The [skill](.agents/skills/tanstack-fullstack-pattern/SKILL.md) defines the rigid layering rules and the "every repo method gets a tool" policy.
This is a full-stack TanStack Start application. There is no separate backend API. The [skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md) defines the rigid layering rules and the "every repo method gets a tool" policy.

```
Route Loader → Server Function (serverFns.ts) → Repository → Database / Seed Data
Expand Down Expand Up @@ -211,7 +212,7 @@ const result = await processResponse(() => myMutation({ data: input }))

## 8. AI Chat and Tools

Server and client tool coverage expectations, the AI chat pipeline, and client-tool wiring samples are in the [skill](.agents/skills/tanstack-fullstack-pattern/SKILL.md). This section covers adapter details, file paths, and the navigation manifest.
Server and client tool coverage expectations, the AI chat pipeline, and client-tool wiring samples are in the [skill](.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md). This section covers adapter details, file paths, and the navigation manifest.

### AI Adapter Interface

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ You can install this repository's generated Agent Skill directly from GitHub:
npx skills add carlosvin/tanstack-fullstack-ai-template --list

# Install the TanStack fullstack pattern skill
npx skills add carlosvin/tanstack-fullstack-ai-template --skill tanstack-fullstack-pattern
npx skills add carlosvin/tanstack-fullstack-ai-template --skill tanstack-promptable-fullstack-app-template

# Optional: install globally (available across projects)
npx skills add carlosvin/tanstack-fullstack-ai-template --skill tanstack-fullstack-pattern -g
npx skills add carlosvin/tanstack-fullstack-ai-template --skill tanstack-promptable-fullstack-app-template -g

# Optional: verify installed skills
npx skills list
```

The skill is published in the standard location used by the CLI:

- `.agents/skills/tanstack-fullstack-pattern/SKILL.md`
- `.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md`

## Architecture

Expand Down Expand Up @@ -330,11 +330,11 @@ Then follow the end-to-end workflow:

### Option B: AI-Assisted via Generated Skill (New or Existing Project)

The skill is defined once in a canonical YAML source and generated into the [agentskills.io](https://agentskills.io) standard at `.agents/skills/tanstack-fullstack-pattern/`. Windsurf and other compatible tools read this path directly.
The skill is defined once in a canonical YAML source and generated into the [agentskills.io](https://agentskills.io) standard at `.agents/skills/tanstack-promptable-fullstack-app-template/`. Windsurf and other compatible tools read this path directly.

The generated outputs are committed intentionally so you can copy the skill into another project or tool without running the build pipeline first. The machine-readable metadata lives in `skills/registry.json`, and the portable markdown copy lives in `skills/dist/`.

**Use the skill in this repo:** clone the template — the skill is at `.agents/skills/tanstack-fullstack-pattern/`.
**Use the skill in this repo:** clone the template — the skill is at `.agents/skills/tanstack-promptable-fullstack-app-template/`.

**Install the skill globally** (available in all your projects):

Expand All @@ -348,13 +348,13 @@ To manually install instead:

```bash
# Windsurf (reads .agents/skills/ when in repo; for global copy)
cp -r .agents/skills/tanstack-fullstack-pattern ~/.codeium/windsurf/skills/
cp -r .agents/skills/tanstack-promptable-fullstack-app-template ~/.codeium/windsurf/skills/

# Cursor (copy from shared standard)
cp -r .agents/skills/tanstack-fullstack-pattern ~/.cursor/skills/
cp -r .agents/skills/tanstack-promptable-fullstack-app-template ~/.cursor/skills/

# Claude Code (copy from shared standard)
cp -r .agents/skills/tanstack-fullstack-pattern ~/.claude/skills/
cp -r .agents/skills/tanstack-promptable-fullstack-app-template ~/.claude/skills/
```

To regenerate after editing the canonical source:
Expand Down
16 changes: 8 additions & 8 deletions scripts/skills/buildSkills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ afterEach(async () => {
await Promise.all(createdDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
})

const validSkill = `id: tanstack-fullstack-pattern
title: TanStack Fullstack Pattern
const validSkill = `id: tanstack-promptable-fullstack-app-template
title: TanStack Promptable Fullstack App Template
summary: Apply the TanStack Start fullstack architectural pattern with interface-first boundaries.
projectName: TanStack AI-Promptable Full-Stack Template
projectSummary: A production-ready TanStack Start template designed to make internal tools AI promptable by default.
Expand Down Expand Up @@ -61,7 +61,7 @@ examples:
- input: Add a new domain entity to the template
output: Update schema, repository interfaces, server functions, routes, and AI tools
content: |
# TanStack Fullstack Pattern
# TanStack Promptable Fullstack App Template

Example content.
`
Expand All @@ -76,17 +76,17 @@ describe('buildSkills', () => {
const parsedRegistry = JSON.parse(registry)
expect(parsedRegistry.skills[0].author.name).toBe('Carlos Martin-Sanchez')
expect(parsedRegistry.skills[0].supportedTools[0].id).toBe('windsurf')
expect(parsedRegistry.skills[0].outputsByFormat.skill).toBe('.agents/skills/tanstack-fullstack-pattern/SKILL.md')
expect(parsedRegistry.skills[0].outputsByFormat.skill).toBe('.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md')

const agentSkill = await readFile(
path.join(rootDir, '.agents', 'skills', 'tanstack-fullstack-pattern', 'SKILL.md'),
path.join(rootDir, '.agents', 'skills', 'tanstack-promptable-fullstack-app-template', 'SKILL.md'),
'utf8',
)
expect(agentSkill).toContain('name: tanstack-fullstack-pattern')
expect(agentSkill).toContain('name: tanstack-promptable-fullstack-app-template')
})

it('fails with a clear error when YAML is malformed', async () => {
const rootDir = await createWorkspace('id: tanstack-fullstack-pattern\nsummary: [broken\n')
const rootDir = await createWorkspace('id: tanstack-promptable-fullstack-app-template\nsummary: [broken\n')

await expect(buildSkills({ rootDir })).rejects.toThrow(/Failed to parse YAML/)
})
Expand All @@ -102,7 +102,7 @@ describe('buildSkills', () => {
const rootDir = await createWorkspace(validSkill)
await buildSkills({ rootDir })
await writeFile(
path.join(rootDir, '.agents', 'skills', 'tanstack-fullstack-pattern', 'SKILL.md'),
path.join(rootDir, '.agents', 'skills', 'tanstack-promptable-fullstack-app-template', 'SKILL.md'),
'drift\n',
'utf8',
)
Expand Down
4 changes: 2 additions & 2 deletions scripts/skills/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

set -e

SKILL_NAME="tanstack-fullstack-pattern"
SKILL_RAW_URL="https://raw.githubusercontent.com/carlosvin/tanstack-fullstack-ai-template/main/.agents/skills/tanstack-fullstack-pattern/SKILL.md"
SKILL_NAME="tanstack-promptable-fullstack-app-template"
SKILL_RAW_URL="https://raw.githubusercontent.com/carlosvin/tanstack-fullstack-ai-template/main/.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md"

echo "Installing $SKILL_NAME skill..."

Expand Down
Loading
Loading