Deploy a TanStack Start app
This guide uses the start-basic example from the TanStack repository to deploy a server-rendered React app built with TanStack Start and Vite. It has nested routes, loaders that fetch posts and users on the server, server functions, API routes, and error and not-found boundaries. The guide adds the three files a District build needs, deploys the app, then changes and deploys it again.
You need a session and the git credential helper from steps 1 and 2 of Getting started. Node.js 22 or later and pnpm are required. Commands use production. district login --staging pins staging for all later commands.
Create the repository and fetch the example
district repo create creates and clones the repository. The clone is empty. gitpick then copies one directory from the TanStack repository without the rest of its history.
# Create and clone the empty repository
district repo create "TanStack Start demo" --clone
cd tanstack-start-demo
# Copy the example into the clone. -o writes to a non-empty directory and keeps .git.
pnpm dlx gitpick TanStack/router/tree/main/examples/react/start-basic . -oThe main branch of the TanStack repository changes. This guide was tested at commit 37877da166fe, with @tanstack/react-start 1.168, Vite 8, and Nitro 3. To fetch the exact example, replace main in the path with that commit.
The example includes a .gitignore that excludes node_modules and .output. Add .wrangler: Wrangler writes its local cache there.
Configure the build for District
A District build runs pnpm install, then the build script in package.json, then bundles the worker that wrangler.jsonc at the repository root names. Getting started lists the keys that the build honors. The example builds with Nitro. Its cloudflare_module preset writes the server entry to .output/server/index.mjs and the client files to .output/public.
The preset also writes its own Wrangler configuration with no_bundle and rules. The build refuses both keys, so the deployConfig: false option turns that file off, and the root wrangler.jsonc names the built entry and the assets directory instead. Three edits make the example build on District.
1. Select the Cloudflare preset
In vite.config.ts, give the Nitro plugin the preset and turn its deploy configuration off. The rest of the file remains unchanged.
// vite.config.ts
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { nitro } from 'nitro/vite'
export default defineConfig({
server: { port: 3000 },
resolve: { tsconfigPaths: true },
plugins: [
tailwindcss(),
tanstackStart({ srcDirectory: 'src' }),
viteReact(),
nitro({ preset: 'cloudflare_module', cloudflare: { deployConfig: false } }),
],
})2. Add the worker configuration
Create wrangler.jsonc at the repository root. nodejs_compat is required: the Nitro server entry imports Node.js modules.
// wrangler.jsonc
{
"name": "tanstack-start-demo",
"main": ".output/server/index.mjs",
"compatibility_date": "2026-08-01",
"compatibility_flags": ["nodejs_compat"],
"assets": { "directory": ".output/public" }
}3. Remove the Node.js start script
In package.json, delete the start script. It names a Node.js server that the Cloudflare preset does not write. The build script stays: vite build && tsc --noEmit is what the build step runs.
Check the build locally
pnpm install writes pnpm-lock.yaml. Commit it. The build then runs pnpm install --frozen-lockfile and installs the same versions. pnpm build writes the two output directories that wrangler.jsonc names.
pnpm install
pnpm build
ls .output/server/index.mjs .output/publicpnpm preview serves the built app in a local Workers runtime on the port that Vite prints.
Push, publish, and deploy
Commit and push the example and the three edits. The credential helper authenticates the push. district app create publishes the repository as an app with one primary target. district deploy builds the pushed commit and serves it on that target.
git add -A
git commit -m "TanStack Start on District"
git push
district app create
district deploy --yesThe build runs on District. The command prints one line per step, the deployment's status, then the target URL:
$ district app create
created tanstack-start-demo (happ_01M1…)
hostname tanstack-start-demo.ondistrict.net
$ district deploy --yes
repository tanstack-start-demo (repo_01M1…)
ref be66373cac24e2b85e282d33c16508a5e42dec9b
hostname tanstack-start-demo.ondistrict.net
environment production
build hbld_01M1… started (re-attach with: district build attach hbld_01M1…)
✔ checkout succeeded 3 s
✔ install succeeded 5 s
✔ build succeeded 4 s
✔ bundle succeeded 2 s
✔ export succeeded 1 s
✔ deployment hdep_01M1… succeeded
https://tanstack-start-demo.ondistrict.net/Open the URL. The home page, /posts, and /users render on the server from the example's loaders. /api/users answers JSON from an API route. A path unknown to the router renders the not-found boundary. The files under .output/public are served as static assets; every other request reaches the server entry.
Change the app and deploy again
Edit a route, such as the heading in src/routes/index.tsx, commit, and push. A deploy builds the clone's HEAD, so an unpushed commit is refused before the build starts.
git commit -am "Change the home page"
git push
# Serve the change on a preview target, then move the primary target to it
district deploy --new-preview --yes
district target set-deployment tanstack-start-demo hdep_01M1…
# Or deploy directly to the primary target
district deploy --yes--new-preview serves the build on a preview target with a generated label. The target expires 7 days after its last deployment. district deploy --from <hdep_…> then deploys the preview's build to the primary target without a second build. Managing deployments covers previews, promotions, and rollbacks. Custom domains serves the app on a name you own.