Skip to main content
Multi-tenancy is a software architecture pattern where a single instance of an application serves multiple customers (tenants). Each tenant’s data is isolated and remains invisible to other tenants, while sharing the same infrastructure and codebase.

What is Multi-tenancy?

Multi-tenancy, especially in cloud-based systems, refers to the ability of a software application to serve multiple customers simultaneously. While these customers share the same infrastructure and codebase, their data remains separate, and each customer has exclusive access to their own data. Benefits of Multi-tenancy:
  • Resource Sharing: Efficient use of shared infrastructure reduces costs
  • Cost Savings: Lower maintenance costs passed on to customers
  • Customization: Each tenant can adjust settings to their needs
  • Easy Updates: System-wide updates benefit all tenants at once
Use Cases:
  • Cloud Office Tools: Multiple organizations share document management and collaboration features
  • CRM Systems: Businesses manage customer interactions on a shared platform with secure, custom configurations
  • ERP Systems: Companies use shared ERP solutions with separate data and configurations
  • E-commerce Platforms: Sellers run personalized storefronts on a shared backend
  • LMS Platforms: Schools and organizations deliver courses on a shared learning system

Implementation Approaches

There are several approaches to implementing multi-tenancy in Refine applications:

1. Route-based Multi-tenancy

In this approach, the tenant identifier is extracted from the URL route:
// Example: /tenants/:tenantId/posts
import { RefineEnterprise } from "@refinedev/enterprise";
import { useRouterAdapter, WithTenant } from "@refinedev/multitenancy";

const App = () => {
  return (
    <RefineEnterprise
      multitenancyProvider={{
        adapter: useRouterAdapter({
          parameterName: "tenantId",
          parameterKey: "id",
        }),
        fetchTenants: async () => {
          const response = await dataProvider(API_URL).getList({
            resource: "tenants",
            pagination: { mode: "off" },
          });
          
          return {
            tenants: response.data,
            defaultTenant: response.data[0],
          };
        },
      }}
    >
      <WithTenant
        fallback={<div>Tenant not found</div>}
        loadingComponent={<div>Loading...</div>}
      >
        {/* Your app code */}
      </WithTenant>
    </RefineEnterprise>
  );
};

2. Subdomain-based Multi-tenancy

Extract tenant information from the subdomain:
import { useCustomAdapter } from "@refinedev/multitenancy";

const subdomainAdapter = () => {
  const getTenantId = () => {
    const hostname = window.location.hostname;
    const parts = hostname.split('.');
    return parts.length > 2 ? parts[0] : null;
  };

  return {
    get: getTenantId,
    set: (tenantId: string) => {
      window.location.hostname = `${tenantId}.${window.location.hostname}`;
    },
  };
};

const App = () => {
  return (
    <RefineEnterprise
      multitenancyProvider={{
        adapter: subdomainAdapter(),
        fetchTenants: async () => {
          // Fetch tenants logic
        },
      }}
    >
      {/* ... */}
    </RefineEnterprise>
  );
};

3. Header-based Multi-tenancy

Pass tenant information through HTTP headers:
const customDataProvider = {
  ...baseDataProvider,
  getList: async ({ resource, filters = [], meta, ...props }) => {
    const { tenantId } = meta;

    // Add tenant header to requests
    const headers = {
      'X-Tenant-ID': tenantId,
    };

    return baseDataProvider.getList({
      resource,
      filters,
      meta: { ...meta, headers },
      ...props,
    });
  },
};

Data Provider Integration

Refine automatically passes the tenantId to your data provider in the meta object:
import dataProvider from "@refinedev/simple-rest";

const API_URL = "https://api.example.com";
const baseDataProvider = dataProvider(API_URL);

const multiTenantDataProvider = {
  ...baseDataProvider,
  getList: async ({ resource, filters = [], meta, ...props }) => {
    const { tenantId } = meta;

    // Option 1: Add tenantId as a filter
    if (tenantId) {
      filters.push({
        field: "tenantId",
        operator: "eq",
        value: tenantId,
      });
    }

    // Option 2: Add tenantId to the URL
    // const url = `${API_URL}/tenants/${tenantId}/${resource}`;

    return baseDataProvider.getList({
      resource,
      filters,
      meta,
      ...props,
    });
  },
  create: async ({ resource, variables, meta }) => {
    const { tenantId } = meta;

    // Ensure tenantId is included in the payload
    return baseDataProvider.create({
      resource,
      variables: {
        ...variables,
        tenantId,
      },
      meta,
    });
  },
  // Implement other methods similarly...
};

Tenant Selector UI

Add a tenant selector to allow users to switch between tenants:
import { TenantSelect } from "@refinedev/multitenancy/antd";

// In your Header component
export const Header = () => {
  return (
    <div>
      <TenantSelect
        optionLabel="name"
        optionValue="id"
        onChange={(tenant) => console.log('Switched to tenant:', tenant)}
      />
    </div>
  );
};
For Material UI:
import { TenantSelect } from "@refinedev/multitenancy/mui";

<TenantSelect
  optionLabel="name"
  optionValue="id"
  sortTenants={(a, b) => a.name.localeCompare(b.name)}
/>

Access Control with Multi-tenancy

Combine multi-tenancy with access control:
export const accessControlProvider = {
  can: async ({ resource, action, params }) => {
    const { tenantId } = params?.meta || {};
    
    // Check if user has access to this tenant
    const userTenants = await getUserTenants();
    if (!userTenants.includes(tenantId)) {
      return {
        can: false,
        reason: "You don't have access to this tenant",
      };
    }

    // Check resource-level permissions
    if (resource === "posts" && action === "delete") {
      const userRole = await getUserRole(tenantId);
      return {
        can: userRole === "admin",
        reason: userRole !== "admin" ? "Only admins can delete posts" : undefined,
      };
    }

    return { can: true };
  },
};
Always validate tenant access on the server side. Client-side tenant filtering is not sufficient for security.

Database Isolation Patterns

1. Shared Database, Shared Schema

All tenants share the same database and tables, with a tenantId column:
CREATE TABLE posts (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  title VARCHAR(255),
  content TEXT,
  created_at TIMESTAMP
);

CREATE INDEX idx_posts_tenant_id ON posts(tenant_id);
Pros: Simple, cost-effective Cons: Risk of data leakage if queries are not properly filtered

2. Shared Database, Separate Schemas

Each tenant has their own schema within the same database:
CREATE SCHEMA tenant_123;
CREATE SCHEMA tenant_456;

CREATE TABLE tenant_123.posts (...);
CREATE TABLE tenant_456.posts (...);
Pros: Better isolation, easier to backup individual tenants Cons: More complex to manage, schema migration complexity

3. Separate Databases

Each tenant has a completely separate database:
const getTenantDataProvider = (tenantId: string) => {
  const tenantDbUrl = `https://api-${tenantId}.example.com`;
  return dataProvider(tenantDbUrl);
};
Pros: Maximum isolation and security Cons: Higher cost, complex management

Working with useMultitenancy Hook

import { useMultitenancy } from "@refinedev/multitenancy";

const MyComponent = () => {
  const {
    tenant,          // Current tenant object
    tenants,         // List of all tenants
    isLoading,       // Loading state
    setTenant,       // Function to set current tenant
    fetchTenants,    // Function to refetch tenants
    deleteTenant,    // Function to delete current tenant
  } = useMultitenancy();

  return (
    <div>
      <h1>Current Tenant: {tenant?.name}</h1>
      <button onClick={() => setTenant(tenants[0])}>
        Switch to {tenants[0]?.name}
      </button>
    </div>
  );
};

Best Practices

  1. Always validate tenant access server-side: Never rely solely on client-side checks
  2. Use database indexes: Add indexes on tenantId columns for better query performance
  3. Implement tenant context globally: Ensure all database queries include tenant filtering
  4. Handle tenant switching gracefully: Clear caches and refresh data when switching tenants
  5. Monitor tenant usage: Track resource usage per tenant for billing and capacity planning
  6. Test cross-tenant isolation: Regularly audit to ensure no data leakage between tenants
  7. Document tenant onboarding: Create clear processes for adding and removing tenants

Example: Complete Multi-tenant App

Here’s a complete example with Strapi backend:
import { Refine } from "@refinedev/core";
import { RefineEnterprise } from "@refinedev/enterprise";
import { useRouterAdapter, WithTenant } from "@refinedev/multitenancy";
import dataProvider from "@refinedev/strapi-v4";
import routerProvider from "@refinedev/react-router";

const API_URL = "https://api.example.com";

const App = () => {
  return (
    <RefineEnterprise
      routerProvider={routerProvider}
      dataProvider={dataProvider(API_URL)}
      multitenancyProvider={{
        adapter: useRouterAdapter(),
        fetchTenants: async () => {
          const response = await dataProvider(API_URL).getList({
            resource: "organizations",
            pagination: { mode: "off" },
          });
          
          return {
            tenants: response.data,
            defaultTenant: response.data[0],
          };
        },
      }}
      resources={[
        {
          name: "posts",
          list: "/:tenantId/posts",
          create: "/:tenantId/posts/create",
          edit: "/:tenantId/posts/edit/:id",
          show: "/:tenantId/posts/show/:id",
        },
      ]}
    >
      <WithTenant
        fallback={<div>Tenant not found</div>}
        loadingComponent={<div>Loading tenant...</div>}
      >
        {/* Your app routes */}
      </WithTenant>
    </RefineEnterprise>
  );
};

Further Reading

Build docs developers (and LLMs) love