A third-party experiment, @tinloof/tanstack-wsr, is rendering selected TanStack Start routes inside a service worker. The implementation is early and has some real gaps, but the underlying idea is useful enough that I think we should explore it at the framework level, especially for local-first applications.
This issue is about the execution model and the public framework hooks it would require. It is not a proposal to adopt the current package as-is.
The execution model
A route opts into worker-side rendering:
export const Route = createFileRoute('/dashboard')({
wsr: true,
loader: () => readLocalData(),
component: Dashboard,
})
Once a service worker controls the page, a hard navigation to that route is handled roughly like this:
- The service worker matches the request against the same route tree used by Start.
- If the matched route chain opts in, it creates a memory history for the URL and runs the loaders inside the service worker.
- It renders the router to an HTML
Response before the document commits.
- The normal Start client entry hydrates that HTML.
- SPA navigations continue to run through the client router as usual.
The result is SSR-shaped HTML generated on the user's device. A local-first route can read IndexedDB or a sync engine during a hard load and paint the current local state immediately, without waiting for an origin round trip or flashing an empty client shell first.
The experiment also introduces worker route handlers, similar to Start server routes, so mutations and reads can share one sync-engine client inside the service worker:
export const Route = createFileRoute('/api/todos')({
worker: {
handlers: {
GET: async () => Response.json(await getClient().list()),
POST: async ({ request }) => {
await getClient().add(await request.json())
return new Response(null, { status: 204 })
},
},
},
})
That part may or may not belong in the same abstraction, but it exposes the bigger question: should Start have a supported way to run a client-safe router entry in a service worker?
The current experiment proves feasibility, not readiness
The package manages to reuse the route tree, compile server functions into origin RPCs, prune server-only routes, render with TanStack Router's SSR utilities, and hydrate with the normal Start client bundle. That's a meaningful proof of concept.
It also has to locate internal Start Vite plugins by name and manually replay parts of the client transform. A supported version should not depend on internals like tanstack-start-core::server-fn:client or reconstruct Start's build pipeline in a second Vite build.
There are several product and correctness questions we would need to resolve before this could be considered a Start feature:
- The first visit still renders at the origin because no service worker exists yet. A loader may therefore run on the server, in the service worker on later hard loads, and in the browser during SPA navigation. That three-environment contract needs to be explicit.
- Generating HTML offline does not make the page reliably offline. The client entry, CSS, fonts, images, and other assets need a versioned manifest and cache strategy.
- A synthetic navigation response must preserve or deliberately reconstruct CSP, COOP, COEP, Permissions-Policy, Referrer-Policy, cache headers, status, and other response metadata normally supplied by Start, middleware, or the deployment platform.
- Non-WSR routes should not wait for a large worker to boot, construct a router, and only then begin the origin request. We would need navigation preload or a browser-native routing strategy, plus measurements.
- Navigation detection should use the actual request mode and worker mutations need an origin/client/CSRF model.
- Loader or render failures need a defined fallback to the origin rather than producing a failed navigation.
- Base paths, service-worker scope, server-function URLs, asset URLs, deploy atomicity, and update behavior all need first-class handling.
- Service workers are ephemeral. Module-level sync clients can be recreated at any time, so durable state still belongs in IndexedDB or another persistent store.
- We need production, offline, CSP, subpath, worker-route, update, and browser-matrix tests, not only a Chromium dev-server happy path.
Questions for Start and Router
- Is the core abstraction a route render target, an execution environment, or a separate worker entry that consumes the router?
- Should a route opt into worker document rendering directly, or should the worker decide using route metadata exposed by Router?
- Can Start expose a public way to build a client-safe router entry, including route pruning, server-function transforms, asset manifests, and hydration metadata?
- What request and response context should be available to loaders and middleware during a worker render?
- How should origin security headers and per-request CSP nonces work for synthetic documents?
- Are worker route handlers a useful primitive, or should applications keep that transport layer outside Router?
- Can non-WSR navigations bypass the worker without paying its startup cost?
- Is this best owned by Start, a supported adapter contract, or an external package built entirely on public hooks?
A useful first exploration
I would start with an example or experimental package rather than a core API:
- Produce a supported client-safe worker entry from the Start build.
- Render one local-only route on a controlled hard navigation.
- Carry the real client asset manifest and navigation response headers into the synthetic response.
- Add origin fallback, navigation preload, and a strict request gate.
- Measure subsequent hard-load latency, non-WSR regression, worker size, and hydration behavior across browsers.
- Test first visit, offline reload, deployment update, CSP, and a killed/restarted worker.
If the model survives those constraints, we can decide what the smallest public Router and Start primitives should be.
Related context:
A third-party experiment,
@tinloof/tanstack-wsr, is rendering selected TanStack Start routes inside a service worker. The implementation is early and has some real gaps, but the underlying idea is useful enough that I think we should explore it at the framework level, especially for local-first applications.This issue is about the execution model and the public framework hooks it would require. It is not a proposal to adopt the current package as-is.
The execution model
A route opts into worker-side rendering:
Once a service worker controls the page, a hard navigation to that route is handled roughly like this:
Responsebefore the document commits.The result is SSR-shaped HTML generated on the user's device. A local-first route can read IndexedDB or a sync engine during a hard load and paint the current local state immediately, without waiting for an origin round trip or flashing an empty client shell first.
The experiment also introduces
workerroute handlers, similar to Start server routes, so mutations and reads can share one sync-engine client inside the service worker:That part may or may not belong in the same abstraction, but it exposes the bigger question: should Start have a supported way to run a client-safe router entry in a service worker?
The current experiment proves feasibility, not readiness
The package manages to reuse the route tree, compile server functions into origin RPCs, prune server-only routes, render with TanStack Router's SSR utilities, and hydrate with the normal Start client bundle. That's a meaningful proof of concept.
It also has to locate internal Start Vite plugins by name and manually replay parts of the client transform. A supported version should not depend on internals like
tanstack-start-core::server-fn:clientor reconstruct Start's build pipeline in a second Vite build.There are several product and correctness questions we would need to resolve before this could be considered a Start feature:
Questions for Start and Router
A useful first exploration
I would start with an example or experimental package rather than a core API:
If the model survives those constraints, we can decide what the smallest public Router and Start primitives should be.
Related context:
createServerFn@tanstack/start-server-core leaking into client bundle #3990