Refine's architecture allows you to customize your app's data providers, access control and routing to support multi tenant features easily. This guide will provide you with a high level overview of the concepts and how to implement them. To see multi tenant app examples, check out the Examples section.
Multitenancy refers to a kind of architecture where a single instance of software runs on a server and serves multiple customers. In a multi-tenant environment, separate customers tap into the same hardware and data storage, creating a dedicated instance for each customer. Each tenant’s data is isolated and remains invisible to others, but is running on the same server.
While there are many ways to implement multi tenant features, we'll implement a route based approach in the following sections. While your m implementation may differ, the concepts will be similar and the approach will be tweakable to your needs.
We'll be using routes to determine which tenant is being accessed. To do this, we'll need to configure our routes to include the tenant information. For example, a products resource will have the route definition for list as /:tenantId/products.
In the examples below, we are only showing the route definitions. You may need additional code to implement styling and layout depending on your choice of UI library. Regardless of the UI library you choose, the routing implementation will be similar to the examples above.
React Router Dom
Next.js
Remix
import{Refine}from"@refinedev/core";importdataProviderfrom"@refinedev/simple-rest";importrouterProviderfrom"@refinedev/react-router-v6";import{BrowserRouter,Outlet,Routes,Route}from"react-router-dom";import{ProductsList,ProductsCreate,ProductsShow,ProductsEdit}from"./products";exportconstApp: React.FC = ()=>{return(<BrowserRouter><RefinedataProvider={dataProvider("<API_URL>")}routerProvider={routerProvider}resources={[{name:"products",// We're prefixing the routes with `/:tenantId` to make them tenant-aware.list:"/:tenantId/products",show:"/:tenantId/products/:id",edit:"/:tenantId/products/:id/edit",create:"/:tenantId/products/create",},]}><Routes>{/* We're defining the `tenantId` as a route parameter. */}
<Routepath="/:tenantId"element={<Outlet/>}><Routepath="products"element={<ProductsList/>}/><Routepath="products/create"element={<ProductsCreate/>}/><Routepath="products/:id"element={<ProductsShow/>}/><Routepath="products/:id/edit"element={<ProductsEdit/>}/></Route></Routes></Refine></BrowserRouter>);};
Dependencies: @refinedev/core@latest
Code Files
File: App.tsx
Content: import { Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";
import routerProvider from "@refinedev/react-router-v6";
import { BrowserRouter, Outlet, Routes, Route } from "react-router-dom";
import { ProductsList, ProductsCreate, ProductsShow, ProductsEdit } from "./products";
export const App: React.FC = () => {
return (
<BrowserRouter>
<Refine
dataProvider={dataProvider("<API_URL>")}
routerProvider={routerProvider}
resources={[
{
name: "products",
// We're prefixing the routes with `/:tenantId` to make them tenant-aware.
list: "/:tenantId/products",
show: "/:tenantId/products/:id",
edit: "/:tenantId/products/:id/edit",
create: "/:tenantId/products/create",
},
]}
>
<Routes>
{/* We're defining the `tenantId` as a route parameter. */}
<Route path="/:tenantId" element={<Outlet />}>
<Route path="products" element={<ProductsList />} />
<Route path="products/create" element={<ProductsCreate />} />
<Route path="products/:id" element={<ProductsShow />} />
<Route path="products/:id/edit" element={<ProductsEdit />} />
</Route>
</Routes>
</Refine>
</BrowserRouter>
);
};
importReactfrom"react";import{Refine}from"@refinedev/core";importrouterProviderfrom"@refinedev/nextjs-router/pages";importdataProviderfrom"@refinedev/simple-rest";importtype{AppProps}from"next/app";functionApp({Component,pageProps}: AppProps){return(<RefinedataProvider={dataProvider("<API_URL>")}routerProvider={routerProvider}resources={[{name:"products",// We're prefixing the routes with `/:tenantId` to make them tenant-aware.list:"/:tenantId/products",show:"/:tenantId/products/:id",edit:"/:tenantId/products/:id/edit",create:"/:tenantId/products/create",},]}><Component{...pageProps}/></Refine>);}exportdefaultApp;
Dependencies: @refinedev/core@latest
Code Files
File: /pages/_app.tsx
Content: import React from "react";
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/nextjs-router/pages";
import dataProvider from "@refinedev/simple-rest";
import type { AppProps } from "next/app";
function App({ Component, pageProps }: AppProps) {
return (
<Refine
dataProvider={dataProvider("<API_URL>")}
routerProvider={routerProvider}
resources={[
{
name: "products",
// We're prefixing the routes with `/:tenantId` to make them tenant-aware.
list: "/:tenantId/products",
show: "/:tenantId/products/:id",
edit: "/:tenantId/products/:id/edit",
create: "/:tenantId/products/create",
},
]}
>
<Component {...pageProps} />
</Refine>
);
}
export default App;
importReactfrom"react";import{Links,LiveReload,Meta,Outlet,Scripts,ScrollRestoration,}from"@remix-run/react";import{Refine}from"@refinedev/core";importrouterProviderfrom"@refinedev/remix-router";importdataProviderfrom"@refinedev/simple-rest";exportdefaultfunctionApp(){return(<htmllang="en"><head><Meta/><Links/></head><body><RefinedataProvider={dataProvider("<API_URL>")}routerProvider={routerProvider}resources={[{name:"products",// We're prefixing the routes with `/:tenantId` to make them tenant-aware.list:"/:tenantId/products",show:"/:tenantId/products/:id",edit:"/:tenantId/products/:id/edit",create:"/:tenantId/products/create",},]}><Outlet/></Refine><ScrollRestoration/><Scripts/><LiveReload/></body></html>);}
Dependencies: @refinedev/core@latest
Code Files
File: /app/root.tsx
Content: import React from "react";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { Refine } from "@refinedev/core";
import routerProvider from "@refinedev/remix-router";
import dataProvider from "@refinedev/simple-rest";
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Refine
dataProvider={dataProvider("<API_URL>")}
routerProvider={routerProvider}
resources={[
{
name: "products",
// We're prefixing the routes with `/:tenantId` to make them tenant-aware.
list: "/:tenantId/products",
show: "/:tenantId/products/:id",
edit: "/:tenantId/products/:id/edit",
create: "/:tenantId/products/create",
},
]}
>
<Outlet />
</Refine>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
We'll be using the tenantId from the route to determine which tenant is being accessed. Refine will infer the tenantId from the current route and pass it to the data provider in meta. You can access the tenantId from the meta object in your data provider and use it in your API calls.
To customize the data providers, you can override each method in the data provider instance or use the swizzle command to be fully able to customize the data provider for your needs.
An example implementation of a custom getList method is shown below.
import dataProvider from"@refinedev/simple-rest"; constAPI_URL="<API_URL>"; const baseDataProvider =dataProvider(API_URL); const customDataProvider ={ ...baseDataProvider, getList:async({ resource, pagination, filters, sorters, meta })=>{ const{ tenantId }= meta; // We're prefixing the tenantId to the resource name // Your API may have a different way of handling this const response =awaitfetch( `${API_URL}/${tenantId}/${resource}?${stringify({ /* ... */ })}`, ); const data =await response.json(); const total =parseInt(response.headers.get("x-total-count")||"0"); return{ data, total }; }, };
Implementation Tips:
Check out the Examples below to see a full implementation of a data provider for a multi tenant app.
Now we've defined our routes and data providers to use tenantId to determine which tenant is being accessed. We'll need to add a tenant selector to the UI to allow users to switch between tenants.
Implementation Tips:
The implementation of the component may differ depending on your choice of UI library. Regardless of the UI library you choose, the implementation will be similar to the example below.
It's best to place the tenant selector in a layout component that wraps the routes. This way, the tenant selector will be available in all pages. If you're using Refine's layout components, it's recommended to place the tenant selector in the header or sider components.
Check out the Examples below to see an example implementation of a tenant selector.
importReactfrom"react";import{useSelect,useParsed,useGo,useGetToPath}from"@refinedev/core";exportconstTenantSelector = ()=>{const{options,queryResult:{isLoading},} = useSelect({// We're using the `tenants` resource to get the list of tenants// Your API may have a different way to access the list of tenants// or you may have a specific set of tenants that you want to showresource:"tenants",optionLabel:"name",optionValue:"id",});// We'll use the useGo and useGetToPath hooks to navigate to the selected tenantconstgo = useGo();constgetToPath = useGetToPath();// We're using the useParsed hook to get the current route information and params (tenantId)const{resource,action,id,params:{tenantId},} = useParsed();constonChange = (event: React.ChangeEvent<HTMLSelectElement>)=>{constselectedTenantId = event.target.value;go({to:getToPath({resource,action:action ?? "list",id,meta:{// We're passing the selected tenantId to the meta object// Refine will use `meta` to decorate the additional parameters when constructing the route to navigate totenantId:selectedTenantId,},}),type:"replace",});};if(isLoading){return<div>Loading...</div>;}return(<selectonChange={onChange}>{options.map(({label,value})=>(<optionkey={value}value={value}selected={value === tenantId}>{label}</option>))}</select>);};
Dependencies: @refinedev/core@latest
Code Files
File: /components/tenant-selector.tsx
Content: import React from "react";
import { useSelect, useParsed, useGo, useGetToPath } from "@refinedev/core";
export const TenantSelector = () => {
const {
options,
queryResult: { isLoading },
} = useSelect({
// We're using the `tenants` resource to get the list of tenants
// Your API may have a different way to access the list of tenants
// or you may have a specific set of tenants that you want to show
resource: "tenants",
optionLabel: "name",
optionValue: "id",
});
// We'll use the useGo and useGetToPath hooks to navigate to the selected tenant
const go = useGo();
const getToPath = useGetToPath();
// We're using the useParsed hook to get the current route information and params (tenantId)
const {
resource,
action,
id,
params: { tenantId },
} = useParsed();
const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const selectedTenantId = event.target.value;
go({
to: getToPath({
resource,
action: action ?? "list",
id,
meta: {
// We're passing the selected tenantId to the meta object
// Refine will use `meta` to decorate the additional parameters when constructing the route to navigate to
tenantId: selectedTenantId,
},
}),
type: "replace",
});
};
if (isLoading) {
return <div>Loading...</div>;
}
return (
<select onChange={onChange}>
{options.map(({ label, value }) => (
<option key={value} value={value} selected={value === tenantId}>
{label}
</option>
))}
</select>
);
};
Here is the example of multi tenant app built with Refine. You can view the source code and run the apps in your local to understand how multi tenant features are implemented.