Setting up Vercel static hosting (Bun + TanStack Start)
Static-hosting deploy config for a Bun project using TanStack Start, prerendered and served from Vercel.
Prerequisites
- Vercel account with the project imported
- Bun project using TanStack Start + Vite
Steps
Add
vercel.jsonat the project root{ "$schema": "https://openapi.vercel.sh/vercel.json", "bunVersion": "1.4.x", // Note this should be updated when Bun is updated, or just set to "1.x" "installCommand": "bun install --frozen-lockfile", "buildCommand": "bun run build", "outputDirectory": "dist/client", "headers": [ { "source": "/assets/(.*)", "headers": [ { "key": "Cache-Control", "value": "max-age=31536000, immutable" } ] } ] }Enable prerendering in
vite.config.tstanstackStart({ prerender: { enabled: true, crawlLinks: true, }, });Install static server function support
bun add @tanstack/start-static-server-functionsCache server function results at build time
Prerendering alone embeds data in HTML, but client-side navigation still needs server functions.
staticFunctionMiddlewarerecords each invocation during the build and writes JSON files underdist/client/__tsr/staticServerFnCache/, which the client fetches instead of calling a server.Option A: global (recommended when all server functions are read-only)
Create
src/start.tsand register the middleware for every server function:import { createCsrfMiddleware, createStart } from "@tanstack/react-start"; import { staticFunctionMiddleware } from "@tanstack/start-static-server-functions"; const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn", }); export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware], functionMiddleware: [staticFunctionMiddleware], }));Defining
src/start.tsdisables TanStack Start's automatic CSRF middleware — includecreateCsrfMiddlewareexplicitly as above.Option B: per function (if you have some server functions that shouldn't be static)
Add the middleware to individual server functions that should be cached. It must be the last entry in that function's
.middleware([...])array:export const listGuidesFn = createServerFn() .middleware([staticFunctionMiddleware]) .handler(async () => { ... });
Wire into project
- No separate integration code needed beyond the build/deploy config above
Verification
- Run
bun run buildlocally and confirmdist/clientcontains prerendered HTML for each route - Confirm
dist/client/__tsr/staticServerFnCache/contains JSON files for server function calls made during prerender - Deploy to Vercel and confirm pages load without a server round-trip and that asset responses carry the immutable cache header
Gotchas
crawlLinksonly prerenders pages reachable via discoverable links — orphaned routes won't be built and will need an explicit prerender entry