DEV Community

Cover image for Multi-Tenancy in TanStack Start: A Simple Guide
harshG775
harshG775

Posted on • Edited on

Multi-Tenancy in TanStack Start: A Simple Guide

Full Source Code View the complete repo on GitHub

Multi-Tenancy in TanStack Start: Subdomain & Hostname Routing

Building a SaaS usually requires identifying a tenant by their subdomain or hostname. Because TanStack Start is built on top of Nitro and Vinxi, we have powerful server-side utilities to handle this during the SSR (Server-Side Rendering) phase.

Here is the goal: Two subdomains, one codebase, completely different branding.

Tenant 1 with custom branding and logo.

Tenant 1

Tenant 2 with custom branding and logo.
Tenant 1


1. Normalize the Hostname

In production, you'll have tenant.com or user.saas.com. In development, you likely have localhost:3000. This utility ensures your logic stays consistent across environments.

// lib/normalizeHostname.ts
export const normalizeHostname = (hostname: string): string => {
    // Handle local development subdomains like tenant.localhost:3000
    if (hostname.includes("localhost")) {
        return hostname.replace(".localhost", "").split(":")[0];
    }
    return hostname;
};
Enter fullscreen mode Exit fullscreen mode

2. Identify the Tenant (Server Function)

We use createServerFn to ensure our tenant lookup—which might involve a database call or a secret API key—never leaks to the client. We use getRequestUrl() from the Start server utilities to grab the incoming URL.

// serverFn/tenant.serverFn.ts
import { getTenantConfigByHostname } from "#/lib/api";
import { normalizeHostname } from "#/lib/normalizeHostname";
import { createServerFn } from "@tanstack/react-start";
import { getRequestUrl } from "@tanstack/react-start/server";

export const getTenantConfig = createServerFn().handler(async () => {
    const url = getRequestUrl();
    const hostname = normalizeHostname(url.hostname);

    const tenantConfig = await getTenantConfigByHostname({ hostname });

    if (!tenantConfig) {
        // You can throw a 404 here, or return null to handle it in the UI
        throw new Response("Tenant Not Found", { status: 404 });
    }

    return tenantConfig;
});
Enter fullscreen mode Exit fullscreen mode

3. Register in the Root Loader

The best place to fetch tenant data is the __root__ route. This ensures the data is resolved once at the top level and is available to every child route and the HTML <head>.

// routes/__root.tsx
import { getTenantConfig } from "#/serverFn/tenant.serverFn";

export const Route = createRootRoute({
    staleTime: Infinity ,
    loader: async () => {
        try {
            const tenantConfig = await getTenantConfig();
            return { tenantConfig };
        } catch (error) {
            if (error instanceof Response && error.status === 404) {
                return { tenantConfig: null };
            }
            throw error;
        }
    },
    // ...
});
Enter fullscreen mode Exit fullscreen mode
  • add staleTime: Infinity, //to stop fetching the getTenantConfig again on client side navigation

4. Dynamic Metadata & UI

One of the biggest benefits of this approach is SEO. You can dynamically update the page title, favicon, and Open Graph tags based on the tenant.

Updating the <head>

// routes/__root.tsx
export const Route = createRootRoute({
    head: (ctx) => {
        const tenant = ctx.loaderData?.tenantConfig;

        return {
            meta: [
                { title: "tenant?.meta.name ?? \"Default App\" },"
                { name: "description", content: tenant?.meta.description },
                { property: "og:image", content: tenant?.meta.logo },
            ],
            links: [{ rel: "icon", href: tenant?.meta.favicon ?? "/favicon.ico" }],
        };
    },
});
Enter fullscreen mode Exit fullscreen mode

Using Tenant Data in Components

Access the data anywhere using useLoaderData from the root.

// routes/index.tsx
import { createFileRoute, useLoaderData } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
    component: HomePage,
});

function HomePage() {
    const { tenantConfig } = useLoaderData({ from: "__root__" });

    if (!tenantConfig) return <h1>404: Tenant Not Found</h1>;

    return (
        <main className="p-6">
            <img src={tenantConfig.meta.logo} alt="Logo" width={100} />
            <h1>Welcome to {tenantConfig.meta.name}</h1>
        </main>
    );
}
Enter fullscreen mode Exit fullscreen mode

Pro-Tips for Multi-Tenancy

  • Caching: Wrap your getTenantConfigByHostname in a cache (like React.cache or a Redis layer) to avoid hitting your database on every single page load.
  • Security: Always validate that the identified tenant is active and not suspended before returning the config.
  • Assets: If you use a CDN, ensure your image paths are absolute or prefixed correctly to avoid cross-domain loading issues.

Top comments (0)