Skip to main content
apps/tanstack-web is the replacement frontend for the legacy Next.js apps/web runtime. It uses TanStack Start, TanStack Router, TanStack Query, Vite, React 19, Tailwind 4, and the existing Tuturuuu shared packages. apps/backend is the planned Rust API runtime. Do not create another backend service for this migration; extend apps/backend route groups and keep API contracts documented in apps/backend/api/openapi.yaml.
apps/backend is not currently deployed and receives no production traffic. apps/web remains the live API runtime. A route marked migrated in this document means its future Rust source contract is implemented and tested; it does not mean that request routing has cut over.
The crate can run as a native container and keeps a Cloudflare Workers Rust entrypoint in apps/backend/wrangler.jsonc for future, explicitly approved preview deployment. For exact local run commands and deployment steps across native local, Docker, Cloudflare Workers, Vercel, and VPS/Cloudflare Tunnel paths, use TanStack/Rust Local And Deployment.

Runtime Shape

Use bun dev:tanstack-web for local TanStack work. Docker dev also exposes the tanstack-web service on port 7824 and injects BACKEND_PUBLIC_ORIGIN, BACKEND_INTERNAL_URL, and BACKEND_INTERNAL_TOKEN for Rust backend calls. Cloudflare preview uses the BACKEND service binding first and keeps BACKEND_INTERNAL_URL as an HTTP fallback for local, Docker, and emergency non-binding runs.

Rendering And Cache Policy

Every migrated TanStack route must declare its rendering/cache class before it is marked terminal in route-overrides.json: This follows TanStack Start’s current guidance: prerender public paths through the Start Vite plugin, use standard HTTP cache headers for ISR-style pages, and prefer CDN headers such as CDN-Cache-Control for Cloudflare-specific shared cache control. Official references: Static Prerendering and Incremental Static Regeneration. The initial prerender set intentionally includes only public, unauthenticated, non-backend-backed pages. Backend-backed public pages are not prerendered at build time because Docker, CI, and Cloudflare builds must not require a live Rust backend just to emit static HTML. Instead, those pages use conservative ISR-style CDN headers from apps/tanstack-web/src/lib/platform/cache.ts.

Cloudflare Preview Deployment

Cloudflare preview deployments are supported before cutover so the team can validate the Worker runtime, smoke endpoints, and benchmark numbers without moving production traffic. The preview path uses:
  • apps/backend/wrangler.jsonc for the Rust Worker bundle.
  • apps/tanstack-web/wrangler.jsonc for the TanStack Start Worker.
  • @cloudflare/vite-plugin in apps/tanstack-web/vite.config.ts, registered before tanstackStart().
  • root wrangler tooling plus bun check:cloudflare to validate both Worker configs without contacting Cloudflare.
Validate the checked Cloudflare contract locally:
Current compatibility contract: apps/tanstack-web deployment runs bun run build internally through the app-local deploy:cloudflare script. Do not run it during docs-only or inspection work unless the task explicitly allows build commands. For a configuration-only preflight, use bun check:cloudflare. Both Wrangler configs declare required secret names with the schema-supported secrets.required property. The validator requires these names and rejects secret-looking values under vars: For local Worker previews, put secret values next to the owning Wrangler config in ignored files:
  • apps/backend/.dev.vars
  • apps/tanstack-web/.dev.vars
Do not commit .dev.vars* or .env.preview* files. bun check:cloudflare validates the ignore rules so preview tokens and account-specific Worker origins do not enter Git. The backend Worker uses BACKEND_ENV=preview in wrangler.jsonc. Do not set Cloudflare preview traffic to development; development-only migration routes remain limited to local development and the explicit local E2E bypass. Use wrangler secret put for initial preview bootstrapping only: Cloudflare creates and deploys a new active Worker version when that command changes a secret. For rotations, canaries, or any deployed Worker that already receives traffic, use wrangler versions secret put, then promote the resulting version with wrangler versions deploy. Deploy the Rust backend Worker first:
BACKEND_INTERNAL_TOKEN is required before backend preview deploys are useful: /readyz reports not-ready when the token is missing. TUTURUUU_APP_COORDINATION_SECRET is required before contact/profile preview routes can verify Tuturuuu ttr_app_ app-session tokens in production-like Cloudflare environments. CRON_SECRET and DISCORD_APP_DEPLOYMENT_URL are required before the Rust-owned Discord cron proxy routes can call the Discord app. AURORA_EXTERNAL_URL is required before the Rust-owned Aurora health and ingest routes can call the Aurora service, and AURORA_EXTERNAL_WSID is required before Aurora ingest routes can write rows. Wrangler prompts for values; do not place those values in wrangler.jsonc, vars, docs, or shell history. SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required backend Worker secrets for the Rust-owned contact/profile APIs. They stay server-only: the backend reads users and user_private_details, updates profile fields, and inserts support_inquiries through Supabase REST with the service-role key. GET /api/migration/status exposes only redacted readiness state and the Supabase origin, never the credential. Smoke-test the returned workers.dev URL before pointing the frontend at it:
Then configure the TanStack Worker. apps/tanstack-web/wrangler.jsonc declares a BACKEND service binding to tuturuuu-backend, so the backend Worker must be deployed first. Store only the browser-safe public backend origin and the shared internal token as Cloudflare secrets for preview runs:
For gradual/canary secret rotation after the first preview deploy, use the versions command family instead:
BACKEND_PUBLIC_ORIGIN should initially be the backend Worker origin, for example the https://<backend-worker>.<subdomain>.workers.dev URL returned by the backend deploy. It remains the browser-safe origin used for public probes and non-protected traffic. Start server functions prefer the BACKEND service binding for Worker-to-Worker backend calls and fall back to BACKEND_INTERNAL_URL only outside Cloudflare or when the binding is absent. Use BACKEND_INTERNAL_URL in local Docker, local Node preview, or emergency HTTP fallback runs; do not make it a required TanStack Worker secret while the service binding is declared. The TanStack Worker still needs the same BACKEND_INTERNAL_TOKEN value so Start server functions can call protected Rust migration inventory endpoints. Local wrangler dev can omit origin bindings because packages/internal-api falls back to http://localhost:7820; local migration dashboard calls will fall back to the checked manifest when the backend token is not configured. Deploy the TanStack Start Worker after the backend origin secrets are present:
For a local Worker-shape smoke before deployment, use Wrangler preview servers in separate terminals and then point the smoke command at those origins:
The checked Wrangler configs reserve distinct local preview ports so both Workers can run at the same time: http://localhost:8780 for the Rust backend and http://localhost:8784 for TanStack Start. Keep those ports stable because bun check:cloudflare validates them and the smoke examples below assume them. Set the same env variable names that the deployed Workers use. Keep values in the shell, local ignored env files, or Wrangler secret storage only: BACKEND_INTERNAL_TOKEN and BACKEND_PUBLIC_ORIGIN. When the TanStack local Worker cannot use the BACKEND service binding, set BACKEND_INTERNAL_URL to the local backend Wrangler origin as an HTTP fallback. Then run the preview smoke command against the returned Worker origins:
For deployed previews, replace those local origins with the workers.dev or custom preview origins returned by Wrangler. Set BACKEND_INTERNAL_TOKEN in the shell before running the smoke command. The smoke command probes /healthz, /readyz, authenticated /api/migration/status, missing/invalid-token rejection for the protected migration status endpoint, and the TanStack root shell. It fails on unexpected 4xx and 5xx responses, requires the TanStack root shell to show Backend reachable, and redacts the bearer token from output. That final check proves the deployed Start server function can reach the Rust Worker through the service binding or configured HTTP fallback and token, not just that the static shell rendered. Use --output to persist the smoke report under ignored tmp/benchmarks/web-migration/<timestamp>/cloudflare-smoke.json when the run is part of a cutover rehearsal. For deployed E2E smoke runs, set TANSTACK_EXPECT_BACKEND_REACHABLE=1 and TANSTACK_EXPECT_BACKEND_TARGET=cloudflare-workers so the migration shell must prove the frontend can reach the Rust Worker. For a non-terminal Cloudflare preview gate, attach that smoke report while explicitly allowing the still-legacy route inventory:
This command is only preview evidence. It proves the current Workers can talk to each other, but it does not satisfy the final route parity, Docker E2E, or benchmark cutover gates. This is still a preview/canary path. Keep apps/web as the production source of truth until the manifest, Docker E2E, benchmark, and cutover gates below pass. Do not map production hostnames to the preview Worker while workers_dev canary traffic and route parity are still in progress. Rollback is currently DNS/routing only: remove or roll back any Cloudflare route or custom-domain mapping that sends traffic to the preview Worker, and leave the Docker blue/green apps/web production stack serving the canonical host. Do not delete Wrangler secrets during rollback unless the secret itself is compromised; keeping them bound makes redeploying the previous Worker version or re-running smoke checks deterministic. Inspect and roll back preview Worker deployments with Wrangler:
Worker rollback does not revert external resources, bindings, routes, custom domains, or secret values. If a preview deploy regresses, roll back the Worker version or remove the preview route, then run bun smoke:cloudflare against the remaining preview origins before resuming canary traffic. Protected TanStack-to-backend traffic now has a Cloudflare service binding from the TanStack Worker to the backend Worker: binding name BACKEND, service tuturuuu-backend. The shared backend client prefers that binding in server-only TanStack code and keeps BACKEND_INTERNAL_URL as a non-Cloudflare HTTP fallback. Protected workspace and admin APIs still must not move to production Worker traffic until each route has backend tests, OpenAPI coverage, an internal-api facade, and migration gate evidence. Browser code must never receive service tokens, and server-owned protected data must stay behind server-owned calls. Current Cloudflare limitations:
  • Runtime service binding smoke is pending. The config and internal client use the BACKEND binding, but deployed smoke still needs to prove the binding path in Cloudflare preview before production host routing.
  • Production host routing is pending full route manifest completion, Docker compare-mode E2E evidence, benchmark evidence, and bun migration:tanstack:gates.
  • Preview Workers should not become the owner for private workspace/admin APIs until each route has backend tests, OpenAPI coverage, an internal-api facade, and migration gate evidence.

Route Ownership Manifest

The current migration manifest is checked in at apps/tanstack-web/migration/route-manifest.json. It records the current apps/web/src/app route inventory, intended target owner for each route, and a progress summary grouped by target owner and route kind. API and route-handler entries also include a methods array derived from exported Next route methods; summary.methodCounts tracks total exported GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS handlers. When only some methods on a legacy route.ts are migrated, route-overrides.json can split that file into method-level artifacts with parentId and method; the unmigrated paired methods stay visible as legacy-next.

Rust GET-handler migration wave

The backend port proceeds GET-first: a route’s read path moves to a Rust handler in apps/backend/src/<module>.rs while its mutation methods stay on the live Next.js route. The Rust handler matches the legacy mount path exactly and returns None (never 405) for every method it does not own, so the worker falls through to Next.js for POST/PUT/PATCH/DELETE. Because the route is still the source of truth for writes, it correctly stays legacy-next in the manifest until the remaining methods are ported and a cutover decision is recorded — GET-only ports do not flip ownership status, so they add no manifest debt. As of this wave the curated non-special fresh-GET surface is migrated: ~235 GET routes now have Rust handlers. The only deliberately-skipped GET routes are /api/v1/workspaces/:wsId/mind/boards/:boardId and /api/v1/workspaces/:wsId/whiteboards/:boardId, whose CRDT/Yjs binary state lives in a store the worker cannot reach; they stay on Next.js. Still pending as separate phases: mutation methods for the GET-migrated routes, and special-auth GET routes (/api/cron/* CRON_SECRET, /api/trpc, /:locale pages, OAuth callbacks, and Hive routes backed by HIVE_DATABASE_URL). Each batch is verified with bun check:backend (mirrors the CI Rust gate: cargo fmt --check, cargo clippy --locked --all-targets --features native -- -D warnings, cargo test --locked, and the wasm32-unknown-unknown worker check) plus a runtime dual coverage probe that asserts every migrated GET is served by the dispatcher (COVER) and every un-ported method still falls through (FRESH).

Worker-incompatible routes (native-backend or Next.js only)

Some legacy routes cannot run on the Cloudflare Worker build of apps/backend because the wasm sandbox has no arbitrary sockets, no filesystem, and no Redis/Postgres drivers. They split into three buckets:
  • Native-backend portable (deferred, needs secrets wired): the scheduler /api/cron/* jobs that just “call an external API + write Supabase” — finance/exchange-rates, inventory/polar-product-sync, payment/{orders,products,subscriptions}, ai/sync-models, tasks/generate-embeddings, calendar/{provider-sync,smart-schedule}, process-{post-email-queue,topic-announcement-queue,notification-batches}, and hive/simulate. These ARE portable to the native Rust container target (--features native, which can do outbound HTTP and reach external DBs) once each job’s provider secret/connection is added to BackendConfig + wrangler.jsonc secrets. They are not portable to the wasm Worker build. Until the secrets are wired they stay on Next.js (Bearer CRON_SECRET).
  • Host/infra-coupled (stay where the runner lives): the cron-runner monitoring surface — monitoring/cron/{control,run,runner-recovery,executions}, plus cron/infrastructure/{docker-recovery-alerts,sample-resources} — reads a filesystem archive / controls the deployment host (Docker control), and cron/infrastructure/sync-trust-cache needs Redis. These belong with whatever orchestrates the runner, not a stateless handler.
  • Transport that can’t be a single handler: /api/trpc/[trpc] is a catch-all that dispatches the entire tRPC procedure router (appRouter). The transport stays on Next.js; its data access migrates incrementally as individual procedures move behind packages/internal-api / the Rust backend.
Record the chosen disposition for each in apps/tanstack-web/migration/route-overrides.json with an evidence note rather than leaving them as untriaged legacy-next backlog. Current inventory:
  • 151 pages
  • 58 layouts
  • 587 route handler artifacts
  • 8 cron handlers
  • 800 total tracked route artifacts
  • 215 migrated artifacts, 228 terminal artifacts, and 572 remaining legacy-owned artifacts in the checked manifest
Regenerate it after adding or removing legacy routes:
Regenerate the TanStack Router route tree after adding or removing apps/tanstack-web/src/routes/** files:
This command preserves the TanStack Start Register augmentation footer that bun check:cloudflare validates for the Cloudflare-compatible Start runtime, then formats the generated routeTree.gen.ts with the generator’s isolated Biome config so repeated route-tree generation does not leave persistent dirty output. The Rust/TanStack Cloudflare workflow runs scripts/generate-tanstack-route-tree.test.js whenever the generator changes. Route ownership status is preserved through apps/tanstack-web/migration/route-overrides.json. Add an override with a non-empty evidence note when a route becomes migrated or receives an accepted removal decision; do not edit the generated manifest by hand. Check that the manifest still matches the legacy route tree while migration is in progress:
Cutover mode is intentionally stricter. It fails while any route remains marked as legacy-next:
The full cutover gate also requires explicit Docker E2E, benchmark, and Cloudflare smoke evidence:
This command validates route parity, terminal migration statuses, backend-owned route mapping, compare-mode Docker E2E evidence, and a full compare benchmark report without committing generated artifacts. The output JSON is the review/handoff artifact for cutover sign-off and stays under ignored tmp/ paths with the source reports. The benchmark report must be created with --setup compare --profile full and include every required metric comparison; smoke reports alone are useful for previews but are not sufficient cutover evidence. The Cloudflare smoke report must come from bun smoke:cloudflare against distinct live Rust backend and TanStack Worker origins, and the cutover gate verifies that each required probe URL matches that provenance. The manifest also includes progress.byOwner, progress.byKind, and progress.topLegacyRoutes. Use those fields when splitting the remaining port work across frontend and backend owners. Use each route’s methods list when checking API parity so a GET port does not accidentally hide an unmigrated POST, OPTIONS, or mutation handler on the same route artifact. The Rust backend exposes the same derived progress at:
apps/tanstack-web reads that endpoint through packages/internal-api and falls back to the checked manifest when the backend is offline. Method-level migration ownership is only valid when the deployment path can route traffic by HTTP method or otherwise avoid sending still-legacy methods to Rust. For example, the Rust backend can own OPTIONS preflight responses for an auth route while the corresponding POST or GET artifact remains legacy-next; a path-only proxy must keep that whole path on Next until the auth method is migrated too. First migrated ownership: First TanStack Start migrated ownership: Accepted removals: Recently migrated backend runtime candidates: Frontend pages now terminal: Frontend migration candidates blocked on auth/data ownership:

Frontend Runtime Adapters

TanStack route ports should use the adapter modules under apps/tanstack-web/src/lib/platform/ instead of rebuilding Next.js behavior in each route: The root TanStack shell already uses the head, locale, theme, and Query adapters. New route ports should add route-specific metadata through createPageHead, prefetch loader data through the shared QueryClient, and pass only sanitized session state to client components.

Data And API Rules

  • TanStack loaders and server functions may act as a BFF layer for SSR, cookies, headers, and query hydration.
  • Product data reads/writes move to Rust-owned endpoints in apps/backend.
  • Browser/shared UI code calls packages/internal-api helpers, not scattered raw API paths.
  • Protected data stays behind server-owned private/admin access. Do not expose private Supabase or protected workspace reads directly to browser code.
  • Use TanStack Query for client fetching and mutation. Do not fetch data in useEffect.
  • bun check runs node scripts/check-tanstack-api-access.js to enforce that apps/tanstack-web/src does not call relative protected /api, /internal, or /trpc paths directly and does not import or create Supabase clients. Run the focused command when reviewing a frontend port before the full repo check.

Backend Crate Structure And The 700-LOC Ceiling

Every source file in the repo — Rust included — must stay well-maintained and under a hard 700-LOC ceiling whenever possible (start splitting around ~400 LOC). The apps/backend crate root was decomposed to honor this:
  • src/lib.rs (~680 LOC) is now just the module registry (mod <handler>;), the public types, and pub(crate) use re-exports.
  • src/dispatch/ holds handle_backend_request plus one dispatch_chunk_NN.rs per route-table chunk. New route arms append to a dispatch_chunk_NN.rs; when a chunk approaches 700 LOC, add a fresh chunk_NN.rs and wire it into dispatch/mod.rs. Each chunk does use crate::*; to reach crate-root helpers/types.
  • Cohesive helper families live in named submodules, each under 700 LOC: types.rs, response.rs, runtime.rs, migration.rs, legacy_routes.rs, static_routes.rs, route_predicates.rs, config_env.rs, constants.rs, native.rs, worker_runtime.rs. Each is re-exported from lib.rs with pub(crate) use <mod>::*; so existing call sites resolve unchanged.
  • The unit-test suite lives in src/tests.rs (mod tests;), split by area so no test file crosses the ceiling.
To extract a new family: move the items into src/<mod>.rs, header it with use crate::*; (plus any external use serde…), give moved items pub(crate) visibility, then add mod <mod>; pub(crate) use <mod>::*; to lib.rs. Keep a struct and the functions that read its private fields in the same module so fields only need pub(crate) where a cross-module reader (e.g. tests.rs) requires it. Do not bulk-restructure lib.rs/dispatch/ while batches are appending without a tmp/agent-coordination/ claim. See apps/backend/AGENTS.md for the full rules.

No New Debt While The Switch Is Pending

Treat apps/web, apps/backend, and apps/tanstack-web as one system. The migration runs in parallel with normal feature work, so any change that touches only apps/web silently grows the backlog the cutover must clear. All future work must keep the three surfaces consistent where applicable:
  • New or changed apps/web API route (any method). If apps/backend already owns that path, port the same behavior change into the Rust handler in the same PR (match status codes, body shape, cache headers; migrate GET first and return None for un-ported methods so they fall through). If it is not owned yet, register/refresh the route in apps/tanstack-web/migration/route-overrides.json and regenerate with bun migration:tanstack:manifest, so the new surface is tracked as backlog instead of invisible debt. Verify ownership with the runtime coverage probe in apps/backend/AGENTS.md (migrated method = COVER, un-ported = FRESH).
  • New or changed dashboard page/route. Mirror the manifest registration so apps/tanstack-web tracking stays accurate, and route shared data access through packages/internal-api (consumed by both frontends) rather than app-local fetchers, so the eventual TanStack port is a move, not a rewrite.
  • Shared data access. Prefer adding a packages/internal-api facade over a one-off apps/web fetcher; the facade is the seam both the Next.js and TanStack frontends call, and it forwards to the Rust backend server-side.
  • Tracking, not blocking. You do not have to finish the Rust/TanStack port in every PR, but you must leave the manifest accurate. A route that is added to apps/web and not reflected in the manifest is the debt this migration exists to avoid. bun migration:tanstack:check and the backend coverage probe are the guards.

Security And Test Requirements

Every migrated Rust endpoint needs the same ownership evidence before its manifest entry can move out of legacy-next:
  • a route-overrides.json entry with a non-empty evidence note that marks the legacy artifact as migrated or accepted-removal
  • an OpenAPI path or schema update in apps/backend/api/openapi.yaml
  • a Rust route dispatcher test covering the success case; full-route ownership also needs unsupported method behavior, while method-level ownership needs an assertion that still-legacy sibling methods are not claimed by Rust
  • a packages/internal-api facade when the TanStack app or shared UI needs to call the endpoint
Contact/profile backend routes now meet that terminal bar for the app-session contract used by TanStack contact: GET /api/v1/users/me/profile, PATCH /api/v1/users/me/profile, and POST /api/v1/inquiries verify ttr_app_ app-session JWTs, enforce same-origin mutation checks for cookie auth, validate request bodies, call Supabase REST through the server-owned Rust outbound adapter, and have mocked native/Worker persistence tests. PATCH /api/v1/users/me/full-name separately revalidates the browser Supabase session, validates the required trimmed full_name, and upserts the authenticated user’s user_private_details row with the caller token. GET / PATCH /api/v1/users/me/default-workspace separately preserves the legacy default-workspace read fallback to null, supports current-user app-session reads, and updates the authenticated user’s saved default workspace only after caller-token workspace membership validation. PATCH /api/v1/inquiries/:id separately revalidates the browser Supabase session, requires a Tuturuuu/XWF email domain, and updates the inquiry admin flags through the same Rust Supabase REST adapter. Broader Supabase session revalidation remains part of the dedicated auth migration milestone, so route notes must stay explicit about the app-session versus browser-session boundary. scripts/backend-openapi-migration-contract.test.js compares the migrated Rust-owned route artifacts in apps/tanstack-web/migration/route-manifest.json with apps/backend/api/openapi.yaml. Add or update the OpenAPI operation in the same commit that marks a Rust route artifact migrated; otherwise bun check fails. For method-level overrides, the same evidence applies to each migrated method, and the route override must leave every unmigrated sibling method as a generated legacy-next artifact. Do not mark a full route migrated only because a safe preflight or placeholder method is implemented in Rust. The manifest check compares both aggregate method counts and each route’s method list against the current apps/web/src/app tree, including generated export patterns such as createOfflineRoute(...) exports. Regenerate the manifest after adding, removing, or changing exported methods in a legacy route.ts. Non-terminal routes with methods: [] are still artifacts in the ownership inventory; port, restore an exported method, or mark them accepted-removal with an explicit reason before cutover. Rust JSON responses must keep the shared response security defaults: Content-Type: application/json, Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none', Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff, and X-Frame-Options: DENY. Route ports that replace legacy public probes or API handlers must preserve explicit cache behavior, including Cache-Control: no-store for /api/health and Cache-Control: public, max-age=300, must-revalidate for /.well-known/*. Non-JSON routes should use the empty-response path instead of serializing JSON null when the legacy handler returned no body. Protected workspace, cron, job, and admin endpoints stay server-owned. Browser code cannot call protected Rust routes directly, cannot receive service tokens, and cannot bypass packages/internal-api / TanStack server functions for session-aware data access. Worker-bound secrets belong in Cloudflare secret bindings; keep local, Docker, and Wrangler configuration to environment variable names only. For Cloudflare specifically:
  • Bind BACKEND_INTERNAL_TOKEN, BACKEND_PUBLIC_ORIGIN, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, CRON_SECRET, and DISCORD_APP_DEPLOYMENT_URL, plus AURORA_EXTERNAL_URL and AURORA_EXTERNAL_WSID, with wrangler secret put, wrangler versions secret put, or Cloudflare dashboard secret bindings. The TanStack Worker reaches the backend Worker through the BACKEND service binding; configure BACKEND_INTERNAL_URL only for non-binding HTTP fallback runs. Use secret put for first preview bootstrap only because it deploys a new active version immediately; use the versions command family for rotations and canaries.
  • Bind TUTURUUU_APP_COORDINATION_SECRET on the backend Worker before testing contact/profile routes. The Rust verifier also accepts existing app-coordination fallback secret names for compatibility, but the dedicated secret name is the preferred Cloudflare binding.
  • Keep local Worker secret values in ignored apps/backend/.dev.vars and apps/tanstack-web/.dev.vars files. Do not commit literal origins that are account-specific private targets, tokens, API keys, cookies, or session material.
  • CMS_APP_URL and NEXT_PUBLIC_CMS_APP_URL are public origin allowlist inputs for the WebGL upload preflight. They may be supplied through Wrangler vars or local .dev.vars, but they are not backend tokens and must not be listed as required secrets.
  • Keep private/admin data ownership server-side. If a migrated endpoint needs a Supabase service role or private schema access, the Rust backend owns that call path; the browser receives only the authorized response shape.
  • Review CORS, cookie domain, SameSite, secure-cookie, and session-origin behavior before mapping a custom hostname. Preview workers.dev origins are different sites from tuturuuu.localhost and production tuturuuu.com, so cookie/session behavior must be proven with smoke or E2E evidence rather than assumed from Docker.
  • Keep BACKEND_ENV=preview for Cloudflare preview. Development-only migration routes and local E2E bypasses must not be enabled by Worker deploys.
  • Do not bypass the migration gate because a Worker smoke passed. Worker smoke proves deploy compatibility; cutover still requires manifest, Docker E2E, and benchmark evidence.
Migration inventory endpoints are also protected because they expose checked legacy route source paths and route ownership state. GET /api/migration/status, GET /api/migration/manifest, GET /api/migration/progress, and GET /api/migration/cutover-gates require Authorization: Bearer <BACKEND_INTERNAL_TOKEN>. The TanStack migration dashboard calls them from a Start server function through packages/internal-api, which adds the bearer token only on the server. The first Rust-owned migration contracts are exposed from apps/backend and consumed by the TanStack shell through a Start server function: /api/migration/cutover-gates is the dashboard authority for cutover state. It must stay blocked while any manifest route remains legacy-next, any backend route artifact is not mapped to rust-backend, or required Docker E2E and benchmark evidence is missing.

Docker And E2E

The production compose stack defines tanstack-web, tanstack-web-blue, and tanstack-web-green services. The Docker Bake file includes matching blue-green-tanstack-web* targets so benchmark and cutover work can build candidate TanStack images beside the legacy web images. apps/backend/Dockerfile copies apps/tanstack-web/migration/route-manifest.json into the Rust build context because the backend includes the manifest at compile time for the migration contract endpoints. Run bun check:docker after manifest path or Dockerfile changes. Legacy E2E remains the default:
TanStack E2E uses the same Playwright suite and points at the TanStack route:
Run both modes sequentially before cutover:
Compare mode writes tmp/e2e/web-migration/compare-report.json after running both frontends. Each frontend result includes normalized origin evidence, passRate, wallMs, and Playwright JSON reporter test counts so bun migration:tanstack:gates can evaluate same-origin mistakes, E2E regressions, and zero-test false positives from the same evidence file. The gate rejects missing, credentialed, invalid, or identical Next/TanStack origins, and it rejects compare reports that do not prove nonzero Playwright execution for both frontends. Set E2E_COMPARE_REPORT_PATH when a run needs to write the file elsewhere under tmp/. apps/web/e2e/public-marketing-routes.noauth.spec.ts is the first shared public-route parity suite for this migration. It runs under the existing apps/web Playwright project so --frontend tanstack and --frontend compare reuse the same assertions for migrated landing, product, legal, redirect, offline, branding, blog, careers, demo, security, partners, UI docs, visualization, and static solution routes. scripts/public-marketing-e2e-coverage.test.js compares the migrated public page entries in apps/tanstack-web/migration/route-manifest.json with that no-auth Playwright spec. When a public marketing, UI docs, redirect, or offline page is marked migrated, the route needs a concrete default-locale E2E example in the shared spec before bun check can pass.

Benchmarks

Use the benchmark harness to compare reachable Next and TanStack frontend routes, plus independent Rust backend smoke routes. It does not yet compare legacy Next.js API route timing against equivalent Rust endpoints. Reports are written under ignored tmp/benchmarks/web-migration/<timestamp>/report.json.
Backend benchmark samples include protected migration endpoints. Set BACKEND_INTERNAL_TOKEN when running the benchmark against local Docker, preview Workers, or any backend that requires internal authorization; otherwise the protected samples correctly fail as unreachable 4xx responses. To compare the legacy production/staging origin against Cloudflare preview Workers, pass all origins explicitly:
Compare reports must prove that the legacy Next.js and TanStack Start samples came from distinct normalized frontend origins. bun benchmark:web-setups rejects same-origin compare runs before writing a report, and bun migration:tanstack:gates rejects benchmark evidence with missing origins, credentialed origins, identical Next/TanStack origins, route/sample URLs that do not match the recorded setup origin, or matched frontend routes without their own frontend-route-p95 comparison. Docker E2E compare evidence uses the same distinct-origin rule for the browser-facing Next and TanStack frontend runs. Use full mode when both frontends are running with representative seeded data:
In compare mode, --require-all fails the benchmark command when any required metric evidence is missing. That keeps local benchmark rehearsals aligned with bun migration:tanstack:gates instead of deferring missing dev-ready, build, image-size, RSS/CPU, JS-output, E2E, or API latency evidence until cutover. Pass reviewer-approved non-HTTP metrics and accepted regression notes through --evidence when the harness cannot measure them directly:
After the full benchmark writes a report, pass that report to bun migration:tanstack:gates with the Docker E2E compare report and Cloudflare smoke report. Generated benchmark, smoke, and E2E reports stay under ignored tmp/ paths. Cutover gates reject evidence older than 24 hours by default; use --evidence-max-age-ms <milliseconds> only when a rehearsal explicitly accepts a longer freshness window. Default gates:
  • frontend route p95 must not regress more than 25%
  • Cloudflare smoke evidence must include passing backend-health, backend-ready, backend-migration-status, backend-migration-status-missing-token, backend-migration-status-invalid-token, and tanstack-root probes
  • cutover reports must include the full metric comparison contract: API p50/p95/p99, dev ready time, first-route cold time, warm navigation time, Docker build time, image size, JS output size, production RSS/CPU baseline, E2E wall time, and E2E pass rate, with numeric baseline, candidate, ratio, and threshold values for each required comparison
  • full compare evidence must include complete matching frontend route coverage; unmatched Next-only or TanStack-only routes fail --require-all and cutover gate validation
  • representative backend smoke routes must stay below the smoke ceiling
  • strict runs fail when any required route is unreachable
Route samples include aggregate p50Ms, p95Ms, and p99Ms, plus coldMs for the first successful request and warmP50Ms / warmP95Ms / warmP99Ms for later samples. Use those fields for first-route cold time, warm navigation time, and representative API latency notes. Add Docker build time, image size, process RSS/CPU, JS output size, and E2E wall-time evidence to the same benchmark directory when running full cutover rehearsals. Record approved exceptions in the migration PR. Do not silently widen thresholds to make a run pass.

Cutover Checklist

  1. Every manifest route is migrated or accepted-removal.
  2. bun migration:tanstack:cutover-check passes.
  3. bun test:e2e:web:docker -- --frontend compare passes and has a compare evidence report.
  4. bun benchmark:web-setups -- --setup compare --profile full --require-all passes or has approved exceptions. HTTPS origins must present a certificate trusted by the host; use the local development CA or NODE_EXTRA_CA_CERTS instead of disabling certificate verification.
  5. bun smoke:cloudflare --output <path> passes against the preview Workers.
  6. bun migration:tanstack:gates -- --e2e-report <path> --benchmark-report <path> --cloudflare-smoke-report <path> passes.
  7. bun check:docker and bun check pass.
  8. Legacy apps/web remains available as the rollback fixture until production monitoring shows the TanStack/Rust path is stable.