Skip to main content
🧙‍♂️ refine grants your wishes! Please give us a ⭐️ on GitHub to keep the magic going.
Version: 4.xx.xx

Strapi-v4

What is Multitenancy?

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.

Introduction​

In this guide, we will create an application with you in the logic of Multi Tenant(Multitenancy). We can say multi tenant application is separate and manage multiple contents independently from each other in a single application.

We will make a Cake House application using refine and Strapi-v4. Our Cake House will consist of two separate stores and there will be special products for these stores. We will explain step by step how to manage these stores, products, and orders separately.

CAUTION

This guide has been prepared to assume you know the basics of refine. If you haven't learned these basics yet, we recommend reading the Tutorial.

Setup

npm i @refinedev/strapi-v4
CAUTION

To make this example more visual, we used the @refinedev/antd package. If you are using Refine headless, you need to provide the components, hooks, or helpers imported from the @refinedev/antd package.

Usage

AuthProvider

Show Code

src/authProvider.ts
import { AuthBindings } from "@refinedev/core";
import { AuthHelper } from "@refinedev/strapi-v4";
import axios from "axios";

export const axiosInstance = axios.create();

const API_URL = "YOUR_API_URL";
const TOKEN_KEY = "strapi-jwt-token";
const strapiAuthHelper = AuthHelper(API_URL + "/api");

export const authProvider: AuthBindings = {
login: async ({ username, password }) => {
try {
const { data, status } = await strapiAuthHelper.login(
username,
password,
);
if (status === 200) {
localStorage.setItem(TOKEN_KEY, data.jwt);

// set header axios instance
axiosInstance.defaults.headers.common[
"Authorization"
] = `Bearer ${data.jwt}`;

return {
success: true,
};
}
} catch (error: any) {
return {
success: false,
error: {
name: error.response.data.error.name,
message: error.response.data.error.message,
},
};
}

return {
success: false,
error: {
message: "Login failed",
name: "Invalid email or password",
},
};
},
logout: async () => {
localStorage.removeItem(TOKEN_KEY);
return {
success: true,
redirectTo: "/",
};
},
onError: async (error) => {
console.error(error);
return { error };
},
check: async () => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
axiosInstance.defaults.headers.common[
"Authorization"
] = `Bearer ${token}`;
return {
authenticated: true,
};
}

return {
authenticated: false,
error: {
message: "Check failed",
name: "Token not found",
},
logout: true,
redirectTo: "/login",
};
},
getPermissions: async () => null,
getIdentity: async () => {
const token = localStorage.getItem(TOKEN_KEY);
if (!token) {
return null;
}

const { data, status } = await strapiAuthHelper.me(token);
if (status === 200) {
const { id, username, email } = data;
return {
id,
username,
email,
};
}

return null;
},
};

TIP

If you need the population for the /me request, you can use it like this in your authProvider.

const strapiAuthHelper = AuthHelper(API_URL + "/api");

strapiAuthHelper.me("token", {
meta: {
populate: ["role"],
},
});
App.tsx
import { Refine, Authenticated } from "@refinedev/core";
import { Layout, notificationProvider, ErrorComponent } from "@refinedev/antd";
import { DataProvider } from "@refinedev/strapi-v4";
import routerProvider, {
NavigateToResource,
CatchAllNavigate,
} from "@refinedev/react-router-v6";

import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";

import "@refinedev/antd/dist/reset.css";

import { authProvider, axiosInstance } from "./authProvider";

const API_URL = "YOUR_API_URL";

const App: React.FC = () => {
return (
<BrowserRouter>
<Refine
authProvider={authProvider}
dataProvider={DataProvider(API_URL + "/api", axiosInstance)}
routerProvider={routerProvider}
notificationProvider={notificationProvider}
>
{/* ... */}
</Refine>
</BrowserRouter>
);
};
TIP

You can find detailed usage information and the source code here.

Create Collections

We created three collections on Strapi as store, product, and order and added a relation between them. For detailed information on how to create a collection, you can check here.

Stores

  • Title: Text
  • Relation with Products
  • Relation with Orders
stores

Products

  • Title: Text
  • Description: Text
  • Image: Media
  • Relation with Stores
  • Relation with Orders
products

Orders

  • Status: Text
  • Customer Name: Text
  • Customer Address: Text
  • Quantity: Number
  • Relation with Stores
  • Relation with Product
orders

Now that we have completed the setup and our collections, we can now log in with the refine and start the listing processes.

Create Resources and Routes

To view the products and orders of two different stores separately, we need to filter by storeId. We will use the storeId information in more than one place. For example, when creating a store-specific order. We will also add this as a prefix to the routes. (example.com/:tenant/products)

src/App.tsx
const App: React.FC = () => {
// When `domain.com` is entered, we set the default tenant to redirect `domain.com/name`.
const tenant = "1";

return (
<BrowserRouter>
<GitHubBanner />
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
authProvider={authProvider}
dataProvider={DataProvider(API_URL + "/api", axiosInstance)}
routerProvider={routerProvider}
// The path definition for `list`, `create`, `show`, `edit` pages is as follows and variables can be used as in `react-router`.
resources={[
{
name: "products",
list: "/:tenant/products",
meta: {
tenant,
},
},
{
name: "orders",
list: "/:tenant/orders",
create: "/:tenant/orders/create",
edit: "/:tenant/orders/edit/:id",
meta: {
tenant,
},
},
]}
notificationProvider={notificationProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
}}
>
<Routes>
{/* ... */}
<Route
index
element={
<NavigateToResource resource="products" />
}
/>
{/* prefix `resources` paths. */}
<Route path="/:tenant">
<Route path="products">
<Route index element={<ProductList />} />
</Route>

<Route path="orders">
<Route index element={<OrderList />} />
<Route
path="create"
element={<OrderCreate />}
/>
<Route
path="edit/:id"
element={<OrderEdit />}
/>
</Route>
</Route>
</Routes>
<UnsavedChangesNotifier />
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};

Using the tenant at the other components

You may want to get the tenant within the project. This is easily get with the useParsed hook.

import { useParsed } from "@refinedev/core";

const { params } = useParsed<{ tenant: string }>();

console.log(params?.tenant); // { tenant: "refine" }

Shop Select to Sider Component

We will create a select component in the Header Menu where the user will select the stores. Let's create our select component first, then let's see how we can define it in the refine Header component.

scr/components/select/StoreSelect.tsx
import { useSelect } from "@refinedev/antd";
import { Select } from "antd";
import { useGetToPath, useGo, useParsed } from "@refinedev/core";

import { IStore } from "interfaces";

type SelectProps = {
onSelect?: () => void;
};

export const StoreSelect: React.FC<SelectProps> = ({ onSelect }) => {
const getToPath = useGetToPath();
const go = useGo();
const { resource, action, params } = useParsed<{ tenant: string }>();

const { selectProps: storeSelectProps } = useSelect<IStore>({
resource: "stores",
optionLabel: "title",
optionValue: "id",
});

if (!params?.tenant) {
return null;
}

return (
<Select
defaultValue={+params?.tenant}
style={{ width: 120 }}
onChange={(tenant) =>
go({
to: getToPath({
resource,
action: action || "list",
meta: {
tenant,
},
}),
})
}
onSelect={onSelect}
>
{storeSelectProps.options?.map(({ value, label }) => (
<Select.Option key={value} value={value}>
{label}
</Select.Option>
))}
</Select>
);
};

Here we have created a select component. Then we fetch the store id and title we created in the Strapi database with useSelect.

Let's define the select component in the refine Header.

Check out how you can customize Header

Show Code

src/components/header/index.tsx
import React from "react";
import { Layout as AntdLayout, Typography, Avatar, Space, theme } from "antd";
import { useActiveAuthProvider, useGetIdentity } from "@refinedev/core";
import { RefineThemedLayoutV2HeaderProps } from "@refinedev/antd";

// highlight-next-**line**
import { StoreSelect } from "../select";

const { Text } = Typography;
const { useToken } = theme;

export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = () => {
const { token } = useToken();

const authProvider = useActiveAuthProvider();
const { data: user } = useGetIdentity({
v3LegacyAuthProviderCompatible: Boolean(authProvider?.isLegacy),
});

return (
<AntdLayout.Header
style={{
backgroundColor: token.colorBgElevated,
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
padding: "0px 24px",
height: "64px",
position: "sticky",
top: 0,
zIndex: 1,
}}
>
<Space
style={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
<StoreSelect />
<Space size="middle">
{user?.username && (
<>
<Text strong>{user.username}</Text>
<Avatar>R</Avatar>
</>
)}
</Space>
</Space>
</AntdLayout.Header>
);
};

header
As you can see, you can create a store-specific product and order by selecting tenant in the Header component and choosing according to the storeId information.

Product List Page

Now we can list the products of the selected store according to the storeId information by filtering it. We can do this filtering by using the filters.permanent property within the refine's useSimpleList hook.

We separate the products of different stores by using the filters.permanent with the storeId we get from the Store Context. So we can control more than single content in one application.

const { params } = useParsed<{ tenant: string }>();

const { listProps } = useSimpleList<IProduct>({
filters: {
permanent: [{ field: "stores][id]", operator: "eq", value: params?.tenant, }],
},
});
Show Code

src/pages/ProductList.tsx
import {
IResourceComponentsProps,
HttpError,
useParsed,
} from "@refinedev/core";
import {
useSimpleList,
useModalForm,
CreateButton,
List,
} from "@refinedev/antd";
import { List as AntdList } from "antd";

import { IProduct } from "interfaces";

import { ProductItem, CreateProduct, EditProduct } from "components/product";

export const ProductList: React.FC<IResourceComponentsProps> = () => {
const { params } = useParsed<{ tenant: string }>();
const { listProps } = useSimpleList<IProduct>({
permanentFilter: [
{
field: "stores][id]",
operator: "eq",
value: params?.tenant,
},
],
metaData: { populate: ["image"] },
});

const {
modalProps: createModalProps,
formProps: createModalFormProps,
show: createShow,
} = useModalForm<IProduct, HttpError, IProduct>({
action: "create",
resource: "products",
redirect: false,
});

const {
modalProps: editModalProps,
formProps: editFormProps,
show: editShow,
} = useModalForm<IProduct, HttpError, IProduct>({
action: "edit",
metaData: { populate: ["image"] },
resource: "products",
redirect: false,
});

return (
<>
<List
headerProps={{
extra: <CreateButton onClick={() => createShow()} />,
}}
>
<AntdList
grid={{ gutter: 16, xs: 1 }}
style={{
justifyContent: "center",
}}
{...listProps}
renderItem={(item) => (
<AntdList.Item>
<ProductItem item={item} editShow={editShow} />
</AntdList.Item>
)}
/>
</List>
<EditProduct
modalProps={editModalProps}
formProps={editFormProps}
/>
<CreateProduct
modalProps={createModalProps}
formProps={createModalFormProps}
/>
</>
);
};

Filter Tenant
TIP

In this example, we used the filter.permanent object to filter the data, as Appwrite does not support multitenancy. However, you can do this from a single point by swizzle the data provider in your own RestApi.

You can check out the swizzle data provider guide for more information.

The resource.meta object is passed as meta to all methods in the data providers. For this you have to swizzle the data provider.

src/dataProvider.ts
//...
export const dataProvider = (): Required<DataProvider> => {
//...
return {
getList: async ({ resource, pagination, filters, sorters, meta }) => {
// ...
console.log(meta.tenant); // { tenant: "refine" }
},
getOne: async ({ resource, id, meta }) => {
// ...
console.log(meta.tenant); // { tenant: "refine" }
}
// ...
}
}

Product Create Page

Now let's see how we can create store-specific products. Which store we choose in Sider, the product we will create will automatically be the product of the selected store.

By overriding the onFinish method of the form and sending the selected store information, we specify which store it will be the product of.

import { useParsed } from "@refinedev/core";
const { params } = useParsed<{ tenant: string }>();

<Form
{...formProps}
...
onFinish={(values) => {
return (
formProps.onFinish?.({
...values,
stores: params?.tenant,
})
);
}}
>
Show Code

CreateProduct
import { useParsed } from "@refinedev/core";
import { mediaUploadMapper, getValueProps } from "@refinedev/strapi-v4";
import { Form, FormProps, Input, Upload, ModalProps, Modal } from "antd";

import { TOKEN_KEY, API_URL } from "../../constants";

type CreateProductProps = {
modalProps: ModalProps;
formProps: FormProps;
};

export const CreateProduct: React.FC<CreateProductProps> = ({
modalProps,
formProps,
}) => {
const { params } = useParsed<{ tenant: string }>();

return (
<Modal {...modalProps}>
<Form
{...formProps}
layout="vertical"
initialValues={{
isActive: true,
}}
onFinish={(values) => {
console.log("values", values);
formProps.onFinish?.(
mediaUploadMapper({
...values,
stores: [params?.tenant],
}),
);
}}
>
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<Input />
</Form.Item>
<Form.Item label="Image">
<Form.Item
name="image"
valuePropName="fileList"
getValueProps={(data) => getValueProps(data, API_URL)}
noStyle
rules={[
{
required: true,
},
]}
>
<Upload.Dragger
name="files"
action={`${API_URL}/api/upload`}
headers={{
Authorization: `Bearer ${localStorage.getItem(
TOKEN_KEY,
)}`,
}}
listType="picture"
multiple
>
<p className="ant-upload-text">
Drag & drop a file in this area
</p>
</Upload.Dragger>
</Form.Item>
</Form.Item>
</Form>
</Modal>
);
};

Create Product

Conclusion

In this guide and in our example app, we talked about how we can build multitenancy apps with refine. Developing a multitenancy application is quite simple with the flexible route infrastructure of refine.

Example

Demo Credentials

Email: demo@refine.dev
Password: demodemo

Run on your local
npm create refine-app@latest -- --example multi-tenancy-strapi