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.
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 inroute-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.jsoncfor the Rust Worker bundle.apps/tanstack-web/wrangler.jsoncfor the TanStack Start Worker.@cloudflare/vite-plugininapps/tanstack-web/vite.config.ts, registered beforetanstackStart().- root
wranglertooling plusbun check:cloudflareto validate both Worker configs without contacting Cloudflare.
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.varsapps/tanstack-web/.dev.vars
.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:
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:
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:
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:
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:
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:
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
BACKENDbinding, 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 atapps/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 inapps/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 ofapps/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}, andhive/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 toBackendConfig+wrangler.jsoncsecrets. 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}, pluscron/infrastructure/{docker-recovery-alerts,sample-resources}— reads a filesystem archive / controls the deployment host (Docker control), andcron/infrastructure/sync-trust-cacheneeds 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 behindpackages/internal-api/ the Rust backend.
apps/tanstack-web/migration/route-overrides.json with an evidence note rather
than leaving them as untriaged legacy-next backlog.
Current inventory:
151pages58layouts587route handler artifacts8cron handlers800total tracked route artifacts215migrated artifacts,228terminal artifacts, and572remaining legacy-owned artifacts in the checked manifest
apps/tanstack-web/src/routes/** files:
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:
legacy-next:
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 underapps/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-apihelpers, 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 checkrunsnode scripts/check-tanstack-api-access.jsto enforce thatapps/tanstack-web/srcdoes not call relative protected/api,/internal, or/trpcpaths 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). Theapps/backend crate root was decomposed to honor this:
src/lib.rs(~680 LOC) is now just the module registry (mod <handler>;), the public types, andpub(crate) usere-exports.src/dispatch/holdshandle_backend_requestplus onedispatch_chunk_NN.rsper route-table chunk. New route arms append to adispatch_chunk_NN.rs; when a chunk approaches 700 LOC, add a freshchunk_NN.rsand wire it intodispatch/mod.rs. Each chunk doesuse 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 fromlib.rswithpub(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.
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
Treatapps/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/webAPI route (any method). Ifapps/backendalready 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 returnNonefor un-ported methods so they fall through). If it is not owned yet, register/refresh the route inapps/tanstack-web/migration/route-overrides.jsonand regenerate withbun migration:tanstack:manifest, so the new surface is tracked as backlog instead of invisible debt. Verify ownership with the runtime coverage probe inapps/backend/AGENTS.md(migrated method = COVER, un-ported = FRESH). - New or changed dashboard page/route. Mirror the manifest registration so
apps/tanstack-webtracking stays accurate, and route shared data access throughpackages/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-apifacade over a one-offapps/webfetcher; 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/weband not reflected in the manifest is the debt this migration exists to avoid.bun migration:tanstack:checkand 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 oflegacy-next:
- a
route-overrides.jsonentry with a non-empty evidence note that marks the legacy artifact asmigratedoraccepted-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-apifacade when the TanStack app or shared UI needs to call the endpoint
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, andDISCORD_APP_DEPLOYMENT_URL, plusAURORA_EXTERNAL_URLandAURORA_EXTERNAL_WSID, withwrangler secret put,wrangler versions secret put, or Cloudflare dashboard secret bindings. The TanStack Worker reaches the backend Worker through theBACKENDservice binding; configureBACKEND_INTERNAL_URLonly for non-binding HTTP fallback runs. Usesecret putfor 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_SECRETon 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.varsandapps/tanstack-web/.dev.varsfiles. Do not commit literal origins that are account-specific private targets, tokens, API keys, cookies, or session material. CMS_APP_URLandNEXT_PUBLIC_CMS_APP_URLare 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. Previewworkers.devorigins are different sites fromtuturuuu.localhostand productiontuturuuu.com, so cookie/session behavior must be proven with smoke or E2E evidence rather than assumed from Docker. - Keep
BACKEND_ENV=previewfor 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.
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 definestanstack-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:
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 ignoredtmp/benchmarks/web-migration/<timestamp>/report.json.
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:
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:
--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:
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, andtanstack-rootprobes - 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-alland cutover gate validation - representative backend smoke routes must stay below the smoke ceiling
- strict runs fail when any required route is unreachable
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
- Every manifest route is
migratedoraccepted-removal. bun migration:tanstack:cutover-checkpasses.bun test:e2e:web:docker -- --frontend comparepasses and has a compare evidence report.bun benchmark:web-setups -- --setup compare --profile full --require-allpasses or has approved exceptions. HTTPS origins must present a certificate trusted by the host; use the local development CA orNODE_EXTRA_CA_CERTSinstead of disabling certificate verification.bun smoke:cloudflare --output <path>passes against the preview Workers.bun migration:tanstack:gates -- --e2e-report <path> --benchmark-report <path> --cloudflare-smoke-report <path>passes.bun check:dockerandbun checkpass.- Legacy
apps/webremains available as the rollback fixture until production monitoring shows the TanStack/Rust path is stable.