bun run build compiles it to a folder of static assets (dist/ by default) that any static host — or CDN — can serve. Because routing is client-side (TanStack Router), every host needs one thing in common: a SPA fallback so that a deep link like /dashboard serves index.html instead of a 404, letting the router take over in the browser.
This page covers building the assets, the fallback and configuration each host needs, and how the deployed spa talks to your API across origins (CORS).
Build the assets
Each spa module ships its owndev, build, and preview Vite scripts. Build from the module directory:
dist/ (hashed JS/CSS, plus everything from public/ copied verbatim). That directory is what you upload to any of the hosts below.
Point the spa at your API
The spa runs in the browser on its own origin and calls your API on another, so it needs the API’s URL at build time. Vite exposes variables prefixed withVITE_ on import.meta.env, so set the base URL there and read it in your data-fetching hooks:
.env.production
CORS_ORIGINS on the API to the deployed spa URL. See Configure CORS on the API at the end of this page.
Cloudflare
Cloudflare serves static SPAs two ways. As of 2026 Cloudflare steers new projects to Workers static assets (the promoted default for React); Pages remains fully supported and is the simpler choice for a dashboard-driven Git flow. Both are free for static-asset requests.Workers static assets (recommended)
Deploy with Cloudflare’swrangler CLI. Add a wrangler.jsonc next to the spa that points at the build output and turns on single-page-application fallback:
wrangler.jsonc
not_found_handling: "single-page-application" returns index.html with a 200 for any path that doesn’t match a built asset — exactly what TanStack Router needs. Build, then deploy:
Pages
Deploy thedist/ folder directly with Wrangler:
bun run build and Build output directory to dist. Pages auto-detects an SPA (when there’s no top-level 404.html) and serves the root index.html for unmatched paths. To make the fallback explicit, add a _redirects file to public/ so it ships in the build:
public/_redirects
Custom domain
For a Worker, add a custom-domain route (Cloudflare provisions DNS + TLS automatically):wrangler.jsonc
Workers and Pages use different config keys — Workers uses the
assets
block (directory + not_found_handling); Pages uses pages_build_output_dir
and the _redirects file. Don’t mix them.Vercel
Vercel auto-detects the Vite preset, so the build command and output directory (dist) are inferred — you don’t configure them. The one thing the preset does not add is the SPA fallback, so add a vercel.json at the repo root that rewrites every path to index.html:
vercel.json
vercel run links the local directory to a Vercel project. Alternatively, connect the repo in the dashboard: Vercel then deploys automatically on every push, promoting the production branch (main by default) to production and giving every other branch and PR its own preview URL.
Use
rewrites, not the legacy routes array, for the SPA fallback — and
don’t mix the two in one vercel.json. If you also enable cleanUrls, change
the rewrite destination to /.GitHub Pages
GitHub Pages serves a project site under a sub-path —https://<user>.github.io/<repo>/ — so two things need adjusting before it works:
-
Set Vite’s
baseto the repo name so asset URLs resolve under the sub-path:Match the router to it so client-side routes live under the same prefix:vite.config.ts -
SPA fallback: Pages has no server rewrite, but it serves
404.htmlfor any unmatched path. Copy the builtindex.htmlto404.htmlso the app boots and the router handles the route.
.github/workflows/deploy-spa.yml
A user/organization site served at the domain root
(
https://<user>.github.io/) needs no base — use / (or omit it). The
sub-path base is only for project pages.Deno Deploy
Use the current Deno Deploy at console.deno.com. Deno Deploy Classic (dash.deno.com) is deprecated and shuts down on July 20, 2026, so don’t start there.
The new platform has first-class static-site support with SPA fallback built in — no serve entrypoint needed. Connect the Git repo and add a deploy block to deno.json so builds are reproducible:
deno.json
runtime.spa: true serves index.html for paths that don’t match a static file, which is the client-side-routing fallback. Vite isn’t in Deno Deploy’s auto-detected framework list, so set the install/build/runtime values yourself as above.
With deployctl (CLI alternative)
If you deploy from the CLI instead of Git, the bundled file server does not fall back to index.html, so ship a tiny entrypoint that does:
serve.ts
Choosing a host
All four serve the samedist/ bundle. They differ only in how you configure the SPA fallback and where the build runs.
| Host | Deploy | SPA fallback | Notes |
|---|---|---|---|
| Cloudflare Workers | wrangler deploy | not_found_handling: "single-page-application" in wrangler.jsonc | 2026 default for new SPAs. |
| Cloudflare Pages | wrangler pages deploy dist or Git | Automatic, or _redirects /* /index.html 200 | Simple Git dashboard flow. |
| Vercel | vercel --prod or Git | rewrites in vercel.json | Vite preset auto-detects dist. |
| GitHub Pages | GitHub Actions | Copy index.html → 404.html | Needs Vite base: "/<repo>/" for project sites. |
| Deno Deploy | Git or deployctl | runtime.spa: true, or serveDir fallback | Use console.deno.com, not Classic. |
Configure CORS on the API
A deployed spa loads from its own origin and calls the API on another, so the browser enforces CORS. The spa side needs nothing beyond the correct API URL (VITE_API_URL); the API must be told to allow the spa’s origin. Set this on the API, not the spa:
SameSite=None; Secure so they’re sent on cross-origin requests, and keep CORS_CREDENTIALS=true.
Checklist
- Build with
bun run build; upload thedist/folder. - Configure the SPA fallback for your host — every client-side route depends on it.
- Set
VITE_API_URL(and otherVITE_*values) at build time; keep secrets out of them. - On GitHub Pages, set Vite
baseand the routerbasepathfor project sites. - Add the spa’s origin to the API’s
CORS_ORIGINSallowlist.
Related
- Spa overview — what a spa module is and how it’s built.
- Deploy an API — ship the backend the spa calls.
- CORS — the cross-origin policy the API applies to spa requests.
- Data fetching — where the spa reads
VITE_API_URLto call the API. - spa:create — scaffold a spa module.