From 80ab6ab2fc94f62e6f603eeb7263c41af289bf2e Mon Sep 17 00:00:00 2001 From: Harsh_G Date: Sun, 1 Mar 2026 18:52:23 +0530 Subject: [PATCH 1/7] feat: add multi-tenancy tutorial for TanStack Start --- docs/start/config.json | 4 + .../react/tutorial/multi-tenant-setup.md | 142 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 docs/start/framework/react/tutorial/multi-tenant-setup.md diff --git a/docs/start/config.json b/docs/start/config.json index 180d38ed44d..51f1a171d6f 100644 --- a/docs/start/config.json +++ b/docs/start/config.json @@ -420,6 +420,10 @@ { "label": "Fetching Data from External API", "to": "framework/react/tutorial/fetching-external-api" + }, + { + "label": "Multi-Tenancy in TanStack Start: A Simple Guide", + "to": "framework/react/tutorial/multi-tenant-setup" } ] } diff --git a/docs/start/framework/react/tutorial/multi-tenant-setup.md b/docs/start/framework/react/tutorial/multi-tenant-setup.md new file mode 100644 index 00000000000..d29b3bec0ba --- /dev/null +++ b/docs/start/framework/react/tutorial/multi-tenant-setup.md @@ -0,0 +1,142 @@ +--- +title: "Multi-Tenancy in TanStack Start: A Simple Guide" +description: "Learn how to structure a multi-tenant app using TanStack Start with React." +--- +**Full Source Code** [View the complete repo on GitHub](https://www.google.com/search?q=https://github.com/harshG775/tanstack-start-multi-tenant-example) + + +# 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. + +--- + +## 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. + +```ts +// 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 +} + +``` + +## 2. Identify the Tenant (Server Function) + +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. + +```ts +// 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 +}) + +``` + +## 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 ``. + +```tsx +// 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 } + } + }, + // ... +}) + +``` + +## 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 `` + +```tsx +// 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" }, + ], + } + }, +}) + +``` + +### Using Tenant Data in Components + +Access the data anywhere using `useLoaderData` from the root. + +```tsx +// 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

404: Tenant Not Found

+ + return ( +
+ Logo +

Welcome to {tenantConfig.meta.name}

+
+ ) +} + +``` + +--- + +## 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. + +--- \ No newline at end of file From 96a3e2bddb57d2ed32ceec5a527a71b46ffbc564 Mon Sep 17 00:00:00 2001 From: Harsh_G Date: Tue, 3 Mar 2026 21:23:18 +0530 Subject: [PATCH 2/7] feat: update multi-tenancy tutorial to focus on hostname-based resolution and enhance clarity --- .../react/tutorial/multi-tenant-setup.md | 170 +++++++++++------- 1 file changed, 101 insertions(+), 69 deletions(-) diff --git a/docs/start/framework/react/tutorial/multi-tenant-setup.md b/docs/start/framework/react/tutorial/multi-tenant-setup.md index d29b3bec0ba..8ff569fa9be 100644 --- a/docs/start/framework/react/tutorial/multi-tenant-setup.md +++ b/docs/start/framework/react/tutorial/multi-tenant-setup.md @@ -1,35 +1,66 @@ --- -title: "Multi-Tenancy in TanStack Start: A Simple Guide" -description: "Learn how to structure a multi-tenant app using TanStack Start with React." + +title: "Hostname-Based Multi-Tenancy" +description: "Learn how to structure a hostname-based multi-tenant app using TanStack Start with React." +-------------------------------------------------------------------------------------------------------- + +> This tutorial assumes TanStack Start v1.132+. + +Multi-tenant applications often need to resolve a tenant from the incoming hostname or subdomain during SSR (Server-Side Rendering). This guide demonstrates how to implement hostname-based multi-tenancy using TanStack Start. + +--- + +## Architecture Overview + +This guide demonstrates: + +* Hostname-based tenant resolution +* SSR tenant detection +* Root-level data hydration +* Dynamic metadata handling + +Request lifecycle: + +``` +Request + → Nitro Runtime + → getRequestUrl() + → normalizeHostname() + → getTenantConfigByHostname() + → Root Loader + → Hydrated Application +``` + --- -**Full Source Code** [View the complete repo on GitHub](https://www.google.com/search?q=https://github.com/harshG775/tanstack-start-multi-tenant-example) +## Full Source Code -# Multi-Tenancy in TanStack Start: Subdomain & Hostname Routing +View the complete repository 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. +[https://github.com/harshG775/tanstack-start-multi-tenant-example](https://github.com/harshG775/tanstack-start-multi-tenant-example) --- ## 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. +In production, you may have `tenant.com` or `user.saas.com`. In development, you typically use `localhost:3000`. This utility keeps behavior consistent across environments. ```ts // 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 + // Handle local development subdomains like tenant.localhost:3000 + if (hostname.includes("localhost")) { + return hostname.replace(".localhost", "").split(":")[0] + } + return hostname } - ``` +--- + ## 2. Identify the Tenant (Server Function) -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. +Use `createServerOnlyFn` to ensure tenant resolution never reaches the client. `getRequestUrl()` retrieves the incoming request during SSR. ```ts // serverFn/tenant.serverFn.ts @@ -39,104 +70,105 @@ 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 url = getRequestUrl() + const hostname = normalizeHostname(url.hostname) - const tenantConfig = await getTenantConfigByHostname({ 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 }) - } + if (!tenantConfig) { + throw new Response("Tenant Not Found", { status: 404 }) + } - return tenantConfig + return tenantConfig }) - ``` +--- + ## 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 ``. +Resolve tenant data in the `__root__` route so it is available to all child routes and the HTML ``. ```tsx // routes/__root.tsx +import { createRootRoute } from "@tanstack/react-router" 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 } - } - }, - // ... + loader: async () => { + try { + const tenantConfig = await getTenantConfig() + return { tenantConfig } + } catch { + return { tenantConfig: null } + } + }, }) - ``` -## 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. +## 4. Dynamic Metadata -### Updating the `` +Tenant data can be used to dynamically update metadata for SEO. ```tsx // 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" }, - ], - } - }, + 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" }, + ], + } + }, }) - ``` -### Using Tenant Data in Components +--- + +## 5. Using Tenant Data in Components -Access the data anywhere using `useLoaderData` from the root. +Access root loader data using `useLoaderData`. ```tsx // routes/index.tsx import { createFileRoute, useLoaderData } from "@tanstack/react-router" export const Route = createFileRoute("/")({ - component: HomePage, + component: HomePage, }) function HomePage() { - const { tenantConfig } = useLoaderData({ from: "__root__" }) + const { tenantConfig } = useLoaderData({ from: "__root__" }) - if (!tenantConfig) return

404: Tenant Not Found

+ if (!tenantConfig) return

404: Tenant Not Found

- return ( -
- Logo -

Welcome to {tenantConfig.meta.name}

-
- ) + return ( +
+ Logo +

Welcome to {tenantConfig.meta.name}

+
+ ) } - ``` --- -## Pro-Tips for Multi-Tenancy +## Production Considerations + +* **Caching:** Cache `getTenantConfigByHostname` (e.g., Redis or in-memory cache) to avoid repeated database lookups. +* **Validation:** Ensure tenants are active and not suspended before returning configuration. +* **Assets:** Use absolute URLs or correctly prefixed CDN paths for cross-domain asset loading. +* **Security:** Avoid exposing internal tenant configuration fields to the client. -* **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. +--- ---- \ No newline at end of file +This approach provides a clean, SSR-first architecture for hostname-based multi-tenancy in TanStack Start while keeping sensitive logic server-only. From e79b14113603d6a4dc7f11b978a0df0350c7112f Mon Sep 17 00:00:00 2001 From: Harsh_G Date: Tue, 3 Mar 2026 21:39:00 +0530 Subject: [PATCH 3/7] docs: fix formatting and syntax in multi-tenant tutorial - Standardize frontmatter quotes and formatting - Update code block language and arrow syntax - Convert list markers to hyphens for consistency - Apply single quotes in code examples for uniformity --- .../react/tutorial/multi-tenant-setup.md | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/docs/start/framework/react/tutorial/multi-tenant-setup.md b/docs/start/framework/react/tutorial/multi-tenant-setup.md index 8ff569fa9be..6ceae6f2f87 100644 --- a/docs/start/framework/react/tutorial/multi-tenant-setup.md +++ b/docs/start/framework/react/tutorial/multi-tenant-setup.md @@ -1,8 +1,7 @@ --- - -title: "Hostname-Based Multi-Tenancy" -description: "Learn how to structure a hostname-based multi-tenant app using TanStack Start with React." --------------------------------------------------------------------------------------------------------- +title: 'Hostname-Based Multi-Tenancy' +description: 'Learn how to structure a hostname-based multi-tenant app using TanStack Start with React.' +--- > This tutorial assumes TanStack Start v1.132+. @@ -14,21 +13,21 @@ Multi-tenant applications often need to resolve a tenant from the incoming hostn This guide demonstrates: -* Hostname-based tenant resolution -* SSR tenant detection -* Root-level data hydration -* Dynamic metadata handling +- Hostname-based tenant resolution +- SSR tenant detection +- Root-level data hydration +- Dynamic metadata handling Request lifecycle: -``` +```txt Request - → Nitro Runtime - → getRequestUrl() - → normalizeHostname() - → getTenantConfigByHostname() - → Root Loader - → Hydrated Application + -> Nitro Runtime + -> getRequestUrl() + -> normalizeHostname() + -> getTenantConfigByHostname() + -> Root Loader + -> Hydrated Application ``` --- @@ -49,8 +48,8 @@ In production, you may have `tenant.com` or `user.saas.com`. In development, you // 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] + if (hostname.includes('localhost')) { + return hostname.replace('.localhost', '').split(':')[0] } return hostname } @@ -64,10 +63,10 @@ Use `createServerOnlyFn` to ensure tenant resolution never reaches the client. ` ```ts // 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" +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() @@ -76,7 +75,7 @@ export const getTenantConfig = createServerOnlyFn(async () => { const tenantConfig = await getTenantConfigByHostname({ hostname }) if (!tenantConfig) { - throw new Response("Tenant Not Found", { status: 404 }) + throw new Response('Tenant Not Found', { status: 404 }) } return tenantConfig @@ -91,8 +90,8 @@ Resolve tenant data in the `__root__` route so it is available to all child rout ```tsx // routes/__root.tsx -import { createRootRoute } from "@tanstack/react-router" -import { getTenantConfig } from "#/serverFn/tenant.serverFn" +import { createRootRoute } from '@tanstack/react-router' +import { getTenantConfig } from '#/serverFn/tenant.serverFn' export const Route = createRootRoute({ loader: async () => { @@ -120,13 +119,11 @@ export const Route = createRootRoute({ 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" }, + { 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' }], } }, }) @@ -140,14 +137,14 @@ Access root loader data using `useLoaderData`. ```tsx // routes/index.tsx -import { createFileRoute, useLoaderData } from "@tanstack/react-router" +import { createFileRoute, useLoaderData } from '@tanstack/react-router' -export const Route = createFileRoute("/")({ +export const Route = createFileRoute('/')({ component: HomePage, }) function HomePage() { - const { tenantConfig } = useLoaderData({ from: "__root__" }) + const { tenantConfig } = useLoaderData({ from: '__root__' }) if (!tenantConfig) return

404: Tenant Not Found

@@ -164,10 +161,10 @@ function HomePage() { ## Production Considerations -* **Caching:** Cache `getTenantConfigByHostname` (e.g., Redis or in-memory cache) to avoid repeated database lookups. -* **Validation:** Ensure tenants are active and not suspended before returning configuration. -* **Assets:** Use absolute URLs or correctly prefixed CDN paths for cross-domain asset loading. -* **Security:** Avoid exposing internal tenant configuration fields to the client. +- **Caching:** Cache `getTenantConfigByHostname` (e.g., Redis or in-memory cache) to avoid repeated database lookups. +- **Validation:** Ensure tenants are active and not suspended before returning configuration. +- **Assets:** Use absolute URLs or correctly prefixed CDN paths for cross-domain asset loading. +- **Security:** Avoid exposing internal tenant configuration fields to the client. --- From ca94a81be30b13854f346a39c19a971b7b16c235 Mon Sep 17 00:00:00 2001 From: Harsh_G Date: Tue, 3 Mar 2026 21:42:46 +0530 Subject: [PATCH 4/7] pr-fix(docs): improve error handling in multi-tenant tutorial --- docs/start/framework/react/tutorial/multi-tenant-setup.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/start/framework/react/tutorial/multi-tenant-setup.md b/docs/start/framework/react/tutorial/multi-tenant-setup.md index 6ceae6f2f87..506b1884dba 100644 --- a/docs/start/framework/react/tutorial/multi-tenant-setup.md +++ b/docs/start/framework/react/tutorial/multi-tenant-setup.md @@ -99,7 +99,10 @@ export const Route = createRootRoute({ const tenantConfig = await getTenantConfig() return { tenantConfig } } catch { - return { tenantConfig: null } + if (error instanceof Response && error.status === 404) { + return { tenantConfig: null } + } + throw error } }, }) From c65718f72127f0f2877551c422db067b609d8517 Mon Sep 17 00:00:00 2001 From: Harsh Gaur Date: Thu, 5 Mar 2026 10:47:27 +0530 Subject: [PATCH 5/7] Improve error handling in tenant config loader Add error handling to catch specific errors when loading tenant configuration. --- docs/start/framework/react/tutorial/multi-tenant-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/start/framework/react/tutorial/multi-tenant-setup.md b/docs/start/framework/react/tutorial/multi-tenant-setup.md index 506b1884dba..fc83fb83f47 100644 --- a/docs/start/framework/react/tutorial/multi-tenant-setup.md +++ b/docs/start/framework/react/tutorial/multi-tenant-setup.md @@ -98,7 +98,7 @@ export const Route = createRootRoute({ try { const tenantConfig = await getTenantConfig() return { tenantConfig } - } catch { + } catch(error) { if (error instanceof Response && error.status === 404) { return { tenantConfig: null } } From 92da3a684ae351a65e7ed9f9e9427b90f0dfe3a2 Mon Sep 17 00:00:00 2001 From: Harsh Gaur Date: Thu, 5 Mar 2026 16:26:23 +0530 Subject: [PATCH 6/7] fix(tenant.serverFn): changing serverOnlyFn to serverFn --- docs/start/framework/react/tutorial/multi-tenant-setup.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/start/framework/react/tutorial/multi-tenant-setup.md b/docs/start/framework/react/tutorial/multi-tenant-setup.md index fc83fb83f47..62734fa39be 100644 --- a/docs/start/framework/react/tutorial/multi-tenant-setup.md +++ b/docs/start/framework/react/tutorial/multi-tenant-setup.md @@ -59,16 +59,16 @@ export const normalizeHostname = (hostname: string): string => { ## 2. Identify the Tenant (Server Function) -Use `createServerOnlyFn` to ensure tenant resolution never reaches the client. `getRequestUrl()` retrieves the incoming request during SSR. +Use `createServerFn` to ensure tenant resolution never reaches the client. `getRequestUrl()` retrieves the incoming request during SSR. ```ts // serverFn/tenant.serverFn.ts import { getTenantConfigByHostname } from '#/lib/api' import { normalizeHostname } from '#/lib/normalizeHostname' -import { createServerOnlyFn } from '@tanstack/react-start' +import { createServerFn } from '@tanstack/react-start' import { getRequestUrl } from '@tanstack/react-start/server' -export const getTenantConfig = createServerOnlyFn(async () => { +export const getTenantConfig = createServerFn().handler(async () => { const url = getRequestUrl() const hostname = normalizeHostname(url.hostname) From 29089e4da2b754f747d94d67426d483f4f0894b7 Mon Sep 17 00:00:00 2001 From: Harsh_G Date: Sat, 7 Mar 2026 12:26:40 +0530 Subject: [PATCH 7/7] docs: enhance multi-tenant tutorial with detailed steps and examples for hostname-based resolution --- .../react/tutorial/multi-tenant-setup.md | 306 +++++++++++++----- 1 file changed, 227 insertions(+), 79 deletions(-) diff --git a/docs/start/framework/react/tutorial/multi-tenant-setup.md b/docs/start/framework/react/tutorial/multi-tenant-setup.md index 62734fa39be..06f34176402 100644 --- a/docs/start/framework/react/tutorial/multi-tenant-setup.md +++ b/docs/start/framework/react/tutorial/multi-tenant-setup.md @@ -3,66 +3,178 @@ title: 'Hostname-Based Multi-Tenancy' description: 'Learn how to structure a hostname-based multi-tenant app using TanStack Start with React.' --- -> This tutorial assumes TanStack Start v1.132+. +> This tutorial assumes @tanstack/react-router: v1.132+. -Multi-tenant applications often need to resolve a tenant from the incoming hostname or subdomain during SSR (Server-Side Rendering). This guide demonstrates how to implement hostname-based multi-tenancy using TanStack Start. +# Multi-Tenant Applications + +In many SaaS applications, a **single codebase serves multiple tenants**. Each tenant may have its own branding, metadata, and configuration. + +In this tutorial, we will build a **hostname-based multi-tenant application** using **TanStack Start** and **TanStack Router**. + +The goal is to identify tenants using the incoming request hostname and provide tenant configuration to the application during **SSR**. + +The complete code for this tutorial is available on [https://github.com/harshG775/tanstack-start-multi-tenant-example](https://github.com/harshG775/tanstack-start-multi-tenant-example). --- -## Architecture Overview +# What We'll Build + +Two tenants running from the same application: + +```text +tenant-1.com → Tenant One branding +tenant-2.com → Tenant Two branding +``` + +Tenant 1 with custom branding and logo. + +![Tenant 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ijod78slb3ceq7rmokwd.png) -This guide demonstrates: +Tenant 2 with custom branding and logo. -- Hostname-based tenant resolution -- SSR tenant detection -- Root-level data hydration -- Dynamic metadata handling +![Tenant 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/diuxzwj9rx6twd3c6oyw.png) -Request lifecycle: +Each tenant will have: -```txt +- custom name +- description +- logo +- favicon + +All resolved automatically during the **request lifecycle**. + +--- + +# Architecture Overview + +Tenant resolution happens during SSR before the router renders. + +```text Request - -> Nitro Runtime - -> getRequestUrl() - -> normalizeHostname() - -> getTenantConfigByHostname() - -> Root Loader - -> Hydrated Application + ↓ +Nitro Runtime + ↓ +getRequestUrl() + ↓ +normalizeHostname() + ↓ +getTenantConfigByHostname() + ↓ +Router Context + ↓ +Hydrated Application +``` + +The tenant configuration is loaded once and injected into the **router context**. + +--- + +# Project Structure + +```text +src +├─ functions +│ └─ tenant.serverFn.ts +├─ lib +│ ├─ api.ts +│ └─ normalizeHostname.ts +├─ routes +│ ├─ __root.tsx +│ └─ index.tsx +└─ router.tsx ``` --- -## Full Source Code +# Step 1: Create a Tenant Data Source + +First we create a simple tenant lookup function. -View the complete repository on GitHub: +`src/lib/api.ts` + +```ts +export type TenantType = { + id: string + hostname: string + meta: { + name: string + description: string + logo: string + favicon: string + } +} + +const tenantsDB = [ + { + id: 'tenant-1', + hostname: 'tenant-1.com', + meta: { + name: 'Tenant One', + description: 'Tenant One is a modern SaaS platform.', + logo: 'https://picsum.photos/seed/tenant1/200/200', + favicon: 'https://picsum.photos/seed/tenant1/32/32', + }, + }, + { + id: 'tenant-2', + hostname: 'tenant-2.com', + meta: { + name: 'Tenant Two', + description: 'Tenant Two helps businesses scale fast.', + logo: 'https://picsum.photos/seed/tenant2/200/200', + favicon: 'https://picsum.photos/seed/tenant2/32/32', + }, + }, +] + +export const getTenantConfigByHostname = ({ + hostname, +}: { + hostname: string +}) => { + return tenantsDB.find((tenant) => tenant.hostname === hostname) ?? null +} +``` -[https://github.com/harshG775/tanstack-start-multi-tenant-example](https://github.com/harshG775/tanstack-start-multi-tenant-example) +In production this would typically query a **database**. --- -## 1. Normalize the Hostname +# Step 2: Normalize the Hostname -In production, you may have `tenant.com` or `user.saas.com`. In development, you typically use `localhost:3000`. This utility keeps behavior consistent across environments. +During development the hostname might look like: + +```text +tenant-1.com.localhost:3000 +``` + +We normalize it before resolving the tenant. + +`src/lib/normalizeHostname.ts` ```ts -// lib/normalizeHostname.ts export const normalizeHostname = (hostname: string): string => { - // Handle local development subdomains like tenant.localhost:3000 + let finalHostname = hostname + if (hostname.includes('localhost')) { - return hostname.replace('.localhost', '').split(':')[0] + const cleaned = hostname.replace('.localhost', '').replace(':3000', '') + + finalHostname = cleaned } - return hostname + + return finalHostname } ``` --- -## 2. Identify the Tenant (Server Function) +# Step 3: Create a Server Function + +We resolve the tenant during SSR using a server function. -Use `createServerFn` to ensure tenant resolution never reaches the client. `getRequestUrl()` retrieves the incoming request during SSR. +`src/functions/tenant.serverFn.ts` ```ts -// serverFn/tenant.serverFn.ts import { getTenantConfigByHostname } from '#/lib/api' import { normalizeHostname } from '#/lib/normalizeHostname' import { createServerFn } from '@tanstack/react-start' @@ -70,9 +182,10 @@ 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 }) + const tenantConfig = getTenantConfigByHostname({ hostname }) if (!tenantConfig) { throw new Response('Tenant Not Found', { status: 404 }) @@ -84,77 +197,97 @@ export const getTenantConfig = createServerFn().handler(async () => { --- -## 3. Register in the Root Loader +# Step 4: Inject Tenant Into Router Context -Resolve tenant data in the `__root__` route so it is available to all child routes and the HTML ``. +Next we load the tenant configuration when the router is created. -```tsx -// routes/__root.tsx -import { createRootRoute } from '@tanstack/react-router' -import { getTenantConfig } from '#/serverFn/tenant.serverFn' - -export const Route = createRootRoute({ - loader: async () => { - try { - const tenantConfig = await getTenantConfig() - return { tenantConfig } - } catch(error) { - if (error instanceof Response && error.status === 404) { - return { tenantConfig: null } - } - throw error - } - }, -}) +`src/router.tsx` + +```ts +import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' +import { getTenantConfig } from './functions/tenant.serverFn' + +export async function getRouter() { + const tenantConfig = await getTenantConfig() // <-- + + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + + context: { + tenantConfig, // <-- + }, + }) + + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} ``` +Now the tenant configuration is available throughout the application. + --- -## 4. Dynamic Metadata +# Step 5: Apply Tenant Branding in the Root Route -Tenant data can be used to dynamically update metadata for SEO. +The root route can now dynamically set metadata and assets. + +`src/routes/__root.tsx` ```tsx -// 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' }], - } +import type { TenantType } from '#/lib/api' +export const Route = createRootRouteWithContext<{ tenantConfig: TenantType }>()( + { + head: ({ match }) => { + const tenant = match.context.tenantConfig // <-- + + return { + meta: [ + { title: tenant.meta.name }, + { name: 'description', content: tenant.meta.description }, + ], + links: [{ rel: 'icon', href: tenant.meta.favicon }], + } + }, }, -}) +) ``` --- -## 5. Using Tenant Data in Components +# Step 6: Access Tenant Data in Routes -Access root loader data using `useLoaderData`. +Tenant data can be accessed using `useRouteContext`. + +`src/routes/index.tsx` ```tsx -// routes/index.tsx -import { createFileRoute, useLoaderData } from '@tanstack/react-router' +import { createFileRoute, useRouteContext } from '@tanstack/react-router' export const Route = createFileRoute('/')({ component: HomePage, }) function HomePage() { - const { tenantConfig } = useLoaderData({ from: '__root__' }) - - if (!tenantConfig) return

404: Tenant Not Found

+ const { tenantConfig } = useRouteContext({ from: '__root__' }) // <-- return ( -
- Logo -

Welcome to {tenantConfig.meta.name}

+
+ {tenantConfig.meta.name} +

{tenantConfig.meta.name}

+

{tenantConfig.meta.description}

) } @@ -162,13 +295,28 @@ function HomePage() { --- +# Result + +The same application now serves different tenants depending on the hostname: + +``` +tenant-1.com → Tenant One +tenant-2.com → Tenant Two +``` + +Each tenant receives its own: + +- metadata +- branding +- configuration + +All resolved during **server-side rendering**. + +--- + ## Production Considerations - **Caching:** Cache `getTenantConfigByHostname` (e.g., Redis or in-memory cache) to avoid repeated database lookups. - **Validation:** Ensure tenants are active and not suspended before returning configuration. - **Assets:** Use absolute URLs or correctly prefixed CDN paths for cross-domain asset loading. - **Security:** Avoid exposing internal tenant configuration fields to the client. - ---- - -This approach provides a clean, SSR-first architecture for hostname-based multi-tenancy in TanStack Start while keeping sensitive logic server-only.