├── .gitignore ├── bun.lockb ├── worker-configuration.d.ts ├── tsconfig.json ├── app ├── env.d.ts ├── client.tsx ├── middleware.tsx ├── ssr.tsx ├── utils │ └── cfContext.ts ├── router.tsx ├── routes │ ├── __root.tsx │ └── index.tsx └── routeTree.gen.ts ├── readme.md ├── wrangler.toml ├── app.config.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vinxi 2 | dist 3 | node_modules 4 | .wrangler 5 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timoconnellaus/tanstack-start-workers/HEAD/bun.lockb -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler by running `wrangler types` 2 | 3 | interface Env { 4 | tanstack_start_workers: KVNamespace; 5 | ASSETS: Fetcher; 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "moduleResolution": "Bundler", 5 | "module": "ESNext", 6 | "target": "ES2022", 7 | "skipLibCheck": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/env.d.ts: -------------------------------------------------------------------------------- 1 | import type { H3EventContext } from "vinxi/http"; 2 | import type { PlatformProxy } from "wrangler"; 3 | 4 | declare module "vinxi/http" { 5 | interface H3EventContext { 6 | cf: CfProperties; 7 | cloudflare: Omit, "dispose">; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/client.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { hydrateRoot } from "react-dom/client"; 3 | import { StartClient } from "@tanstack/start"; 4 | import { createRouter } from "./router"; 5 | 6 | const router = createRouter(); 7 | 8 | hydrateRoot(document.getElementById("root")!, ); 9 | -------------------------------------------------------------------------------- /app/middleware.tsx: -------------------------------------------------------------------------------- 1 | import { defineMiddleware } from "vinxi/http"; 2 | 3 | export default defineMiddleware({ 4 | onRequest: async (event) => { 5 | if (import.meta.env.DEV) { 6 | const { getPlatformProxy } = await import("wrangler"); 7 | const proxy = await getPlatformProxy(); 8 | event.context.cloudflare = proxy; 9 | } 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | A minimal example of Tanstack Start running on Cloudflare Workers with SSR (not cloudflare pages) 2 | 3 | 1. `bun install` 4 | 2. `bun run dev` 5 | 2. `bun deploy` -- to deploy to a cloudflare worker 6 | 7 | To generate types for bindings / env vars: `bun run cf-typegen` 8 | 9 | Demo here: https://tanstack-start-workers.tim-oconnell-australia.workers.dev/ 10 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "tanstack-start-workers" 3 | compatibility_date = "2024-10-03" 4 | compatibility_flags = ["nodejs_compat"] 5 | main = "./dist/worker/index.js" 6 | assets = { directory = "./dist/public", binding = "ASSETS" } 7 | 8 | [[kv_namespaces]] 9 | binding = "tanstack_start_workers" 10 | id = "9d83514a12624e83a897647578f7281b" 11 | -------------------------------------------------------------------------------- /app/ssr.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { 3 | createStartHandler, 4 | defaultStreamHandler, 5 | } from "@tanstack/start/server"; 6 | import { getRouterManifest } from "@tanstack/start/router-manifest"; 7 | 8 | import { createRouter } from "./router"; 9 | 10 | export default createStartHandler({ 11 | createRouter, 12 | getRouterManifest, 13 | })(defaultStreamHandler); 14 | -------------------------------------------------------------------------------- /app/utils/cfContext.ts: -------------------------------------------------------------------------------- 1 | import { type HTTPEvent } from "vinxi/http"; 2 | 3 | export async function getCloudflareContext(event: HTTPEvent) { 4 | if (import.meta.env.DEV) { 5 | // Attach the cloudflare context 6 | const cf = await import("wrangler"); 7 | const proxy = await cf.getPlatformProxy({ persist: true }); 8 | return proxy; 9 | } else { 10 | return event.context.cloudflare; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/router.tsx: -------------------------------------------------------------------------------- 1 | import { createRouter as createTanStackRouter } from "@tanstack/react-router"; 2 | import { routeTree } from "./routeTree.gen"; 3 | 4 | export function createRouter() { 5 | const router = createTanStackRouter({ 6 | routeTree, 7 | defaultNotFoundComponent: () =>
Not Found
, 8 | }); 9 | 10 | return router; 11 | } 12 | 13 | declare module "@tanstack/react-router" { 14 | interface Register { 15 | router: ReturnType; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@tanstack/start/config"; 2 | import { App } from "vinxi"; 3 | 4 | const tanstackApp = defineConfig({ 5 | server: { 6 | preset: "cloudflare-pages", 7 | 8 | output: { 9 | dir: "{{ rootDir }}/dist", 10 | publicDir: "{{ output.dir }}/public", 11 | serverDir: "{{ output.dir }}/worker", 12 | }, 13 | 14 | rollupConfig: { 15 | external: ["node:async_hooks"], 16 | }, 17 | 18 | hooks: { 19 | compiled() {}, 20 | }, 21 | }, 22 | }); 23 | 24 | const routers = tanstackApp.config.routers.map((r) => { 25 | return { 26 | ...r, 27 | middleware: r.target === "server" ? "./app/middleware.tsx" : undefined, 28 | }; 29 | }); 30 | 31 | const app: App = { 32 | ...tanstackApp, 33 | config: { 34 | ...tanstackApp.config, 35 | routers: routers, 36 | }, 37 | }; 38 | 39 | export default app; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vinxi dev", 8 | "deploy": "bun run build && wrangler deploy", 9 | "build": "vinxi build", 10 | "start": "vinxi start", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "cf-typegen": "wrangler types" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "description": "", 18 | "dependencies": { 19 | "@tanstack/react-router": "^1.58.13", 20 | "@tanstack/start": "^1.58.13", 21 | "@vitejs/plugin-react": "^4.3.1", 22 | "react": "^18.3.1", 23 | "react-dom": "^18.3.1", 24 | "solid-js": "^1.9.1", 25 | "vinxi": "^0.4.3", 26 | "wrangler": "^3.78.10", 27 | "zod": "^3.23.8" 28 | }, 29 | "devDependencies": { 30 | "@cloudflare/workers-types": "^4.20240925.0", 31 | "@types/react": "^18.3.9", 32 | "@types/react-dom": "^18.3.0", 33 | "typescript": "^5.6.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import { createRootRoute } from "@tanstack/react-router"; 2 | import { Outlet, ScrollRestoration } from "@tanstack/react-router"; 3 | import { Body, Head, Html, Meta, Scripts } from "@tanstack/start"; 4 | import * as React from "react"; 5 | 6 | export const Route = createRootRoute({ 7 | meta: () => [ 8 | { 9 | charSet: "utf-8", 10 | }, 11 | { 12 | name: "viewport", 13 | content: "width=device-width, initial-scale=1", 14 | }, 15 | { 16 | title: "TanStack Start on Cloudflare Workers", 17 | }, 18 | ], 19 | component: RootComponent, 20 | }); 21 | 22 | function RootComponent() { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | function RootDocument({ children }: { children: React.ReactNode }) { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | {children} 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, useRouter } from "@tanstack/react-router"; 2 | import { createServerFn } from "@tanstack/start"; 3 | import { getEvent } from "vinxi/http"; 4 | 5 | const incrementCount = createServerFn("POST", async () => { 6 | const event = getEvent(); 7 | const response = 8 | await event.context.cloudflare.env.tanstack_start_workers.get("count"); 9 | if (!response) { 10 | await event.context.cloudflare.env.tanstack_start_workers.put( 11 | "count", 12 | JSON.stringify({ count: 1 }), 13 | ); 14 | } else { 15 | const previousCount = JSON.parse(response).count as number; 16 | await event.context.cloudflare.env.tanstack_start_workers.put( 17 | "count", 18 | JSON.stringify({ count: previousCount + 1 }), 19 | ); 20 | } 21 | }); 22 | 23 | const getCount = createServerFn("GET", async () => { 24 | const event = getEvent(); 25 | const response = 26 | await event.context.cloudflare.env.tanstack_start_workers.get("count"); 27 | if (!response) { 28 | return { count: 0 }; 29 | } else { 30 | return JSON.parse(response) as { count: number }; 31 | } 32 | }); 33 | 34 | export const Route = createFileRoute("/")({ 35 | component: Home, 36 | loader: async () => await getCount(), 37 | }); 38 | 39 | function Home() { 40 | const router = useRouter(); 41 | const state = Route.useLoaderData(); 42 | 43 | return ( 44 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/routeTree.gen.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | 5 | // @ts-nocheck 6 | 7 | // noinspection JSUnusedGlobalSymbols 8 | 9 | // This file is auto-generated by TanStack Router 10 | 11 | // Import Routes 12 | 13 | import { Route as rootRoute } from './routes/__root' 14 | import { Route as IndexImport } from './routes/index' 15 | 16 | // Create/Update Routes 17 | 18 | const IndexRoute = IndexImport.update({ 19 | path: '/', 20 | getParentRoute: () => rootRoute, 21 | } as any) 22 | 23 | // Populate the FileRoutesByPath interface 24 | 25 | declare module '@tanstack/react-router' { 26 | interface FileRoutesByPath { 27 | '/': { 28 | id: '/' 29 | path: '/' 30 | fullPath: '/' 31 | preLoaderRoute: typeof IndexImport 32 | parentRoute: typeof rootRoute 33 | } 34 | } 35 | } 36 | 37 | // Create and export the route tree 38 | 39 | export interface FileRoutesByFullPath { 40 | '/': typeof IndexRoute 41 | } 42 | 43 | export interface FileRoutesByTo { 44 | '/': typeof IndexRoute 45 | } 46 | 47 | export interface FileRoutesById { 48 | __root__: typeof rootRoute 49 | '/': typeof IndexRoute 50 | } 51 | 52 | export interface FileRouteTypes { 53 | fileRoutesByFullPath: FileRoutesByFullPath 54 | fullPaths: '/' 55 | fileRoutesByTo: FileRoutesByTo 56 | to: '/' 57 | id: '__root__' | '/' 58 | fileRoutesById: FileRoutesById 59 | } 60 | 61 | export interface RootRouteChildren { 62 | IndexRoute: typeof IndexRoute 63 | } 64 | 65 | const rootRouteChildren: RootRouteChildren = { 66 | IndexRoute: IndexRoute, 67 | } 68 | 69 | export const routeTree = rootRoute 70 | ._addFileChildren(rootRouteChildren) 71 | ._addFileTypes() 72 | 73 | /* prettier-ignore-end */ 74 | 75 | /* ROUTE_MANIFEST_START 76 | { 77 | "routes": { 78 | "__root__": { 79 | "filePath": "__root.tsx", 80 | "children": [ 81 | "/" 82 | ] 83 | }, 84 | "/": { 85 | "filePath": "index.tsx" 86 | } 87 | } 88 | } 89 | ROUTE_MANIFEST_END */ 90 | --------------------------------------------------------------------------------