Logotipo de Bitdoze

Primeros pasos con TanStack Start y Convex (Guía 2026)

Guía paso a paso para empezar con TanStack Start y Convex: crea la base de un SaaS full-stack en React y conecta el backend en tiempo real de Convex en 2026.

Dragos

Actualizado Publicado 36 min de lectura

Primeros pasos con TanStack Start y Convex (Guía 2026)

TanStack Start

Part 1 of 1

1
Primeros pasos con TanStack Start y Convex (Guía 2026)

Este es el primer artículo de nuestra serie sobre cómo construir una aplicación SaaS moderna con TanStack Start y Convex. TanStack Start ya está en v1 Release Candidate, con las funciones completas y una API estable, y las herramientas han cambiado significativamente desde la versión original de 2025 de esta guía. Al terminar este artículo tendrás una app React full-stack funcionando con SSR, enrutamiento basado en archivos y un backend en tiempo real de Convex.

Qué ha cambiado desde 2025

El ecosistema de TanStack Start pasó por varios cambios incompatibles:

  • Estructura del proyecto: el directorio app/ pasó a src/
  • Servidor de desarrollo: vinxi fue reemplazado por un plugin nativo de Vite (construido sobre Nitro)
  • Cambio de nombre del paquete: @tanstack/start ahora es @tanstack/react-start
  • Gestión del head: Meta de @tanstack/start fue reemplazado por HeadContent de @tanstack/react-router

Esta guía refleja el estado actual (2026) del framework.

¿Qué es TanStack Start?

TanStack Start es un framework React full-stack construido sobre TanStack Router. Proviene del equipo de TanStack (los mismos detrás de TanStack Query, Router, Table y otras librerías muy usadas). Características clave:

  • Enrutamiento basado en archivos: igual que en Next.js o Remix, las rutas corresponden a archivos en src/routes/.
  • Renderizado del lado del servidor (SSR) con hidratación en el cliente.
  • Obtención de datos integrada mediante hooks de TanStack Query.
  • Tooling de Vite (o Rsbuild 2 como alternativa) para un desarrollo y compilación rápidos.
  • Salida de servidor con Nitro: despliega en cualquier host Node.js, plataforma serverless o VPS.

TanStack Start alcanzó la v1.0 Release Candidate en septiembre de 2025 y publica versiones 1.x en npm. La API se considera estable. Si quieres ver cómo se compara TanStack Start con Next.js y Astro, tenemos una comparativa dedicada.

¿Qué es Convex?

Convex es una plataforma backend-as-a-service en tiempo real. Se encarga de tu base de datos, funciones serverless y sincronización del cliente para que puedas centrarte en el código del frontend. También puedes crear una app en tiempo real con Convex usando otros frameworks.

  • Base de datos en tiempo real: sincroniza automáticamente los cambios de datos con los clientes conectados
  • Funciones serverless: escribe consultas (queries, lectura) y mutaciones (mutations, escritura) en TypeScript, alojadas en Convex
  • Seguridad de tipos de extremo a extremo: tipos compartidos entre frontend y backend
  • Panel web: gestiona datos, consulta logs y depura desde docs.convex.dev/dashboard
  • Almacenamiento de archivos, tareas programadas y búsqueda vectorial integrados

Convex se integra con TanStack Start mediante el paquete @convex-dev/react-query, que te permite consultar y mutar datos con los hooks estándar de TanStack Query.

Requisitos previos

Antes de empezar, asegúrate de tener:

  • Node.js >= 20.19 (se recomienda Node 22 LTS). El paquete @tanstack/react-router lo exige como mínimo. Si estás en Node 18, verás errores de motor al instalar.
  • npm (viene con Node)
  • Una terminal y un editor de código
  • Una cuenta de Convex es opcional para desarrollo local — Convex funciona en local sin ella. Puedes vincular una cuenta en la nube más tarde con npx convex login.

Sin Docker, sin VPS, sin DNS para este tutorial. Todo se ejecuta en local.

Instalación: genera el proyecto con Convex

La forma más rápida de empezar es la plantilla de Convex, que genera un proyecto con TanStack Start y Convex ya conectados:

Generación rápida (recomendado)

bash
npm create convex@latest -- -t tanstack-start my-saas-app
cd my-saas-app

Esto crea un proyecto con React 19, TypeScript, TanStack Start y la configuración del cliente de Convex ya lista. El directorio convex/ incluye un archivo de esquema y los tipos generados.

Configuración manual

bash
npx create-start-app@latest
cd my-app
npm install convex @convex-dev/react-query @tanstack/react-router-with-query @tanstack/react-query

La vía manual te da un proyecto TanStack Start básico. Tendrás que conectar Convex en src/router.tsx por tu cuenta (lo cubrimos en la sección del router más abajo). Consulta la documentación de TanStack Start para construir desde cero para más detalles.

Verifica que la generación funcionó:

bash
cat package.json | grep -E "tanstack|convex"

Deberías ver @tanstack/react-start, @tanstack/react-router, @tanstack/react-query, convex y @convex-dev/react-query en la salida.

Caso de fallo: si npm create convex falla, comprueba tu versión de Node (node -v) y la conectividad con el registro de npm (npm ping).

Arrancar el entorno de desarrollo

Ejecuta el servidor de desarrollo:

bash
npm run dev

Internamente, esto ejecuta convex dev --start 'vite dev' — la CLI de Convex controla Vite directamente. En la primera ejecución, Convex te pedirá un nombre para el proyecto y levantará un backend local:

text
? Choose a name: my-saas-app
✔ Downloaded Convex backend binary
✔ Started running a deployment locally at http://127.0.0.1:3210
    name as CONVEX_DEPLOYMENT to .env.local
    URL as VITE_CONVEX_URL to .env.local

  ➜ Local:    http://localhost:3000/
✔ Convex functions ready! (839.54ms)

Verifica que funciona

  1. Abre http://localhost:3000 en tu navegador — deberías ver la página por defecto.
  2. Confirma que aparece “Convex functions ready” en la terminal.
  3. Comprueba que .env.local contiene VITE_CONVEX_URL:
bash
cat .env.local | grep VITE_CONVEX_URL

Qué está pasando:

  • Inicialización de Convex: convex dev levanta un backend local y guarda VITE_CONVEX_URL y CONVEX_DEPLOYMENT en .env.local. Estas variables conectan tu frontend con el backend de Convex.
  • Servidor de desarrollo de Vite: tu app corre en http://localhost:3000.
  • Panel de Convex: ejecuta npx convex dashboard para abrir la interfaz web donde gestionar datos, ver logs y probar funciones.

Consejo: ejecuta npx convex login para vincular tu despliegue local a una cuenta de Convex y poder sincronizar y desplegar en la nube.

Casos de fallo:

  • Falta VITE_CONVEX_URL: si el frontend no puede conectar, reinicia npm run dev después de que el primer convex dev haya generado el archivo de entorno.
  • Puerto 3000 ocupado: Vite elige otro puerto automáticamente (mira la salida de la terminal), o define server.port en vite.config.ts.

Estructura del proyecto

La plantilla crea esta estructura (ojo: src/, no app/):

text
.
├── src/
│   ├── routes/               # Page routes
│   │   ├── __root.tsx        # Root layout (HTML structure, global providers)
│   │   └── index.tsx         # Homepage component ('/')
│   ├── router.tsx            # Router setup (integrates Convex + Query)
│   ├── routeTree.gen.ts      # Auto-generated route tree
│   ├── start.ts              # App entry point (replaces old client.tsx + ssr.tsx)
│   └── styles/
│       └── app.css           # Global styles
├── convex/                   # Backend logic
│   ├── schema.ts             # Database schema (defines tables)
│   ├── _generated/           # Auto-generated types and API
│   └── ...                   # Your queries and mutations
├── public/                   # Static assets
├── .env.local                # Environment variables (VITE_CONVEX_URL)
├── vite.config.ts            # Vite configuration
├── convex.config.ts          # Convex config
├── package.json
└── tsconfig.json

Archivos clave:

  • src/router.tsx — crea la instancia de TanStack Router y conecta Convex + el cliente de Query. Este es el punto de integración.
  • src/routes/__root.tsx — define el layout raíz (<html>, <head>, <body>) y renderiza <Outlet /> para las rutas hijas.
  • src/routes/index.tsx — la página de inicio, usa createFileRoute('/').
  • src/start.ts — el punto de entrada único que reemplaza a los antiguos client.tsx + ssr.tsx.
  • convex/schema.ts — define las tablas de tu base de datos. Consulta la documentación de esquemas de Convex.
  • .env.local — guarda VITE_CONVEX_URL. Nunca subas este archivo a Git.

Si ves un directorio app/

Si tu proyecto tiene app/routes/, app/router.tsx, app/client.tsx y app/ssr.tsx, estás siguiendo documentación desactualizada. La raíz correcta ahora es src/. Vuelve a generar el proyecto con la plantilla más reciente.

Cómo se conectan el router y el cliente de Convex

La integración ocurre en src/router.tsx. Este es el código actual de la plantilla:

tsx
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
import { routerWithQueryClient } from '@tanstack/react-router-with-query'
import { ConvexQueryClient } from '@convex-dev/react-query'
import { ConvexProvider } from 'convex/react'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  const CONVEX_URL = (import.meta as any).env.VITE_CONVEX_URL!
  if (!CONVEX_URL) console.error('missing envar VITE_CONVEX_URL')

  const convexQueryClient = new ConvexQueryClient(CONVEX_URL)
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        queryKeyHashFn: convexQueryClient.hashFn(),
        queryFn: convexQueryClient.queryFn(),
        gcTime: 5000,
      },
    },
  })
  convexQueryClient.connect(queryClient)

  const router = routerWithQueryClient(
    createRouter({
      routeTree,
      defaultPreload: 'intent',
      context: { queryClient },
      scrollRestoration: true,
      defaultPreloadStaleTime: 0,
      Wrap: ({ children }) => (
        <ConvexProvider client={convexQueryClient.convexClient}>
          {children}
        </ConvexProvider>
      ),
    }),
    queryClient,
  )
  return router
}

Qué está pasando:

  1. ConvexQueryClient se construye a partir de VITE_CONVEX_URL — es el puente entre las funciones de Convex y TanStack Query.
  2. QueryClient se configura con el hashFn y el queryFn de Convex para que las consultas pasen por Convex automáticamente.
  3. routerWithQueryClient de @tanstack/react-router-with-query conecta el router y el cliente de Query para la obtención de datos compatible con SSR.
  4. ConvexProvider envuelve la app mediante la prop Wrap, dejando el cliente de Convex disponible para todos los componentes.
  5. defaultPreloadStaleTime: 0 deja la caché en manos de React Query — esto evita peticiones duplicadas y datos obsoletos al usar routerWithQueryClient.
¿Por qué dos paquetes de integración?

Existen dos paquetes que coexisten para conectar TanStack Router con TanStack Query:

  • @tanstack/react-router-with-query — el que usa la plantilla oficial de Convex. Proporciona routerWithQueryClient().
  • @tanstack/react-router-ssr-query — el que aparece en la documentación de inicio rápido de Convex. Proporciona setupRouterSsrQueryIntegration().

Ambos funcionan. Esta guía sigue el enfoque de la plantilla (router-with-query) porque es lo que obtienes al generar el proyecto. Si lees la documentación de Convex y ves el otro paquete, no te confundas — resuelven el mismo problema con APIs ligeramente distintas.

Enrutamiento básico con TanStack Router

TanStack Start usa enrutamiento basado en archivos a través de TanStack Router. Dos archivos definen la base del enrutamiento:

src/routes/index.tsx — la página de inicio:

tsx
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
  component: Home,
})

function Home() {
  return (
    <div>
      <h1>Welcome to My SaaS App</h1>
      <p>Edit this page in <code>src/routes/index.tsx</code></p>
    </div>
  )
}

src/routes/__root.tsx — el layout raíz:

tsx
// src/routes/__root.tsx
import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'

interface MyRouterContext {
  queryClient: QueryClient
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: RootComponent,
})

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  )
}

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  )
}

Puntos clave:

  • createFileRoute('/') mapea un archivo a una ruta URL.
  • createRootRouteWithContext configura el layout raíz con contexto tipado (el queryClient del router).
  • HeadContent (reemplaza al antiguo Meta) inyecta metadatos en <head>.
  • Scripts inyecta los bundles de JavaScript al final de <body>.
  • Outlet renderiza el componente de la ruta actual.

Añadir cabecera y pie de página

Vamos a añadir una cabecera con navegación y un pie de página a src/routes/__root.tsx. Así todas las páginas mantienen una marca consistente:

Cambio incompatible desde 2025

El antiguo import de Meta desde @tanstack/start ya no existe. Usa HeadContent de @tanstack/react-router en su lugar. ScrollRestoration ahora se gestiona con scrollRestoration: true en la configuración del router — no hace falta ningún componente explícito.

tsx
// src/routes/__root.tsx
import {
  createRootRouteWithContext,
  HeadContent,
  Link,
  Outlet,
  Scripts,
} from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'

interface MyRouterContext {
  queryClient: QueryClient
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  notFoundComponent: () => (
    <div style={{ textAlign: 'center', padding: '2rem' }}>
      <h1>404 - Page Not Found</h1>
      <Link to="/">Go Home</Link>
    </div>
  ),
  component: RootComponent,
})

function RootComponent() {
  return (
    <RootDocument>
      <header
        style={{
          padding: '1rem',
          borderBottom: '1px solid #eee',
          background: '#f8f9fa',
          position: 'sticky',
          top: 0,
          zIndex: 10,
        }}
        role="banner"
      >
        <nav aria-label="Main navigation">
          <Link
            to="/"
            style={{
              marginRight: '1rem',
              fontWeight: 'bold',
              color: '#333',
              textDecoration: 'none',
            }}
            aria-label="Home page"
          >
            My SaaS App
          </Link>
        </nav>
      </header>
      <main style={{ padding: '1rem', minHeight: '80vh' }} role="main">
        <Outlet />
      </main>
      <footer
        style={{
          padding: '1rem',
          borderTop: '1px solid #eee',
          textAlign: 'center',
          background: '#f8f9fa',
        }}
        role="contentinfo"
      >
        <p>&copy; {new Date().getFullYear()} My SaaS App. All rights reserved.</p>
      </footer>
    </RootDocument>
  )
}

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  )
}

Qué ha cambiado respecto a la versión de 2025:

  • Meta fue reemplazado por HeadContent (importado de @tanstack/react-router, no de @tanstack/start).
  • ScrollRestoration se eliminó — ahora se gestiona con scrollRestoration: true en la configuración del router.
  • Se añadió notFoundComponent para corregir el aviso del 404.
  • La interfaz MyRouterContext es más simple — el cliente de Convex se envuelve con ConvexProvider en la prop Wrap del router, así que no necesita estar en el tipo del contexto.

Caso de fallo: si el import de HeadContent falla, lo estás importando desde @tanstack/start en lugar de @tanstack/react-router. Actualiza el import.

Nota sobre estilos: este ejemplo usa estilos en línea por simplicidad. La plantilla de Convex ahora incluye Tailwind v4, que puedes usar en su lugar. Los próximos artículos de la serie introducirán componentes de Radix UI.

Verifica la instalación (build y prueba de humo)

Ejecuta una compilación de producción para detectar errores de tipos y confirmar que todo compila:

bash
npm run build

Esto ejecuta npm run typecheck && vite build y genera la salida en .output/server/index.mjs (formato Nitro). Después prueba el servidor de producción:

bash
npm run start

Abre la URL que muestra la terminal. La app debería renderizarse igual que en modo desarrollo.

Verifica también el backend de Convex:

bash
npx convex dashboard

Esto abre el panel de Convex en tu navegador. Deberías ver tu proyecto con sus funciones.

¡Listo!

Ya tienes una app full-stack funcionando con:

  • SSR mediante TanStack Start (con Nitro)
  • Enrutamiento basado en archivos con TanStack Router
  • Backend en tiempo real de Convex conectado a través de TanStack Query
  • Un layout raíz con cabecera, pie de página y manejo de 404
  • Una compilación de producción sin errores

Caso de fallo: si npm run build falla con errores de TypeScript, ejecuta primero npm run typecheck para aislarlos. Si tienes errores de memoria (OOM) en un VPS pequeño, aumenta la memoria de Node: NODE_OPTIONS=--max-old-space-size=4096 npm run build.

Precios de Convex y notas de despliegue

Convex tiene un plan gratuito generoso, suficiente para prototipos y las primeras fases de desarrollo:

Desglose de costes

Plan gratuito ($0):

  • 1M de llamadas a funciones/mes
  • 20 GB-horas de cómputo de actions
  • 0,5 GB de almacenamiento de base de datos
  • 1 GB de almacenamiento de archivos
  • 40 despliegues
  • 1.000 sesiones concurrentes

Professional ($25/desarrollador/mes):

  • 25M de llamadas a funciones incluidas
  • Copias de seguridad diarias
  • Dominios personalizados
  • Streaming de logs

Business y Enterprise: mínimo $2.500/mes.

Precios a agosto de 2026 — consulta convex.dev/pricing para las cifras actuales.

Arquitectura de despliegue: Convex es un backend gestionado en la nube — tus funciones y la base de datos de Convex corren en la nube de Convex. El frontend de TanStack Start se puede desplegar en cualquier host Node.js o plataforma compatible con Nitro. Si quieres desplegar tu app TanStack Start en tu propio VPS, Dokploy es una opción sólida sobre algo como Hetzner Cloud.

Los precios de los VPS subieron en general en 2026 — si estás reconsiderando un servidor alquilado, mira qué ha cambiado y cuándo gana un mini PC.

Para desplegar la parte de Convex en producción, consulta nuestra guía sobre cómo desplegar una app de Convex en producción. Si prefieres mantener todo en tu propia infraestructura, puedes mirar autoalojar Convex — también tenemos benchmarks que comparan el rendimiento de Convex autoalojado frente al plan gratuito en la nube.

Solución de problemas habituales

Falta VITE_CONVEX_URL

Síntoma: el frontend no puede conectar con Convex. La consola muestra missing envar VITE_CONVEX_URL.

Solución: reinicia npm run dev. La primera ejecución de convex dev genera .env.local con la URL. Si arrancaste el frontend antes de que Convex terminara de inicializarse, la variable aún no estará disponible.

Verifica: cat .env.local | grep VITE_CONVEX_URL debería mostrar una URL como http://127.0.0.1:3210.

Confusión entre app/ y src/

Síntoma: estás creando archivos en app/routes/ pero nada funciona.

Solución: la estructura del proyecto pasó de app/ a src/. Si estás siguiendo un tutorial antiguo o la versión de 2025 de este artículo, tus archivos deben ir en src/routes/, src/router.tsx, etc. Si tienes dudas, vuelve a generar el proyecto con la plantilla más reciente.

Errores de orden de los plugins de Vite

Síntoma: la compilación falla con errores crípticos relacionados con los plugins de Vite.

Solución: en vite.config.ts, viteReact() (o @vitejs/plugin-react) debe ir después de tanstackStart() en el array de plugins:

ts
plugins: [
  tsConfigPaths({ projects: ['./tsconfig.json'] }),
  tanstackStart(),   // first
  viteReact(),       // after tanstackStart
],
Errores de verbatimModuleSyntax

Síntoma: bundles del servidor filtrándose en bundles del cliente, o errores de importación que no tienen sentido.

Solución: la documentación de TanStack Start advierte que verbatimModuleSyntax en tsconfig.json puede provocar que bundles del servidor se filtren en los bundles del cliente. Déjalo desactivado (ponlo en false o elimínalo).

El puerto 3000 ya está en uso

Síntoma: error EADDRINUSE al arrancar el servidor de desarrollo.

Solución: Vite elegirá otro puerto automáticamente — mira la salida de la terminal. Para forzar un puerto concreto, añade server: { port: 3001 } en vite.config.ts. O mata el proceso que usa el puerto 3000: lsof -ti:3000 | xargs kill.

El panel de Convex no carga

Síntoma: la URL del panel que aparece en la terminal no se abre o muestra un error.

Solución: ejecuta npx convex dashboard manualmente. Si estás en un servidor remoto, el panel se vincula a localhost por defecto — necesitarás un túnel SSH o una regla de firewall para acceder desde tu máquina local.

Aviso notFoundError en la terminal

Síntoma: Warning: A notFoundError was encountered on the route with ID "__root__", but a notFoundComponent option was not configured...

Solución: añade notFoundComponent a la definición de tu ruta raíz (ver la sección de cabecera/pie de página más arriba). Esto proporciona una página 404 personalizada en lugar del error por defecto.

Qué sigue en esta serie

Ahora que la base está lista, el siguiente artículo cubrirá cómo escribir tu primera consulta y mutación de Convex — usando el ejemplo de tasks del inicio rápido. Aprenderás a definir un esquema, escribir funciones del servidor y obtener datos con useSuspenseQuery y convexQuery de @convex-dev/react-query.

La serie también cubrirá autenticación con Clerk, componentes de Radix UI y pagos con Polar.sh.

Crea una app de tareas completa con TanStack Start