Skip to main content
Frameworks

TanStack Start on Easel

Deploy full-stack TanStack Start applications to Easel with SSR, streaming, server functions, and Nitro.

Easel runs TanStack Start applications built with the standard Nitro Vite plugin. Client assets and prerendered pages go to the CDN, and SSR, server functions, server routes, and middleware run in Easel Functions with streaming. Your app stays standard TanStack Start, not an Easel fork.

At a glance

Support levelProduction-ready
RenderingStatic, SSR, streaming, selective SSR, and SPA mode
Server runtimeNode.js
Deployment integrationNitro
Adapter requiredYes
Automatic detectionYes
Most recently tested withTanStack Start 1.168.x

Deploy a TanStack Start application

TanStack Start uses Nitro to produce the server entry and static assets Easel deploys. Install it:

npm install nitro

Add the Nitro Vite plugin after tanstackStart() and before the React plugin:

vite.config.ts
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
  plugins: [
    tanstackStart(),
    nitro(),
    react(),
  ],
});

Don't set a Nitro preset or NITRO_PRESET yourself. Easel selects the hosting output it expects during the platform build, and a different preset produces a server shape Easel can't deploy.

Create a project from your Git repository or deploy with the CLI:

easel deploy

Easel detects TanStack Start and runs your build script or vite build. Install command, build command, root directory, and Node.js version live in build configuration.

What Easel deploys

The Nitro build produces a client application and a server entry that handles TanStack Start requests.

Application outputEasel resource
JavaScript, CSS, fonts, and public filesCDN assets
Prerendered pagesCDN assets
Server-rendered routes and loadersEasel Functions
Server functionsApplication server function
Server routesApplication server function
Start middlewareApplication server function
Streaming responsesFunction response stream

Server functions, server routes, and middleware all live in one application server bundle. They aren't separate, individually configurable functions unless the build output splits them.

Supported features

FeatureSupportNotes
File-based routingSupportedGenerated TanStack Router routes are part of the build
Full-document SSRSupportedRoutes render in Easel Functions
Streaming SSRSupportedHTML is forwarded as the framework produces it
Route loaders and beforeLoadSupportedRun where the route's SSR mode puts them
Server functionsSupportedValidated, type-safe calls from client and server code
Streaming server functionsSupportedReadableStream and async-generator responses
Server routesSupportedExternal HTTP endpoints from the Start app
MiddlewareSupportedRequest, function, and server-route middleware
Selective SSRSupportedFull, data-only, or client-only per route
SPA modeSupportedThe application server provides the document fallback
Static prerenderingSupported with setupRoutes configured for prerendering are emitted as static output
Incremental Static RegenerationExperimentalDepends on the TanStack Start and Nitro versions in use
Cookies, redirects, headersSupportedStandard framework response APIs
Request cancellationSupportedServer work can observe the request's AbortSignal
Early HintsRuntime-dependentVerify in a preview deployment

Rendering

Each route chooses how much work happens on the server.

Full SSR

Full SSR sends the route's data and rendered markup from the server:

src/routes/products.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/products")({
  loader: async () => getProducts(),
  component: ProductsPage,
});

function ProductsPage() {
  const products = Route.useLoaderData();

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

The first request runs through the Start server entry in an Easel Function. Easel forwards HTML chunks as they render, so the shell reaches the browser while slower data resolves. TanStack Router handles later navigations in the browser.

Data-only and client-only routes

Set ssr: "data-only" to run the loader on the server and render the component in the browser. Set ssr: false for a route that depends entirely on browser APIs:

export const Route = createFileRoute("/dashboard")({
  ssr: "data-only",
  loader: async () => getDashboardData(),
  component: DashboardPage,
});

Server functions

Server functions hold server-only logic you can call from loaders, components, and other server functions:

src/data/products.functions.ts
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";

export const getProduct = createServerFn({ method: "GET" })
  .inputValidator(z.object({ id: z.string() }))
  .handler(async ({ data }) => {
    const { findProduct } = await import("./products.server");
    return findProduct(data.id);
  });

TanStack Start turns client-side calls into HTTP requests to the application server function. The handler can reach databases, server-only environment variables, request headers and cookies, and the request's cancellation signal.

Keep implementation details that must never reach the browser in .server.ts modules, and import them inside the handler as shown. TanStack Start's build keeps those modules out of the client bundle.

Streaming from server functions

A server function can return a ReadableStream or be an async generator:

export const generateReport = createServerFn({ method: "POST" }).handler(
  async function* () {
    for await (const section of buildReport()) {
      yield { status: "progress", section };
    }

    yield { status: "complete" };
  },
);

Easel keeps the response open and forwards each chunk as it's yielded. The invocation stays active until the stream closes, the client disconnects, or the function hits its maximum duration (limits). For work that must outlive the request, queue it and stream or poll its saved state.

Server routes

Server routes are HTTP endpoints for callers outside your Start app, such as webhooks, public APIs, and health checks:

src/routes/api/health.ts
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/health")({
  server: {
    handlers: {
      GET: async () => Response.json({ ok: true }),
    },
  },
});

They can return JSON, HTML, redirects, custom headers, ReadableStream bodies, and Server-Sent Events. Use server functions for calls your own app makes, and server routes for everything else.

Middleware

Start middleware adds shared behavior, such as authentication, request context, and response headers, across routes and server functions:

import { createMiddleware, getRequestHeader } from "@tanstack/react-start";

export const authMiddleware = createMiddleware().server(async ({ next }) => {
  const user = await authenticate(getRequestHeader("authorization"));

  if (!user) {
    throw new Response("Unauthorized", { status: 401 });
  }

  return next({ context: { user } });
});

This middleware runs inside your application function, after Easel's security layer. Use the Firewall for infrastructure-level filtering and Start middleware for application logic.

Static prerendering

TanStack Start can prerender selected routes at build time. Easel deploys the HTML and assets Start and Nitro emit to the CDN, and requests for those pages don't invoke a function. Easel doesn't discover routes on its own, and the configuration depends on your TanStack Start version. Check the generated routes in a preview deployment after changing it.

Caching

Set Cache-Control on server-route responses, or from a server function with setResponseHeaders:

import { createServerFn, setResponseHeaders } from "@tanstack/react-start";

export const getPublicCatalog = createServerFn({ method: "GET" }).handler(
  async () => {
    setResponseHeaders(
      new Headers({
        "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60",
      }),
    );

    return getCatalog();
  },
);

The Easel CDN caches eligible responses from these headers. Server functions are often user-specific, so cache them publicly only when the output is safe to share. See Caching.

Nitro storage and cache utilities are only as durable as their driver. In-memory and local-filesystem drivers aren't shared across function instances, so configure an external driver for shared data.

Environment variables

Server functions, server routes, middleware, and server-side loaders read variables from process.env. Vite inlines import.meta.env values into client code at build time, so treat them as public and redeploy after changing them. Set values per environment in Environment variables.

OpenTelemetry

Register standard Node.js OpenTelemetry instrumentation at server startup to trace loaders, server functions, and server routes. Easel shows your spans in request traces. See Tracing.

Project configuration

SettingDefault
Install commandDetected from the package manager
Build commandPackage script or vite build
Development commandPackage script or vite dev
Client and server outputDetected from the Nitro build

In a monorepo, set the root directory to the folder that holds the app's package.json and vite.config.

Local development

Keep using npm run dev. The Nitro plugin runs the application server inside the Vite dev server. Use a preview deployment to check production bundling, streaming, and CDN caching.

Known limitations

  • Nitro integration maturity. nitro/vite builds on Vite's environment APIs and is still changing. TanStack Start, Nitro, or Vite updates can change the server output. Pin versions when stability matters more than updates.
  • Incremental Static Regeneration is experimental. Use build-time prerendering or explicit cache headers for predictable results.
  • Function runtime. No WebSockets, an ephemeral filesystem, per-instance memory, and no work after the response unless you register it. See Function runtime and Background work.

Troubleshooting

The build doesn't produce a server entry

Confirm that nitro is installed and that nitro() appears after tanstackStart() in vite.config.ts. A client-only build has no server entry for Easel to deploy.

Easel can't deploy the generated output

Remove any Nitro preset from your config and any NITRO_PRESET environment variable. A Node, Bun, or provider preset produces a different output layout.

A server function is unavailable

Confirm that it's created with createServerFn and exported from a module in the production build.

Server-only code appears in the client build

Move private code into a .server.ts module and import it inside the server handler. Check that server variable names don't use Vite's public client prefix.

Compatibility policy

Easel's TanStack Start baseline is the version under At a glance. TanStack Start and nitro/vite move quickly, so test upgrades in a preview deployment, as described in the compatibility policy.

Next steps