Skip to content

ADR-0016: TanStack Start as the Web Framework for apps/web ​

Last Updated: 2026-05-31
Status: Active
Context: Decksmith


Context ​

Phase 4.1 requires choosing a foundation for apps/web. The original plan was a plain SPA (Vite + TanStack Router). Before starting, we evaluated whether TanStack Start — a full-stack meta-framework built on TanStack Router — is a better fit.

Key requirements driving this evaluation:

  1. Decksmith has public-facing content (public deck pages, card details, craft guide articles) that must be indexable by search engines and renderable as social previews.
  2. The original workaround (ADR-0010 hybrid meta endpoint) is brittle — bot detection, redirect overhead, and a second implementation path that needs maintenance.
  3. The authentication architecture (ADR-0014) uses httpOnly cookies, which are SSR-friendly by design.
  4. apps/mobile is planned — the backend (apps/api) must remain a standalone API server.

TanStack Start v1.0 was released March 2026. It is the first stable version.


Current Decision ​

Adopt TanStack Start as the framework for apps/web, with the constraint that apps/api remains the sole backend. No server functions are used in apps/web. Route loaders call apps/api via HTTP.

SSR/CSR split by route ​

Routes are either SSR or CSR based on one rule: public + indexable → SSR. Authenticated + interactive → CSR.

RouteRenderingReason
/ (landing)SSRFirst impression, marketing, SEO
/cards/:idSSRCard data indexable, social previews
/craft-guide/:slugSSREditorial content, SEO
/decks/:id (public)SSRPublic decks accessible anonymously, SEO
/login, /registerCSRNo SEO value
/dashboardCSRAuth-required, personal data
/decks (list)CSRAuth-required
/collectionCSRAuth-required, interactive
/decks/:id/editCSRHighly interactive deck builder
/settingsCSRAuth-required

After the initial page load, navigation between all routes — SSR and CSR alike — is client-side. The user experiences a seamless SPA regardless of which routes are SSR or CSR.

What TanStack Start adds over TanStack Router alone ​

FeatureUsed in Decksmith
SSR + streaming✅ For public routes
Route loaders (server-side data prefetch)✅ For SSR routes
Middleware (auth guard, redirects)✅ For protected routes
Unified Vite bundling✅ Replaces vite.config.ts
Server functions❌ Not used — see constraint below
API routes❌ Not used — apps/api handles all API logic

Critical constraint: no backend code in apps/web ​

TanStack Start allows writing server functions and API routes inside apps/web. We do not use these features. apps/api is the only backend.

The only thing route loaders in apps/web are permitted to do on the server side is fetch data from apps/api:

ts
// ✅ Correct: route loader calls apps/api
export const Route = createFileRoute('/decks/$id')({
  loader: async ({ params, context }) => {
    const res = await fetch(`${env.API_URL}/api/v1/decks/${params.id}`, {
      headers: { cookie: context.request.headers.get('cookie') ?? '' },
    });
    if (!res.ok) throw notFound();
    return res.json() as Promise<DeckResponse>;
  },
});
ts
// ❌ Forbidden: direct DB access in apps/web
export const Route = createFileRoute('/decks/$id')({
  loader: async ({ params }) => {
    return prisma.deck.findUnique({ where: { id: params.id } }); // NEVER
  },
});

If a loader needs data that doesn't exist in apps/api yet, the answer is: add an endpoint to apps/api first, then call it from the loader. Always.

apps/web allowed and forbidden imports ​

apps/web can import:

  • packages/schema — Zod DTOs for form validation and response types
  • packages/api-client — typed HTTP client
  • packages/query — TanStack Query hooks (for CSR data fetching)
  • packages/web-ui — React components
  • packages/tokens — design tokens

apps/web cannot import:

  • packages/db — Prisma client (server-only)
  • packages/domain — domain logic (server-only)
  • packages/scryfall — external API client (server-only)
  • packages/pdf — PDF generation (worker-only)

This constraint applies to route loaders as well. Loaders run on the server, but they are part of apps/web and must respect these boundaries.


Rationale ​

Why TanStack Start instead of plain Vite + TanStack Router? ​

The core issue is public content. Decksmith has three content types that require SSR:

  1. Public deck pages — must be indexable for SEO and social previews. ADR-0010 created a bot detection workaround for this. SSR eliminates the workaround.
  2. Card detail pages — card data (oracle text, legalities, prices) is valuable search content. With SPA, Google must execute JavaScript to index it.
  3. Craft guide articles — editorial content that should rank in search results.

Without SSR, these pages are blank <div id="root"></div> for crawlers.

Why TanStack Start specifically? ​

TanStack Router is already decided for apps/web (CLAUDE.md). TanStack Start is not a framework change — it is the full-stack layer built on top of TanStack Router. The router instance, file conventions, hooks (useNavigate, useParams), and TanStack Query integration are identical.

Alternative frameworks (Next.js, Remix) would require adopting a different router and ecosystem, rebuilding the planned architecture from scratch, and accepting Vercel lock-in (Next.js) or web-standards-first constraints (Remix).

Why no server functions? ​

Server functions in TanStack Start allow business logic to run server-side inside apps/web. This would create a second backend alongside apps/api, violating ADR-0005 (package boundaries) and undermining the planned mobile app architecture — apps/mobile cannot reuse server functions. Keeping apps/api as the sole backend ensures one consistent API surface for all clients.

Why SSR for public routes only? ​

Authenticated app routes (deck builder, collection manager, settings) are complex interactive UIs with no SEO value. Server-rendering them adds latency and complexity without benefit. CSR is the right choice for those routes.

Auth compatibility ​

ADR-0014 established httpOnly cookies for session management. Cookies are automatically sent with every HTTP request, including server-side fetches initiated by route loaders. No changes to the auth architecture are required.


Trade-offs ​

Benefits:

  • Native SSR for public content — no bot detection workaround, no redirect overhead
  • SEO for card pages, public decks, craft guide
  • Same ecosystem as TanStack Router — zero cognitive shift
  • Hybrid SSR/CSR — public pages are pre-rendered, app shell is SPA
  • httpOnly cookie auth works identically with and without SSR
  • Deployment-agnostic (Node.js, Cloudflare Workers, Vercel, etc.)

Costs:

  • apps/web now requires a Node.js runtime at deployment (was previously static CDN)
  • TanStack Start v1.0 is newer — fewer third-party examples and integrations than Next.js
  • Route loaders add a second data-fetching pattern alongside TanStack Query hooks

Risks:

  • "Just one small DB import": The temptation to access packages/db directly from a loader will grow as the app scales. Mitigation: the constraint is enforced as an Architectural Rule in CLAUDE.md, not just documented here.
  • Ecosystem immaturity: v1.0 released March 2026. Some edge cases may lack documentation. Mitigation: TanStack Router (the core) is stable and widely used.
  • RSC not yet available: React Server Components are planned for a v1.x release. Not required for Decksmith's current scope.

Evolution History ​

2026-05-31: Initial decision ​

  • Evaluated TanStack Start v1.0 (released March 2026) as the foundation for apps/web
  • Confirmed SSR for public routes, CSR for authenticated app routes
  • Established constraint: no server functions, no backend code in apps/web
  • ADR-0010 hybrid meta endpoint superseded — SSR handles link previews natively
  • ADR-0005 updated with loader constraint

References ​

Built with VitePress