Back to Blog
Tutorial10 min read2026-07-05

How to Deploy a TanStack Start App

TanStack Start is a full-stack React framework with type-safe server functions, SSR, and streaming, powered by Vite and Nitro. Learn how to deploy it to production as a Node server.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

TanStack Start is the full-stack React framework from the TanStack team (creators of TanStack Query and Router). It combines type-safe routing, server functions, SSR, and streaming, built on Vite and Nitro under the hood. Because it uses Nitro, deploying to a self-hosted Node server is straightforward and portable. This guide walks through it.

What TanStack Start gives you

  • Type-safe file-based routing via TanStack Router.
  • Server functions (createServerFn) — call server code from the client with full type safety, no manual API layer.
  • SSR and streaming out of the box.
  • Nitro-powered server build that targets many runtimes.

The server functions feature is the headline: you write a function, mark it server-only, and call it from components — TanStack Start generates the RPC boundary for you.

Building for a Node server

TanStack Start builds with Vite and emits a Nitro server. Target the node-server preset for self-hosting:

npm run build

Configure the Nitro preset in your Vite/Start config (or via env):

// app.config.ts (or vite config, depending on version)
import { defineConfig } from '@tanstack/react-start/config';

export default defineConfig({
  server: {
    preset: 'node-server',
  },
});

The build emits a Nitro server (typically under .output/server) you run with Node, binding to HOST and PORT from the environment:

HOST=0.0.0.0 PORT=3000 node .output/server/index.mjs

Server functions with a database

Server functions are the natural place to query a database. Link a managed PostgreSQL so DATABASE_URL is injected, then:

import { createServerFn } from '@tanstack/react-start';
import postgres from 'postgres';

const sql = postgres(process.env.DATABASE_URL!);

export const getUsers = createServerFn({ method: 'GET' }).handler(async () => {
  return await sql`SELECT id, name FROM users LIMIT 20`;
});

This function runs only on the server. The client calls getUsers() and gets typed data back; the SQL, the client, and DATABASE_URL never ship to the browser. Initialize the postgres client at module scope so the pool persists across requests.

The Dockerfile

# ---- Build ----
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Runtime ----
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
COPY --from=build /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

Because Nitro bundles dependencies into .output, the runtime stage skips node_modules entirely — small image, fast cold start. Confirm your output directory; it may differ slightly across TanStack Start versions.

Environment variables

VariablePurpose
NODE_ENVproduction
HOST0.0.0.0
PORTinjected listen port
DATABASE_URLinjected by managed DB link

Vite only exposes env vars prefixed with VITE_ to the client. Keep DATABASE_URL and other secrets unprefixed so they stay server-side and are only read inside server functions.

Health checks

Add an API route or server function endpoint that returns 200, and point the platform's readiness probe at it. Nitro lets you define server routes under server/routes (or the framework's equivalent) for a plain /health endpoint that doesn't touch the database.

Migrations

If you manage schema with Drizzle or Prisma, run migrations as a release step rather than on server boot to avoid races across replicas:

npx drizzle-kit migrate

On PandaStack, wire this as a once-per-release job before the new version takes traffic.

Deploying

Push, connect the repo, link a managed PostgreSQL so DATABASE_URL is injected, set HOST and NODE_ENV, add the migration step, and deploy. The platform builds the Dockerfile (or auto-detects the framework) and runs the Nitro server. Live build logs surface Vite/Nitro errors; app logs confirm the server bound to the injected port.

git push origin main

A note on framework maturity

TanStack Start is newer than Next.js or Nuxt, and its config surface has shifted between releases — always check the current docs for the exact preset and output paths. The upside is its type-safety story for server functions is among the best in the React ecosystem.

Conclusion

TanStack Start deploys as a Nitro Node server: build with the node-server preset, run the emitted index.mjs binding to 0.0.0.0 and the injected port, and query DATABASE_URL inside type-safe server functions. Nitro's bundled output keeps the runtime image lean.

Try a TanStack Start app plus a managed PostgreSQL on PandaStack's free tier — connect your repo at [dashboard.pandastack.io](https://dashboard.pandastack.io) and the database auto-wires via DATABASE_URL.

References

  • [TanStack Start Documentation](https://tanstack.com/start/latest)
  • [TanStack Start: Server Functions](https://tanstack.com/start/latest/docs/framework/react/server-functions)
  • [TanStack Start: Hosting / Deployment](https://tanstack.com/start/latest/docs/framework/react/hosting)
  • [Nitro: Node Server Preset](https://nitro.build/deploy/runtimes/node)
  • [postgres.js Client](https://github.com/porsager/postgres)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also