Architect
Standards
ReferenceNormative

Vite

The default for applications with no SSR requirement — Vite plus TanStack Router, and a migration to Start that stays incremental.

Verified 2026-10-03 · Shared rules re-checked against a production TanStack Start monorepo (Vite 8.2.2, @tanstack/react-router 1.170.31). The SPA-only sections were not re-verified.

No SSR needed → Vite + TanStack Router. SPA, simplest build, fastest dev loop. If SSR is genuinely required it is TanStack Start instead — same router, same file conventions.

Never Next.js. Standing decision, with one narrow exception that is not an application.

Why the SPA is the default

Migrating Vite + Router → Start is incremental. Both are Vite underneath, so config, plugins and test setup transfer wholly between them, and the router and its file conventions do not change. That is what makes starting without SSR cost nothing — you are not choosing a dead end, you are declining a cost you do not yet have.

ssr: false is not a plan. Decide up front whether SSR is coming, because it decides whether features have a server/ folder — a client bundle boundary only exists when there is a server. See client/server safety.

A content-led product is not an application

If the product is pages — content is the point, most of it static — it is a website → Astro. Only sessions (auth, dashboards, forms, long-lived client state) make it an application.

Full line: website versus application.

What carries over unchanged

ConcernRule
Structuresrc/features/<feature>/ with core/ components/ hooks/ form/, one public door per feature
RoutesThin — file-based routing, generated route tree never edited
Forms and dataTanStack Query everywhere, seeded by route loaders with ensureQueryData; mutations declare meta.invalidates. Forms edit a draft mapped to the API
StylingTailwind + shadcn, always
BoundariesA boundary script and the door check in CI

What is different without a server

  • There is no server/ folder — the feature's queries and hooks call the generated client directly. Add server/*.server.ts at the moment a server appears, not before.
  • Every secret is the backend's. An SPA ships its whole bundle to the browser; there is no server-only config module to hide anything in. Anything the client holds is public — see secrets.
  • Reads go through the feature's query options to a real backend. Keeping every call inside queryFn and mutationFn is what makes the later move to server functions a change in one place per feature.

Traps

  • Treating route files as components' home → routes accumulate logic and become untestable. The component lives in the feature's components/.
  • Reintroducing stringly-typed navigation next to a router whose type-safe params are the reason to use it.
  • Node and package-manager version drift — pin engines and the package manager version.
  • Calling the backend from components "until we need SSR" → every call site moves later. Keep calls in query options and hooks from the start, so SSR adds a server/ folder instead of an audit of every import.

On this page