Build a TanStack App
TanStack Start is a full-stack React framework built on TanStack Router. It provides file-based routing, server functions, server routes, SSR, and streaming out of the box. TanStack Start is a Vite plugin, so a TanStack Start app builds and deploys as a standard Node service on Railway.
Railway is an official TanStack hosting partner, and the TanStack scaffolder ships a Railway deployment option that configures your app to deploy here with no extra setup.
This guide covers how to deploy a TanStack app to Railway in three ways:
Create a TanStack app
Note: If you already have a TanStack app locally or on GitHub, skip to Choose a production server.
Ensure Node is installed, then create a new project with the Railway deployment option:
npx @tanstack/cli@latest create my-app --deployment railwayThe --deployment railway flag adds a Nitro server and a start script, which is everything Railway needs to build and run your app. Follow the prompts to choose your remaining options.
If you run the command without the --deployment flag, select Railway at the Deploy prompt. The default Nitro (agnostic) choice also deploys on Railway, but it adds the Nitro server without a start script, so Railway chooses the server command for you. Picking Railway keeps that decision explicit. See Choose a production server.
Run the app locally
cd my-app
npm run devOpen http://localhost:3000 to see your app.
Choose a production server
vite build compiles your app, but TanStack Start needs a server runtime on top of the build. Which one you use changes both the build output and the command that starts it:
| Setup | Build output | Start command |
|---|---|---|
Nitro with a start script (recommended) | .output/server/index.mjs | node .output/server/index.mjs |
Nitro, no start script | .output/server/index.mjs | node .output/server/index.mjs, chosen by Railway |
| No server runtime | dist/server/ and dist/client/ | npx srvx --prod -s ../client dist/server/server.js, chosen by Railway |
Use Nitro with a start script. That's what the Railway option in the scaffolder sets up, it produces a self-contained server bundle, and it's the configuration this guide documents from here on.
All three layouts deploy on Railway without configuration. When there is no start script, Railway's builder (Railpack v0.39 and later) picks the right server for your build output: node .output/server/index.mjs for Nitro apps, and srvx for the default Vite build. The build logs say which command Railway picked and suggest adding a start script.
Adding the start script is still recommended: it keeps the production server your decision, and it makes the app portable to hosts that only run npm start.
To add Nitro to an existing app, install it and register its Vite plugin:
npm install nitro// vite.config.ts
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react';
export default defineConfig({
plugins: [nitro(), tanstackStart(), viteReact()],
});Then add a start script, which Railway uses to run your app:
{
"scripts": {
"start": "node .output/server/index.mjs"
}
}Deploy the TanStack app to Railway
TanStack Start builds a Node.js server that handles SSR, server functions, server routes, and static asset serving. It deploys as a standard Node service on Railway.
Deploy from the CLI
- Install the Railway CLI:
- Install the CLI and authenticate it using your Railway account.
- Initialize a Railway Project:
- Run the command below in your TanStack app directory.
railway init - Follow the prompts to name your project.
- After the project is created, click the provided link to view it in your browser.
- Run the command below in your TanStack app directory.
- Deploy the Application:
- Use the command below to deploy your app:
railway up - This command will scan, compress and upload your app's files to Railway. You'll see real-time deployment logs in your terminal.
- Once the deployment completes, go to View logs to check if the service is running successfully.
- Use the command below to deploy your app:
- Set Up a Public URL:
- Generate a domain for your service:
railway domain - You can also generate one from the Networking section under the Settings tab of your service.
- Generate a domain for your service:
Deploy from a GitHub repo
- Create a New Project on Railway:
- Go to Railway to create a new project.
- Deploy from GitHub:
- Select Deploy from GitHub repo and choose your repository.
- If your Railway account isn't linked to GitHub yet, you'll be prompted to do so.
- Select Deploy from GitHub repo and choose your repository.
- Deploy the App:
- Click Deploy to start the deployment process.
- Once deployed, a Railway service will be created for your app, but it won't be publicly accessible by default.
- Verify the Deployment:
- Once the deployment completes, go to View logs to check if the server is running successfully.
- Set Up a Public URL:
- Navigate to the Networking section under the Settings tab of your new service.
- Click Generate Domain to create a public URL for your app.
Use a Dockerfile
If you'd rather control the build yourself, use a Dockerfile. This one assumes Nitro, which builds to .output:
FROM node:lts-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . ./
RUN npm run build
FROM node:lts-alpine
WORKDIR /app
COPY --from=build /app/.output ./.output
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]The Nitro build bundles its own dependencies into .output, so the runtime stage does not need node_modules or your package.json.
Note: With a Dockerfile, service variables aren't available during the build unless you declare them as build arguments. To bake a VITE_ variable into the client bundle, add ARG VITE_MY_VAR to the build stage before RUN npm run build. Committing a Dockerfile also switches your service to the Dockerfile builder on the next deploy, even if it previously built with Railpack.
Deploy via the CLI or from GitHub. Railway automatically detects the Dockerfile and uses it to build and deploy the app.
Port configuration
You don't need any. Both Nitro and srvx read the PORT environment variable that Railway sets, and both bind all interfaces by default, so your app is reachable as soon as it starts.
If you have an older app with an app.config.ts that sets a port, you can delete that file. It was part of the Vinxi-based setup that TanStack Start no longer uses, and it has no effect on current versions.
Server functions and server routes
Server functions run on the server, never in the browser. Use them to reach environment variables, databases, and other server-side resources from your loaders and components:
// src/fns.ts
import { createServerFn } from '@tanstack/react-start';
export const getMessage = createServerFn().handler(async () => {
const dbUrl = process.env.DATABASE_URL; // server-only
return { message: 'Hello from Railway' };
});To expose an HTTP endpoint, add a server property to a file route:
// src/routes/api.hello.ts
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: async () => {
return Response.json({ message: 'Hello from Railway' });
},
},
},
});Environment variables
TanStack Start apps use two kinds of variables, and they behave differently on Railway.
Server variables are read at runtime with process.env in server functions, server routes, and loaders. They're available on the next deploy after you change them, and they never reach the browser. Use them for secrets.
Client variables must be prefixed with VITE_, are read with import.meta.env.VITE_*, and are baked into the JavaScript bundle when npm run build runs. Never put secrets in a VITE_ variable, since anyone can read them in the shipped bundle.
Set both kinds as service variables on your Railway service. Variables are available during the build and at runtime, so VITE_ values are baked in correctly.
Changing a VITE_ variable takes effect on the next build. Updating a variable in the dashboard prompts a redeploy, and a redeploy of a Railpack-built service rebuilds it with the new value. A plain container restart doesn't rebuild, so it keeps the old value. See Manage environment variables in frontend builds for details.
Add a Postgres database
- In your Railway project, click + New, then Database, then PostgreSQL.
- Add the connection string to your TanStack app's service as a reference variable:
DATABASE_URL=${{Postgres.DATABASE_URL}}This connects over Railway's private network, so it works at runtime with no extra configuration. Query the database from server functions and loaders with an ORM like Drizzle or Prisma:
import { createServerFn } from '@tanstack/react-start';
import { drizzle } from 'drizzle-orm/node-postgres';
import { visits } from '../db/schema';
export const recordVisit = createServerFn().handler(async () => {
const db = drizzle(process.env.DATABASE_URL!);
await db.insert(visits).values({});
const rows = await db.select().from(visits);
return { visitCount: rows.length };
});Run migrations with a pre-deploy command
Run schema migrations in a pre-deploy command, which executes between the build and the deploy with access to your service variables and the private network:
- Navigate to your service Settings on Railway.
- In the Deploy section, set Pre-deploy Command to your ORM's migration command. For Drizzle:
npx drizzle-kit migrate
Keep the migration tool in dependencies rather than devDependencies so it's present in the deployed image. Generate migration files locally (npx drizzle-kit generate for Drizzle) so the pre-deploy step only applies them. The deploy logs show the migration output, and a failed migration stops the deploy before your app starts.
Use Bun instead of Node
Railpack detects a bun.lock file and switches the whole pipeline to Bun: it installs with bun install, builds with bun run build, and runs the production server with Bun. Scaffold with npx @tanstack/cli@latest create my-app --package-manager bun and deploy the same way as a Node app.
Troubleshooting
The build succeeds, but the domain returns a 502 and the deployment flips to CRASHED.
Check the runtime logs for srvx exiting with ENOENT ... dist/server/server.js. On Railpack versions before v0.39, an app with the nitro() plugin but no start script hit this on every deploy: the srvx fallback looked for dist/ while Nitro built .output/. Redeploy to pick up the current Railpack version, or add the start script from Choose a production server.
The app deploys as a static site, and every page 404s.
@tanstack/react-start is in devDependencies, so Railway's TanStack Start detection misses it. The build logs show Deploying as vite static site with a suggestion to move @tanstack/react-start to dependencies. Do that and redeploy.
A VITE_ variable is undefined in the browser, or shows a stale value.
Check the prefix first, since only VITE_-prefixed variables reach client code. If the variable is set and still shows an old value, it was baked in by an earlier build. Trigger a redeploy so the app rebuilds with the current value.
A deploy healthcheck fails even though the app starts cleanly.
Railway's healthcheck requires an HTTP 200 response and does not follow redirects, so a healthcheck path that answers a 3xx fails every deploy. Point it at a path that returns a 200 directly.
The build logs recommend setting up Nitro. Your app has no server runtime, so Railway is serving the default Vite build with srvx. This works, but Nitro is the recommended production server. See Choose a production server.
Pages render, but CSS and JavaScript assets 404.
Your server isn't serving the client build. With Nitro this is handled for you. Without it, check that a custom start command points srvx's -s flag at the client output directory.
The deploy works but the app serves the dev server.
Never use vite dev as a production start command. It's not a production server and won't behave correctly behind Railway's edge. Build the app and run the built server instead.
A custom server entry or start command isn't detected. Autodetection covers the standard layouts. If you run a custom server file, set a custom start command in your service settings, or use a Dockerfile for full control.
Next steps
Once your app is deployed, these guides cover what usually comes next:
- Manage environment variables - Handle
VITE_prefixed variables in TanStack Start. - Choose between SSR, SSG, and ISR - Understand rendering strategies.
- Add a Database Service - Connect Postgres, MySQL, Redis, and more.
- Monitor your app - Track logs, metrics, and deployment health.