agentleFS
carlosvincarlosvin/tanstack-fullstack-ai-template/.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md

tanstack-promptable-fullstack-app-template

Skill3 starsChanged 7 days agoMITView on GitHub

**WORKFLOW SKILL** - Architecture contract for TanStack Start apps — schemas, repositories, loader-first routes, and AI-promptable tools.

  • Reads credentials

The file

Show its rules and text

We split this skill into the rules it gives an agent

20 rules. About 0 are common, 3 are distinctive, and 17 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 17

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Read Core Contract first — it is the non-negotiable architecture.

    line 82
  2. Distinctive:

    Run the Architecture Checklist before every non-trivial change.

    line 83
  3. Very specific:

    When the app already sets PROMPT_CONCEPT or already mounts one shell, keep that declaration.

    line 97
  4. Distinctive:

    When it is not clear which user experience to implement, ask which of the three to build and wait for an answer.

    line 99
  5. Very specific:

    After a value has crossed a trust boundary, keep schema-inferred types through UI, handlers, and tools — do not widen back to string and …

    line 141
  6. Very specific:

    Bound the agent loop: every chat() call sets agentLoopStrategy: maxIterations(N) explicitly (default N=10).

    line 151
  7. Very specific:

    No type erasure: after Schema.parse / validateSearch, carry schema-inferred types through server functions, repos, tools, and components — …

    line 164
  8. Very specific:

    AI stack is complete: every repo method → server tool + safe handler.

    line 169
  9. Very specific:

    Direct @/ imports: no re-export index.ts barrels.

    line 174
  10. Very specific:

    Import the file that defines the symbol.

    line 212
  11. Very specific:

    Do not define new createServerFn inline in route files — keep RPC entry points centralized in serverFns.ts.

    line 237
  12. Very specific:

    When adding node-only packages, extend vite.config.ts:

    line 241
  13. Very specific:

    Add your DB driver and auth/crypto libraries when they are not isolated in *.server.ts.

    line 256
  14. Very specific:

    Put recoverable fields in validateSearch.

    line 276
  15. Very specific:

    Virtual / computed fields: If semantics cannot live in schema metadata, keep a small registry next to the derivation and expose …

    line 359
  16. Very specific:

    Validate at the edge: use a runtime validator (Zod, ArkType, Valibot, …) in Start middleware when assembling request context — …

    line 365
  17. Very specific:

    Do not re-validate context in handlers: no shallow "is this field present?" guards on middleware output.

    line 367
  18. Very specific:

    Add Part A once for every UX.

    line 446
  19. Very specific:

    Prompt chrome and mobile first: when it is not clear which user experience to implement, ask Choose a UX and wait, then load promptable-ux …

    line 471
  20. Very specific:

    Apps built from the template: follow AGENTS.md §15 — e.g.

    line 494

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill 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
SKILL.md

Companion skills (install if missing)

This template publishes multiple skills. If only this skill is installed, add companions before related work:

  • observability-and-env (companion) — Logging, error-tracking bootstrap, env schemas, webEnvMiddleware, and shellSession setup. Install when work touches observability, process.env, or browser runtime config.
    npx skills add carlosvin/tanstack-fullstack-ai-template --skill observability-and-env
  • reference-tech-stack (companion) — Opinionated vendor map for this template's reference app. Install when implementing against the demo stack defaults (UI kit, validator, DB, deploy).
    npx skills add carlosvin/tanstack-fullstack-ai-template --skill reference-tech-stack
  • repository-architecture (companion) — Language-agnostic repository architecture: injected collection owners, indexes, stale-data cleanup, and resource lifetime. Install when designing or refactoring repository contracts.
    npx skills add carlosvin/tanstack-fullstack-ai-template --skill repository-architecture
  • promptable-ux (companion) — Shared UX for both prompt concepts (hidden side or bottom panel, and prompt-first). Install when placing the prompt, choosing a layout concept, or rendering assistant markdown.
    npx skills add carlosvin/tanstack-fullstack-ai-template --skill promptable-ux
  • agentic-ux (companion) — Fully agentic tool-only shell. Catalog views are declarative surfaces from tool data. Install when the deployment has no domain screens.
    npx skills add carlosvin/tanstack-fullstack-ai-template --skill agentic-ux

Discover all skills: npx skills add carlosvin/tanstack-fullstack-ai-template --list

TanStack Fullstack Pattern

Purpose: Capture the interface-first, schema-layered, AI-promptable contract for TanStack Start apps from this template. Day-to-day conventions (UI kit, chat wiring, logging, tests) live in the repo’s AGENTS.md — use this skill for architecture, AGENTS.md for operations.

Companion handbook: AGENTS.md — structure, styling, auth snippets, lint/test tooling, validation checklist, AI chat setup.

Companion skill: observability-and-env — env schemas, browser shell session, logging/error-tracking bootstrap. Load it for observability work; this skill keeps only the architecture invariants (no vendor-specific logging or APM choices).

Skill routing

TaskLoad
New entity, routes, schemas, AI tools, auth, server boundariesThis skill
Logging, error tracking, instrument.*.mts, src/env/, shellSession, env leaksobservability-and-env
"Which package does this template use?" / match the demo app stackreference-tech-stack
Collection repositories, indexes, stale-data cleanup, resource lifetimerepository-architecture
Prompt chrome, mobile first, assistant markdownpromptable-ux (after Choose a UX)
Fully agentic shell, no domain screensagentic-ux (after Choose a UX)
Architecture + env/loggingThis skill + observability-and-env
Scaffolding a new appThis skill + reference-tech-stack + the UX skill from Choose a UX

How to use this skill

  1. Read Core Contract first — it is the non-negotiable architecture.
  2. Run the Architecture Checklist before every non-trivial change.
  3. Jump to Server execution boundaries, Schema Boundaries, Request Context, or Special Patterns only when that concern applies. Special Patterns are not Core Contract. Follow Choose a UX before prompt chrome. Mobile first lives in companion promptable-ux.
  4. Use AGENTS.md for operational how-to — not for inventing alternate architecture. This skill is vendor-agnostic for UI kits and observability SDKs. Concrete packages for this template live in companion skill reference-tech-stack; env/logging setup lives in observability-and-env. Collection ownership, indexes, stale-data cleanup, and resource lifetime live in repository-architecture. The three user experiences live in promptable-ux (side and prompt-first) and agentic-ux (fully agentic). When it is not clear which user experience to implement, ask in Choose a UX and wait.

Choose a UX

The UX companion skills cover three experiences. One experience per deployment.

ExperienceCompanion skillBuild
sidepromptable-uxDomain screens, with the prompt hidden until opened
prompt-firstpromptable-uxBottom-pinned composer, conversation stage, and domain routes
agenticagentic-uxThin shell only. No domain screens. Catalog views are declarative surfaces from tool data. The guest kind follows the declared kit

When the app already sets PROMPT_CONCEPT or already mounts one shell, keep that declaration. This template's three sites stay as they are: side (unset or side), prompt-first, and agentic.

When it is not clear which user experience to implement, ask which of the three to build and wait for an answer. Do not pick one. Do not place a prompt, add domain screens, or start an agentic shell until the developer chooses.

Tri-modal UX architecture (4 seams, 1 shared core)

agentic owns the shared dynamic chat core (AgenticMessageItems + useInlineDetailViews → AgenticA2uiSurface). prompt-first and side embed that core in route chrome (ChatThread + PromptChatProvider, which owns opened details). Branch only at four seams with exhaustive Record<PromptConcept, …> / satisfies maps (PROMPT_CONCEPT=side | prompt-first | agentic):

  1. Env & session (shellSession.ts, webEnv.server.ts): PromptConceptSchema, parse PROMPT_CONCEPT, project promptConcept via getBrowserShellSession (observability-and-env).
  2. Root shell (AppLayout.tsx): read shellSession.promptConcept — AgenticChatProvider + AgenticShell for agentic; PromptChatProvider + ChatDrawer / PromptBar for side / prompt-first (promptable-ux / agentic-ux).
  3. Home route (routes/index.tsx): exhaustive home map — overview loaders for side/prompt-first; agentic returns [] / null (shell owns the stage).
  4. Chat endpoint (routes/api/chat.ts): shared dataTools (CRUD + showTasksViewTool / showTaskViewTool); add navigate / invalidateRouter and route system prompts only for side/prompt-first.

Full seam notes: references/tri-modal-ux.md.

Fixed vs swappable stack

Fixed by this skill (TanStack): TanStack Start, Router, and AI (server functions, middleware, file routes, validateSearch, loaders, chat() / tools / SSE). Use @tanstack/cli for current library docs. This repo’s architecture lives in Agent Skills (.agents/skills/*/SKILL.md) only.

Swappable (not prescribed here): runtime validation, database, auth mechanism, AI provider, observability vendors, UI kit, markdown renderer, lint/test/deploy tooling. Patterns stay interface-first (Schema.parse(), repository interfaces, AIAdapterService, ObservabilityService).

Concrete packages for this template: companion skill reference-tech-stack. Env/logging bootstrap: observability-and-env.

Pick one validator library per app and use it consistently across router search, server-fn validators, and AI tool schemas. Code samples below use Zod as the reference syntax (see reference-tech-stack); translate idioms when using ArkType or Valibot.

Common failure modes (avoid these)

  • URL-recoverable state ignored: filters/tabs/modals in useState/useEffect, or navigate on every search keystroke — use validateSearch + props; debounce free-text (Special Patterns).
  • Wrong schema edge / half-parse: repository schemas in UI/tools; inbound-only parse; Array.find/as instead of Schema.parse / validateSearch.
  • Server fn without a tool; UI-only auth; type escape hatches (any, loose Record, as after parse).
  • Duplicated parent work; server logic in loaders (process.env secrets, DB/repo imports in route files).
  • Wrong primitive / leaky graph: createServerFn for non-RPC singletons (use createServerOnlyFn); missing *.server.ts / server-only import.
  • Runtime context guards or casts on middleware context; leaking serverEnv to the browser (use shellSession).
  • Barrel / relative in-src imports — import defining files with @/ (routes/**/index.tsx stays).
  • Prompt chrome / UX invented here — ask Choose a UX and wait; load promptable-ux or agentic-ux; keep declared PROMPT_CONCEPT.

Core Contract

  1. Interfaces: Database, AI, observability (and other externals) sit behind interfaces; implementations are swappable.
  2. Schemas as the type source: Wire and tool shapes use a runtime validator (Zod, ArkType, Valibot, …) with schema-inferred types. Hand-written interfaces define behavior (Repository, AIAdapterService, …), not ad-hoc JSON types.
  3. Three schema layers + trust boundaries: Repository (DB-shaped), tools / server-fn (API-shaped, shared between createServerFn and toolDefinition), router search (URL-shaped). Translate between layers with Schema.parse(). Untrusted values become typed only at trust boundaries, using the chosen runtime validator (Schema.parse() or equivalent — not parallel parsers):
    • Repository implementations: documents/rows from a database and JSON from external APIs are parsed with the repository-layer schema before they enter the app.
    • URL search params: TanStack Router validateSearch (the search schema is the validator).
    • Other untrusted edges: createServerFn .inputValidator, AI toolDefinition inputSchema, env (companion observability-and-env), and third-party widgets that type values as bare string (parse with the same schema). After a value has crossed a trust boundary, keep schema-inferred types through UI, handlers, and tools — do not widen back to string and re-parse with a helper that duplicates the schema.
  4. TypeScript inside the typed flow: After trust boundaries, preserve inferred types end-to-end — prefer satisfies, discriminated unions, as const tuples, and exhaustive switch (e.g. default branch calling assertNever) over any, broad unknown plumbing, or as casts (only use as at documented third-party/library seams per AGENTS.md). TypeScript narrows already-typed unions; it is not a substitute for Schema.parse() on untrusted input.
  5. Repository vs tools: Repository implementations use repository-layer schemas only. Server functions and AI tools share the same tools-layer schemas (.inputValidator / toolDefinition inputSchema). TanStack AI parses tool arguments with inputSchema before server execute and before client .client() handlers, so those handlers receive the parsed value and pass it through. UI and AI consume tools-layer types only — never import repository schemas at those edges.
  6. Server functions: GET queries throw on failure; POST mutations chain .middleware([requireAuthMiddleware, invalidateMiddleware]); handlers return data or throw HttpError; callers normalize with processResponse / safeToolHandler / createSafeServerTool.
  7. Routes: Thin route files (createFileRoute, validateSearch, loaderDeps, loader, component); page UI in src/components/. Loaders fetch via server functions — no useEffect data fetching for route data.
  8. URL-as-state: Recover filters, sort, selection, bulk-action choices, and modal routes from the path and validateSearch. Copying that state into useState with useEffect is a code smell. Keep UI-only fields (selected, bulkStatus) out of loaderDeps. Page components take loader data, search, and callbacks as props; reset a form with key, not an effect. Ephemeral chrome (mobile nav, side chat, drafts, spinners) stays local.
  9. Router config bundle: ship a project-local Link wrapper with search: true default (use it for every internal link) and these router defaults together: defaultStaleTime, defaultPreload: 'intent', defaultPreloadStaleTime: 0, scrollRestoration: true, notFoundComponent.
  10. Auth ticket built in middleware: auth middleware enriches ctx.context with a repository-built ticket (this template: context.repository.getUserProfile(email) into buildAccessTicket; apps may use getUserAccess when roles live there) carrying identity, roles, and guards; Repository mutations accept a TraceabilityContext (createdBy, lastModifiedBy, …) constructed from that ticket so writes are attributed consistently across UI and AI.
  11. AI tool coverage: expose every repository method as a server AI tool via createSafeServerTool; add distinct-values tools for enum-ish filters; expose navigate and invalidateRouter as client tools.
  12. Promptable by default: root loader checks getAIAvailability() and only mounts the prompt UI when configured (no disabled state). Where that UI sits is companion promptable-ux (this template: hidden side panel). Chat input includes a browserContext (timezone, locale, path) consumed by buildSystemPrompt alongside the auth ticket.
  13. Bound the agent loop: every chat() call sets agentLoopStrategy: maxIterations(N) explicitly (default N=10); tune after measuring — do not rely on the framework default.
  14. Metadata for AI and UI: Attach human-readable descriptions to schema fields (reference — Zod .describe() → JSON Schema description; ArkType/Valibot have equivalents). Use structured schema extras only for non-description hints — unit, format, optional title. Prefer deriving prompts and UI copy from schemas + JSON Schema export and router introspection over parallel hand-maintained maps.
  15. Parent layouts: Shared beforeLoad, redirects, and expensive reads belong on the parent layout route; children read parent loader data via getRouteApi / useLoaderData({ from }) — do not duplicate parent work.
  16. Server execution boundaries: Route loaders are isomorphic — they run on the server during SSR and on the client during SPA navigations. Loaders only call exported createServerFn from serverFns.ts (e.g. getTasks({ data: deps })). DB access, secrets, and Node-only SDKs live in *.server.ts or behind createServerOnlyFn; extend tanstackStart({ importProtection }) when adding node packages.
  17. Startup-validated env + typed context + browser shell: Parse env once at startup into webServerEnv and shellSession; inject via next({ context }); chain middleware for inferred types; expose browser-safe config only through getBrowserShellSession / shellSession — never serverEnv or window.__ENV__. Setup recipe: companion skill observability-and-env.

Architecture Checklist

Scan before changing code:

  • One tools-layer schema per wire shape: createServerFn .inputValidator(Schema) and AI toolDefinition({ inputSchema }) share the same schema — no duplicate hand-written wire types.
  • Parse both directions: tools → repository inputs and repository rows → tools/API outputs each end in the target layer’s Schema.parse() (pure mapper functions are fine if the final step is always .parse()).
  • Trust boundaries use the validator: repository implementations Schema.parse DB documents / API JSON; routes use validateSearch; untyped widget values use the same schema .parse() — no Array.find / homemade parsers that duplicate enums.
  • No type erasure: after Schema.parse / validateSearch, carry schema-inferred types through server functions, repos, tools, and components — do not widen back to Record<string, unknown> / any.
  • Repository interfaces = repo-layer types only: mapping lives beside schemas / mappers — not in React components.
  • Auth ticket is repository-backed and server-enforced: middleware builds the ticket from the injected repository (this template: getUserProfile → buildAccessTicket; or getUserAccess when that is the role source); guards run in server handlers, never UI-only.
  • Writes use TraceabilityContext: pass audit fields from the ticket (or stock context.user.email) through a single context object on Repository mutations — avoid sprinkling raw email arguments. Repository implementations must persist createdBy / lastModifiedBy from that context onto the entity.
  • Navigation is one decision: ship the router defaults bundle and the project Link wrapper (search: true) together so URL state survives navigation.
  • AI stack is complete: every repo method → server tool + safe handler; client navigate / invalidateRouter; root getAIAvailability(); chat payload includes browserContext; chat({ agentLoopStrategy: maxIterations(N) }).
  • Routes: validateSearch + loaderDeps; duplicate beforeLoad / shared loaders only on parent layouts; recover page state from URL path + search params and pass props to isolated components instead of useState / useEffect.
  • Metadata discipline: schema field descriptions for narrative copy; structured extras for units/formats; closed vocabularies = as const tuple + schema enum + inferred type.
  • Server boundaries: loaders call serverFns only — no process.env secrets, DB drivers, or repo imports in route files; *.server.ts for DB drivers / Node SDKs; createServerOnlyFn for non-RPC infra; importProtection updated for new node packages.
  • Request context: middleware validates once; handlers chain middleware and read context.* directly — no runtime context helpers, no context casts, no Register; browser sees only getBrowserShellSession output (env bootstrap: observability-and-env).
  • Direct @/ imports: no re-export index.ts barrels; modules under src/ import the defining file with @/ (@/* → ./src/*). routes/**/index.tsx stays for file routing. Targets outside src/ (package.json, docs, instrument bootstrap) stay relative.

Server execution boundaries

TanStack route loaders are isomorphic — they run during SSR and on client-side navigations. Treat every route module as potentially shipping to the browser.

Forbidden in route files

  • Top-level imports of openMongoRepositoryScope, repositories, database drivers, fs, or other Node-only modules.
  • process.env for secrets inside loader bodies.
  • Inline DB queries or repository calls inside loader.

Required pattern

Define reads/writes in src/services/api/serverFns.ts. Route loaders only invoke them:

// src/routes/tasks/index.tsx — thin route
export const Route = createFileRoute('/tasks/')({
  validateSearch: TasksSearchSchema,
  loaderDeps: ({ search }) => search,
  loader: ({ deps }) => getTasks({ data: deps }),
})
// src/services/api/serverFns.ts — server-only handler body
export const getTasks = createServerFn({ method: 'GET' })
  .middleware([repositoryMiddleware])
  .inputValidator(TaskFilterSchema.optional())
  .handler(async ({ data: filter, context }) => {
    const repoFilter = filter ? TaskRepoFilterSchema.parse(filter) : undefined
    return context.repository.getTasks(repoFilter)
  })

Module imports

Import the file that defines the symbol. Use the @/ alias (@/* → ./src/*) for every module under src/. Do not add index.ts files that only re-export. routes/**/index.tsx is the exception: TanStack Start file routing requires that name.

Targets outside src/ stay relative: package.json, docs/help.md?raw, and the instrument bootstrap (instrument.*.mts and the emitted .mjs).

File naming and tripwires

  • *.server.ts / *.server.tsx: DB clients, repositories with drivers, private API keys, Node-only SDKs (e.g. mongoClient.server.ts, getRepository.server.ts).
  • When rename is awkward: first line import '@tanstack/react-start/server-only'.

createServerFn vs createServerOnlyFn

PrimitiveUse when
createServerFnLoaders, mutations, and AI tools need to trigger server work over RPC (GET / POST).
createServerOnlyFnInternal singletons (DB client factory) that must never be client-callable.
import { createServerOnlyFn } from '@tanstack/react-start'
import type { Repository } from '@/services/repository/types'

export const readTasks = createServerOnlyFn(async (repository: Repository) => {
  return repository.getTasks()
})

Do not define new createServerFn inline in route files — keep RPC entry points centralized in serverFns.ts.

Import protection (Vite)

When adding node-only packages, extend vite.config.ts:

tanstackStart({
  importProtection: {
    behavior: 'error',
    client: {
      // Reference template uses mongodb + jose — replace with your packages
      specifiers: ['<db-driver>', '<auth-crypto-lib>'],
      files: ['**/services/db/**', '**/repository/*.server.ts'],
    },
  },
})

Add your DB driver and auth/crypto libraries when they are not isolated in *.server.ts. Set ignoreImporters: ['**/*.test.ts'] if unit tests import server modules in jsdom. Verify with your build command. Docs: npx @tanstack/cli search-docs "import protection" --library start.

If the user asks for DB/secrets in a component or route config

  1. Stop — explain the isomorphic loader / client-bundle risk.
  2. Refactor — move logic to serverFns.ts, *.server.ts, or createServerOnlyFn.

Rationalizations (reject these)

ExcuseReality
“Loader ran on SSR so it’s server-only”Loaders re-run on client navigations.
“Dynamic import in the loader is enough”Route module static imports still enter the client graph.
“One-line process.env read won’t matter”Isomorphic code can expose env reads to the client bundle.
“useState + useEffect is simpler for this page filter/selection”Local React state is lost on refresh, cannot be deep-linked or driven by AI navigate, and couples components to effects instead of pure props.

URL-Recoverable State & Prop-Driven Components

Refresh, share, and AI navigate restore filters, sort, selection, bulkStatus, and modal routes (/tasks/new, /tasks/$taskId/edit) from the path or validateSearch. Mirroring that state with useState / useEffect is a code smell.

  • Put recoverable fields in validateSearch. Put only server-query fields in loaderDeps.
  • Page components receive data, search, and callbacks as props. Reset forms with key={${id}-${updatedAt}}, not an effect.
  • Ephemeral chrome stays local: mobile nav open, side-chat open, drafts, spinners, resize, and scroll.
  • Close the mobile nav from link clicks and when pathname changes (back, forward, or client-tool navigate).

Schema Boundaries

Route search schema → loader → tools schema → server fn → mapping → repo schema → repo

repo output → mapping → tools schema → AI or UI

Layer 1 — Repository (DB-shaped): define in src/services/schemas/repository.ts (target layout; today some apps still colocate in schemas.ts). No field descriptions required here. Infer types from your validator.

Layer 2 — Tools / server functions (API-shaped): one schema for .inputValidator(Schema) and toolDefinition({ inputSchema }). Server functions parse at .inputValidator. Server execute and client .client() handlers already receive the inputSchema parse, so pass those args through.

Reference implementation (Zod) — use ArkType or Valibot with TanStack adapters when preferred:

// Example: tools-layer object — .describe() for text; .meta() for non-description fields
const TaskInputSchema = z.object({
  title: z.string().min(1).describe('Short title'),
  status: TaskStatusSchema.default('pending').describe('Current status'),
  estimateHours: z
    .number()
    .optional()
    .describe('Estimated effort in hours')
    .meta({ unit: 'h', format: 'decimal' }),
})

Closed vocabularies (enums, tool categories, filter buckets): const VALUES = [...] as const, then a schema enum, attach descriptions for AI/UI, optional structured extras, infer the union type from the schema. Do not treat export const LABELS = { id: 'Display Name' } as const as the authority for the same strings unless it is derived from or validated by that schema.

const TOOL_CATEGORY_VALUES = ['Metadata & Navigation', 'Strategic Objectives'] as const

export const ToolCategorySchema = z
  .enum(TOOL_CATEGORY_VALUES)
  .describe(
    'Used in toolDefinition metadata.category; groups tools and documents allowed values for the LLM.',
  )
  .meta({ title: 'Tool category' })

export type ToolCategory = z.infer<typeof ToolCategorySchema>

Layer 3 — Router search (URL-shaped): local validateSearch schemas; fields are usually optional for partial URLs. TanStack Router runs this schema for you — that is the trust boundary for URL params.

const TasksSearchSchema = z.object({
  status: z.enum(TASK_STATUSES).optional(),
  priority: z.enum(TASK_PRIORITIES).optional(),
  search: z.string().optional(),
})
export const Route = createFileRoute('/tasks/')({
  validateSearch: TasksSearchSchema,
  loaderDeps: ({ search }) => search,
  loader: ({ deps }) => getTasks({ data: deps }),
})

Trust boundaries: Untrusted data becomes typed only at the edges. Repository implementations parse DB rows / HTTP JSON with RepoLayerSchema.parse(...) (reference app: parseTaskRepo). Router search is already typed after validateSearch — page components receive inferred search types. Interior code (colors, labels, exhaustive switches) consumes those types; it does not re-implement the enum.

Widget libraries often type onChange as string | null. That is still an untrusted edge — parse with the same schema (OptionalStatusSchema.parse(value)), not STATUSES.find((s) => s === value).

Boundary mapping (mandatory): layer switches happen only in mapper functions; inbound tool payloads become repo inputs with RepoLayerSchema.parse(...), outbound repo documents become tools/API shapes with ToolsLayerSchema.parse(...).

function toRepoCreateInput(tool: z.infer<typeof TaskCreateToolSchema>): TaskRepoInput {
  return TaskRepoInputSchema.parse({ title: tool.title, status: tool.status })
}
function toToolTask(row: TaskRepo): z.infer<typeof TaskToolSchema> {
  return TaskToolSchema.parse({ id: row.id, title: row.title, status: row.status })
}

TypeScript discipline (complements runtime validation): the validator (Schema.parse / validateSearch) checks at trust boundaries; TypeScript keeps the interior honest — exhaustive switch, satisfies, assertNever. Do not add a second parser (Array.find, ad-hoc guards) for the same closed vocabulary.

type TaskStatus = 'pending' | 'done'
const STATUS_LABEL = { pending: 'Pending', done: 'Done' } as const satisfies Record<TaskStatus, string>
function assertNever(x: never): never { throw new Error(`Unexpected ${String(x)}`) }

Virtual / computed fields: If semantics cannot live in schema metadata, keep a small registry next to the derivation and expose explainField — do not duplicate fields already described by schemas.

Request Context

Context and philosophy — parse, don't validate

  • Validate at the edge: use a runtime validator (Zod, ArkType, Valibot, …) in Start middleware when assembling request context — token/session parsing, process.env / serverEnv, external headers, repository enrichment.
  • TypeScript inside handlers: once middleware calls next({ context }), Start infers ctx.context from the middleware chain on that server fn / route. Chain the middleware that provides the fields you need (e.g. .middleware([webEnvMiddleware]) or .middleware([requireAuthMiddleware])).
  • Do not re-validate context in handlers: no shallow "is this field present?" guards on middleware output. Parse at true external boundaries only; let middleware chaining carry types downstream. No Register / module-augmentation for middleware context.

Field names are app-specific (accessTicket, identity, serverEnv, …). This stock template middleware attaches an accessTicket (JWT identity + repository profile, with roles and guards) plus startup-validated serverEnv and shellSession (see observability-and-env for how those are parsed and injected).

Env and browser shell

Invariant (Core Contract #17): parse env once → inject serverEnv + shellSession via middleware → project shellSession to the browser only via getBrowserShellSession in the root loader.

Do not duplicate the setup recipe here. File layout, env schemas, instrument.*.mts, logger factories, and webEnvMiddleware wiring are in companion skill observability-and-env.

Core rules for agents

  1. Direct context access — read ctx.context fields directly (e.g. context.accessTicket, context.serverEnv, context.shellSession).
  2. No runtime context guards — never add or call wrappers such as getShellAuthContext(ctx.context), getAccessTicket(ctx.context), or accessTicketFrom(context). Middleware guarantees shape; missing fields are a middleware bug, not something handlers paper over.
  3. No type bypasses — never cast context with as unknown, as any, or context as SomeContext. Chain middleware (e.g. requireAuthMiddleware after authMiddleware, or webEnvMiddleware for env fields) so context is inferred.
  4. Maintain boundary validation — keep a runtime validator on external inputs: env, auth claims before enrichment, request bodies, third-party payloads, browser session serialization. Do not duplicate validation on context already built by trusted middleware.
  5. Env is a startup singleton — parse with the chosen schema library once; put the result on context; do not call process.env or re-parse in handlers.

Enforce authorization in server handlers for every mutation and sensitive read. UI may hide controls; handlers are authoritative.

export const updateTask = createServerFn({ method: 'POST' })
  .middleware([requireAuthMiddleware, invalidateMiddleware])
  .handler(async ({ data, context }) => {
    context.accessTicket.requireTaskEditor(data.taskId)
    const repoPatch = TaskRepoPatchSchema.parse(mapToolUpdateToRepo(data))
    return context.repository.updateTask(data.taskId, repoPatch, {
      lastModifiedBy: context.accessTicket.identity.email,
    })
  })

Stock template equivalent — same rules; requireAuthMiddleware chains auth so context.accessTicket is inferred. Build a TraceabilityContext (do not pass a bare email string as the mutation’s second argument):

.handler(async ({ data, context }) => {
  const repoPatch = TaskRepoPatchSchema.parse(mapToolUpdateToRepo(data))
  const trace = updateWriteTrace(context.accessTicket.identity.email)
  return context.repository.updateTask(data.taskId, repoPatch, trace)
})

Security boundaries

  • Never leak serverEnv to the browser — secrets and server-only config stay on the server; handlers must not return them from server functions or route loaders consumed by client bundles.
  • Browser-safe projections only — client code receives shellSession via getBrowserShellSession + root loader (public env fields + app). Do not hand-pick fields from serverEnv.

Interface Contracts

Repository interfaces reference repository-layer types only. Repository mutations take an optional TraceabilityContext built from the auth ticket (stock template: helpers such as createWriteTrace / updateWriteTrace from context.accessTicket.identity.email) — not ad-hoc optional email parameters at each call site.

Collection-level ownership (indexes, queries, stale-data cleanup), boundary parsing across languages (runtime validators such as Zod or Pydantic, or a decode into the domain type in strongly typed languages), and resource lifetime (AsyncDisposable, Java AutoCloseable, Python context managers) live in companion skill repository-architecture. This section keeps the TanStack contract: tools-layer versus repository-layer types, and TraceabilityContext on writes.

Implementations must persist audit fields from the trace onto the entity (createdBy on create, lastModifiedBy on update). Ignoring the trace argument is a contract violation.

Auth for writes lives on POST server functions (requireAuthMiddleware), not on a second repository type.

interface TraceabilityContext { createdBy?: string; lastModifiedBy?: string }
interface Repository {
  getTasks(filter?: TaskRepoFilter): Promise<TaskRepoOutput[]>
  createTask(input: TaskRepoInput, trace?: TraceabilityContext): Promise<TaskRepoOutput>
  updateTask(id: string, input: Partial<TaskRepoInput>, trace?: TraceabilityContext): Promise<TaskRepoOutput | null>
  // … getTask / getDistinctValues / getUserProfile / getUserAccess / deleteTask
}

Implementation Flow

  1. Schemas: repo + tools + search layers; repository I/O and mappers with Schema.parse(); URL state via validateSearch.
  2. Repository: interfaces in types.ts; seed + production implementations. Collection owners, indexes, and client disposal: companion repository-architecture.
  3. Server functions: serverFns.ts — GET queries, POST mutations with shared validators.
  4. AI tools: each server function → toolDefinition + createSafeServerTool; wire client tools in the prompt shell (AGENTS.md §8). Prompt placement: Choose a UX, then promptable-ux or agentic-ux. When it is not clear which user experience to implement, ask which of the three and wait.
  5. Middleware: start.ts — auth, invalidation, optional pre-auth 308 redirects for legacy paths.
  6. Routes: validateSearch, loaderDeps, loaders; parent layouts for shared beforeLoad/data.
  7. Chat: adapter, chat(), buildSystemPrompt, tool list — details in AGENTS.md §8. Follow Choose a UX before wiring chrome. When it is not clear which user experience to implement, ask which of the three and wait. Side and prompt-first load promptable-ux. Agentic loads agentic-ux and skips domain screens.

Per-entity 3-UX checklist

Add Part A once for every UX; add Part B only when the deployment includes route screens (side / prompt-first). Details: references/tri-modal-ux.md.

  • Part A — Core (all three modes): three-layer schemas + mappers → repository + TraceabilityContext writes → serverFns.ts → toolDefinition / createSafeServerTool + show*ViewTool in shared dataTools → A2UI builders rendered by AgenticMessageItems (and PromptChatProvider’s useInlineDetailViews for side/prompt-first).
  • Part B — Route extension: thin validateSearch routes + prop-driven pages → UserFacingTo / buildSystemPrompt location context → optional prompt-first landing summaries. Keep /<entity> for drill-down.

Special Patterns (use when the feature applies)

  • Overlay repository: a Repository decorator over a read-only upstream plus sparse user overrides; pure applyOverrides; writes only to overrides. This is optional composition — not a Core Contract read/write factory split.
  • URL bulk edit: selection and category tabs in search params; batched mutation; per-row auth.
  • Debounced free-text search (URL-as-state): Keep filters in validated search params + loaderDeps, but do not control free-text search from the URL on every keystroke. Use an uncontrolled text input (defaultValue from the current search param) and a debounced callback — your UI library's debounce hook, @tanstack/pacer, or a small local helper — to navigate({ replace: true, search }) only after the user pauses. Discrete filters (dropdowns, tabs, segmented controls) may navigate immediately. Call navigate from the debounce callback — do not watch a debounced value in useEffect just to navigate. UI-agnostic: this skill does not prescribe a component library; follow the project's UI kit in AGENTS.md §3 for concrete input/select components.

const updateSearch = (updates: Partial<Search>, replace = false) => { navigate({ to: '/tasks', replace, search: (prev) => ({ ...prev, ...updates }) }) } const debouncedSearch = debounce((value: string) => { const next = value || undefined if (next === search.search) return updateSearch({ search: next }, true) }, 300)


- **Help surface:** single `docs/help.md` can back `/help`, an AI tool, and suggested prompts (see AGENTS.md).
- **Distinct values:** `getDistinctValues` → GET server fn → read-only AI tool so filters match real data.
- **Dynamic AI navigation:** derive route/help context from `router.flatRoutes` + `validateSearch` introspection where possible.
- **Prompt chrome and mobile first:** when it is not clear which user experience to implement, ask **Choose a UX** and wait, then load **`promptable-ux`** or **`agentic-ux`**. Do not choose the layout inside this skill. Existing deployments keep their declared `PROMPT_CONCEPT`.

## Agent Skills, TanStack CLI, and AI

- **Agent Skills (only standard):** this template’s skills are [agentskills.io](https://agentskills.io) folders — `.agents/skills/<name>/SKILL.md`. Edit those files directly. Install into other projects with `npx skills add carlosvin/tanstack-fullstack-ai-template` (all skills) or `--skill <id>`. List: `npx skills add carlosvin/tanstack-fullstack-ai-template --list`. Similar: `gh skill install carlosvin/tanstack-fullstack-ai-template`. Do not maintain a second skill format (canonical YAML, TanStack Intent package skills, or generated copies).
- **CLI (prefer current docs over memory):** `npx @tanstack/cli --help` → `libraries`, `search-docs "<query>" --library router|start|ai`, `doc <library> <path>`.
- **AI stack:** `@tanstack/ai`, `@tanstack/ai-react`, `/api/chat` — provider table, SSE wiring, system prompt sections, and chat endpoint anatomy are spelled out in **AGENTS.md §8**.

## Use the handbook (AGENTS.md)

| Need | Where |
|------|--------|
| UI kit / packages / lint-test tooling | §3, §10–§11, §15 + **`reference-tech-stack`** |
| Prompt concepts / agentic shell | **`promptable-ux`**, **`agentic-ux`**, §3, §8 |
| Auth, middleware, guards | §5 |
| AI adapters, chat, tools, prompts | §8 |
| Observability, env, `shellSession` | §9, §13 + **`observability-and-env`** |
| Repository ownership / indexes / lifetime | **`repository-architecture`** |

## Verification

**This template repo (skill authors):** edit `.agents/skills/<id>/SKILL.md`, then run `pnpm skills:check` (companions / install commands) and `pnpm skills:waza` (Agent Skills spec; required in CI).

**Apps built from the template:** follow **AGENTS.md** §15 — e.g. `pnpm format && pnpm lint && pnpm test && pnpm build`; smoke with dev server and `/api/health` when configuration allows.

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.