Tools: Multi-Tenancy in TanStack Start: A Simple Guide

Tools: Multi-Tenancy in TanStack Start: A Simple Guide

Source: Dev.to

Multi-Tenancy in TanStack Start: Subdomain & Hostname Routing ## 1. Normalize the Hostname ## 2. Identify the Tenant (Server Function) ## 3. Register in the Root Loader ## 4. Dynamic Metadata & UI ## Updating the <head> ## Using Tenant Data in Components ## Pro-Tips for Multi-Tenancy Full Source Code View the complete repo on GitHub 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 2 with custom branding and logo. 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. We use createServerOnlyFn 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. 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>. 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. Access the data anywhere using useLoaderData from the root. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: // 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 COMMAND_BLOCK: // 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 } COMMAND_BLOCK: // 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 } COMMAND_BLOCK: // serverFn/tenant.serverFn.ts import { getTenantConfigByHostname } from "#/lib/api" import { normalizeHostname } from "#/lib/normalizeHostname" import { createServerOnlyFn } from "@tanstack/react-start" import { getRequestUrl } from "@tanstack/react-start/server" export const getTenantConfig = createServerOnlyFn(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 COMMAND_BLOCK: // serverFn/tenant.serverFn.ts import { getTenantConfigByHostname } from "#/lib/api" import { normalizeHostname } from "#/lib/normalizeHostname" import { createServerOnlyFn } from "@tanstack/react-start" import { getRequestUrl } from "@tanstack/react-start/server" export const getTenantConfig = createServerOnlyFn(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 }) COMMAND_BLOCK: // serverFn/tenant.serverFn.ts import { getTenantConfigByHostname } from "#/lib/api" import { normalizeHostname } from "#/lib/normalizeHostname" import { createServerOnlyFn } from "@tanstack/react-start" import { getRequestUrl } from "@tanstack/react-start/server" export const getTenantConfig = createServerOnlyFn(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 }) COMMAND_BLOCK: // routes/__root.tsx import { getTenantConfig } from "#/serverFn/tenant.serverFn" export const Route = createRootRoute({ loader: async () => { try { const tenantConfig = await getTenantConfig() return { tenantConfig } } catch (error) { // Handle cases where the tenant doesn't exist return { tenantConfig: null } } }, // ... }) Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // routes/__root.tsx import { getTenantConfig } from "#/serverFn/tenant.serverFn" export const Route = createRootRoute({ loader: async () => { try { const tenantConfig = await getTenantConfig() return { tenantConfig } } catch (error) { // Handle cases where the tenant doesn't exist return { tenantConfig: null } } }, // ... }) COMMAND_BLOCK: // routes/__root.tsx import { getTenantConfig } from "#/serverFn/tenant.serverFn" export const Route = createRootRoute({ loader: async () => { try { const tenantConfig = await getTenantConfig() return { tenantConfig } } catch (error) { // Handle cases where the tenant doesn't exist return { tenantConfig: null } } }, // ... }) COMMAND_BLOCK: // 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 COMMAND_BLOCK: // 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" }, ], } }, }) COMMAND_BLOCK: // 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" }, ], } }, }) CODE_BLOCK: // 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 CODE_BLOCK: // 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> ) } CODE_BLOCK: // 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> ) } - 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.