Deploying TanStack Start to Firebase App Hosting: What Nobody Tells You

tanstack firebase deployment react vite

TanStack Start is one of the most exciting full-stack React frameworks to emerge in 2025. Type-safe routing, server functions that feel like local calls, and a Vite-native build pipeline. Firebase App Hosting is Google's answer to Vercel — push to GitHub, get a deployed SSR app. In theory, connecting the two should be straightforward. In practice, there's a gap in the docs that will cost you hours if you don't know about it.

Here's how to actually get it working.

The Problem Nobody Mentions

TanStack Start v1.132+ made a significant architectural change: it dropped Nitro as a default dependency. Previous versions bundled Nitro automatically, which meant your vite build produced a .output/server/index.mjs file — a real HTTP server that listened on a port. Hosting platforms knew what to do with that.

The new builds produce something different. You get dist/server/server.js, which exports a WinterCG-compatible fetch handler. It's a function, not a server. There's no listen() call. No port binding. Nothing that Cloud Run (which powers App Hosting) can health-check against.

If you deploy this to Firebase App Hosting, you'll see:

The user-provided container failed to start and listen on the port
defined by the PORT=8080 environment variable within the allocated timeout.

This isn't a misconfiguration. Your app genuinely isn't starting an HTTP server.

The Fix: Bring Nitro Back

TanStack provides an official escape hatch: @tanstack/nitro-v2-vite-plugin. This is a compatibility plugin that restores Nitro as your server runtime, complete with deployment presets for every major platform.

Install it:

npm install @tanstack/nitro-v2-vite-plugin

Then add it to your vite.config.ts, with the firebase_app_hosting preset:

import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin'
import viteReact from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    tanstackStart(),
    nitroV2Plugin({
      preset: 'firebase_app_hosting',
    }),
    viteReact(),
  ],
})

Now vite build produces two things that matter:

  1. .output/server/index.mjs — a real Node.js HTTP server that reads PORT from the environment
  2. .apphosting/bundle.yaml — a manifest that tells Firebase App Hosting exactly how to run your app

The generated bundle.yaml looks like this:

version: v1
runConfig:
  runCommand: node .output/server/index.mjs
metadata:
  framework: nitro
  frameworkVersion: 2.x

Firebase reads this and knows what to do. No custom start script needed in package.json, though adding one doesn't hurt:

{
  "scripts": {
    "start": "node .output/server/index.mjs"
  }
}

Setting Up App Hosting

With the build sorted, the Firebase side is standard:

firebase apphosting:backends:create

This walks you through connecting your GitHub repo and choosing a region. Every push to your chosen branch triggers a build and deploy.

The one thing that catches people out is secrets. App Hosting doesn't read .env files. Every environment variable referenced in your apphosting.yaml must exist in Firebase Secrets Manager:

firebase apphosting:secrets set MY_SECRET_NAME
firebase apphosting:secrets:grantaccess MY_SECRET_NAME --backend my-backend

If any secret is missing, the build fails immediately with a clear error pointing to the missing name. Set them all before your first deploy.

What About vite preview?

You might be tempted to use vite preview as your production server. Don't. It's a development tool for locally previewing builds. It doesn't respect PORT reliably, it doesn't bind to 0.0.0.0 by default, and it's not designed for production traffic. The Nitro server is purpose-built for this.

The Full Sequence

For anyone starting fresh:

  1. Scaffold your TanStack Start app
  2. Install @tanstack/nitro-v2-vite-plugin
  3. Add nitroV2Plugin({ preset: 'firebase_app_hosting' }) to your Vite config
  4. Create your Firebase project and App Hosting backend
  5. Set all secrets via firebase apphosting:secrets set
  6. Push to GitHub

Your app will be live at https://your-backend--your-project.region.hosted.app within a few minutes.

Looking Forward

TanStack Start is moving fast. The team has been clear that the Nitro plugin is a "temporary compatibility" layer while the ecosystem catches up to the new fetch-handler architecture. Eventually, platforms like Firebase will likely support WinterCG handlers natively. But today, in February 2026, the Nitro plugin is the production-ready path. It works, it's maintained, and it takes five lines of config to set up.

The gap between "builds locally" and "runs in production" is always where frameworks earn or lose trust. TanStack Start has the right escape hatch — it just needs better signposting.