Skip to main content
TanStack is a suite of high-performance, framework-agnostic libraries that solve common challenges in modern web development, including data fetching, routing, tables, forms, and virtualization. Its headless, composable design gives developers full control over rendering while providing powerful utilities for complex UI and data workflows. TanStack can only be used on Application Hosting; it cannot be deployed as a static site. We recommend the following best practices to get the best performance when using TanStack on Sevalla:
  • Use TanStack Router loaders for efficient server-side data fetching.
  • Apply proper Cache-Control headers on API responses to maximize CDN performance.
  • Utilize TanStack Router’s prefetching to enhance client navigation speed.
  • Use server handlers for API endpoints to reduce latency and improve SSR performance.
  • Enable Nitro compression in production for smaller payloads.
  • Reduce bundle size with code splitting and lazy loading for route-based components.

Containerization

Dockerfile

The build for Dockerfiles is fully customizable. The following is an example Dockerfile for TanStack:
# Dockerfile
FROM node:lts-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Copy package files
COPY package.json package-lock.json* ./
RUN npm ci

# Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build with Vite/Nitro
RUN npm run build

# Production image
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodejs

# Copy built application
COPY --from=builder --chown=nodejs:nodejs /app/.output ./.output
COPY --from=builder /app/package.json ./package.json

USER nodejs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", ".output/server/index.mjs"]

Nixpacks

Nixpacks automatically uses npm run start as the default start command, so no configuration is required for most TanStack deployments. If you need more control, you can customize the build process by creating a nixpacks.toml file and using Nixpacks-specific environment variables to adjust dependency installation, build steps, and runtime settings. When deploying TanStack, you must also set NIXPACKS_SPA_CADDY=false to ensure that Nixpacks does not apply single-page-app routing behavior.

CDN

Sevalla provides a premium, Cloudflare-powered CDN for Application Hosting at no additional cost. To get the most out of Sevalla’s CDN when deploying your TanStack application, we recommend the following best practices:
  • Enable the CDN for all production applications to ensure global, low-latency delivery.
  • Set appropriate Cache-Control headers on API routes to ensure proper caching behavior.
  • Purge CDN cache after critical deployments to ensure users receive updated content.
  • Rely on Vite’s versioned assets in the dist/ directory, which are safe to cache indefinitely.
  • Store static files in the public/ directory, allowing the CDN to serve them efficiently.
  • Use Nitro’s built-in compression for optimal performance.

Image optimization

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

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

function Home() {
  return (
    <img
      src="/images/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      loading="lazy"
    />
  )
}

Static assets

Place static assets in the public/ directory, Nitro and Vite automatically serve these with optimal caching headers:
public/
├── images/
├── fonts/
└── favicon.ico
Nitro and Vite automatically serve these with optimal caching headers.

Edge caching

Edge caching stores your Sevalla site cache on Cloudflare’s 260+ global data centers, delivering responses from the location nearest to each visitor for faster performance. To maximize the benefits of Sevalla’s Edge Caching for your TanStack application, we recommend the following best practices:
  • Set appropriate Cache-Control headers in loaders to control caching behavior.
  • Combine edge caching with the CDN for a complete caching strategy.
  • Use TanStack Router’s navigation for client-side revalidation.

Cache-Control

With Sevalla’s Cloudflare integration, Cache-Control headers are respected at the edge, giving you precise control over how content is cached and served globally. The following are some common directives you can use in your TanStack application:
  • public, s-maxage=3600 - Caches the response on the CDN for 1 hour, improving performance for frequently accessed content.
  • public, max-age=31536000, immutable - Ideal for versioned static assets, allowing them to be cached for up to 1 year with no revalidation.
  • private - Prevents CDN caching and ensures the response is only cached by the end user’s browser, for personalized or sensitive data.
Sevalla does not yet support the stale-while-revalidate Cache-Control directive. To prevent unexpected caching behavior, we recommend not using this directive in your API or asset caching settings.

Cache-Control headers for API routes

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

async function fetchPublicData() {
  // Your data fetching logic
  return { data: "example" };
}

export const Route = createFileRoute("/api/public-data")({
  server: {
    handlers: {
      GET: async () => {
        const data = await fetchPublicData();

        return new Response(JSON.stringify(data), {
          status: 200,
          headers: {
            "Content-Type": "application/json",
            "Cache-Control": "public, max-age=3600, s-maxage=3600",
          },
        });
      },
    },
  },
});

Loader caching with headers

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

async function fetchProducts() {
  // Your data fetching logic
  return [{ id: 1, name: 'Product 1' }]
}

export const Route = createFileRoute('/products/')({
  loader: async () => {
    const products = await fetchProducts()
    return { products }
  },
  headers: () => ({
    'Cache-Control': 'public, max-age=60, s-maxage=3600',
  }),
  component: Products,
})

function Products() {
  const { products } = Route.useLoaderData()

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

API routes with caching

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

async function fetchProducts() {
  // Your data fetching logic
  return [{ id: 1, name: "Product 1" }];
}

export const Route = createFileRoute("/api/products")({
  server: {
    handlers: {
      GET: async () => {
        const products = await fetchProducts();

        return new Response(JSON.stringify(products), {
          status: 200,
          headers: {
            "Content-Type": "application/json",
            "Cache-Control": "public, s-maxage=3600",
          },
        });
      },
    },
  },
});

Client-side revalidation

// src/routes/blog/$slug.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useEffect } from 'react'

export const Route = createFileRoute('/blog/$slug')({
  component: BlogPost,
})

function BlogPost() {
  const navigate = Route.useNavigate()

  useEffect(() => {
    // Revalidate on focus
    const handleFocus = () => {
      navigate({ to: '.', replace: true })
    }

    window.addEventListener("focus", handleFocus)
    return () => window.removeEventListener("focus", handleFocus)
  }, [navigate])

  return <article>{/* ... */}</article>
}

Health checks

Ensure your application remains available during deployments by implementing health checks:
  • Always implement health checks for production applications.
  • Keep checks lightweight; responses should complete in under 1 second.
  • Verify critical dependencies (e.g., databases, Redis) as part of the checks.
  • Return 200 for degraded states to allow deployments to continue smoothly.
  • Return 503 only for critical failures that require pod restarts.
  • Close connections cleanly (e.g., database pools, Redis) to prevent resource leaks.
  • Test deployments in staging with monitoring scripts to validate health checks.

Basic health check

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

export const Route = createFileRoute("/api/health")({
  server: {
    handlers: {
      GET: async () => {
        return new Response(
          JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }),
          {
            status: 200,
            headers: { "Content-Type": "application/json" },
          }
        );
      },
    },
  },
});

Graceful shutdown

TanStack Start with Nitro provides automatic graceful shutdown, but you can add custom cleanup logic, such as closing database connections, by handling SIGTERM and SIGINT yourself. The example below demonstrates how to shut down a PostgreSQL connection pool during termination:
// src/lib/db.server.ts
import { Pool } from "pg";

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
});

// Graceful pool shutdown
const gracefulPoolShutdown = async () => {
  console.log("Closing database pool...");
  await pool.end();
  console.log("Database pool closed");
};

process.on("SIGTERM", async () => {
  await gracefulPoolShutdown();
  process.exit(0);
});

process.on("SIGINT", async () => {
  await gracefulPoolShutdown();
  process.exit(0);
});
This pattern ensures active connections are closed cleanly before your application stops, preventing dropped queries or pool corruption.

Multi-tenancy

Sevalla fully supports multi-tenancy. You can build multi-tenant TanStack applications using wildcard domains, allowing you to efficiently and securely serve multiple tenants from separate subdomains. Use the following best practices for multi-tenancy on Sevalla with your TanStack application:
  • Use wildcard domains to serve subdomain-based tenants (e.g., tenant1.app.com).
  • Add custom domains individually for tenants who have their own domains.
  • Cache tenant data to reduce database queries and improve performance.
  • Validate tenant existence before rendering pages to prevent errors or unauthorized access.
  • Isolate tenant data in the database using separate schemas or a tenant_id column.
  • Leverage free SSL certificates, which are automatically provided for both wildcard and custom domains.

TanStack start multi-tenant implementation

Extract tenant from subdomain in loader:
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'

// Tenant data fetching function
async function getTenantData(tenantId: string) {
  // Implement database query or API call
  // Example: return await db.query("SELECT * FROM tenants WHERE subdomain = $1", [tenantId])
  return { id: tenantId, name: `Tenant ${tenantId}` }
}

export const Route = createFileRoute('/')({
  loader: async ({ context, location }) => {
    const url = new URL(location.href)
    const hostname = url.hostname
    const subdomain = hostname.split(".")[0]

    // Skip for main domain
    if (subdomain === "www" || subdomain === "yourdomain") {
      return { tenant: null }
    }

    const tenant = await getTenantData(subdomain)
    return { tenant }
  },
  component: Home,
})

function Home() {
  const { tenant } = Route.useLoaderData()

  return (
    <div>
      <h1>Welcome to {tenant?.name || "Our Platform"}</h1>
    </div>
  )
}
Tenant-specific data:
// src/lib/tenant.ts
import { pool } from "./db";

export async function getTenantData(tenantId: string) {
  const result = await pool.query(
    "SELECT * FROM tenants WHERE subdomain = $1",
    [tenantId]
  );

  return result.rows[0] || null;
}

Custom domains per tenant

Allow tenants to use their own domains (e.g., customdomain.com → tenant data). Map custom domains to tenants:
// src/lib/tenant.ts
export async function getTenantByDomain(hostname: string) {
  // First, check if this is a custom domain
  const customDomain = await pool.query(
    "SELECT tenant_id FROM custom_domains WHERE domain = $1",
    [hostname]
  );

  if (customDomain.rows.length > 0) {
    const tenantId = customDomain.rows[0].tenant_id;
    return await getTenantById(tenantId);
  }

  // Otherwise, extract from subdomain
  const subdomain = hostname.split(".")[0];
  return await getTenantData(subdomain);
}