@sensinum/strapi-plugin-multi-domain
v5.2.2
Published
Allow to provide multi domain support
Readme
This plugin brings multi-domain support to your Strapi application. It allows you to isolate data between different domains, providing a robust solution for SaaS applications and other multi-user platforms where data separation is crucial.
✅ GET THE LICENSE: To obtain the license we encourage you to visit the plugin commercial website, select the appropriate plan, and reach out to your preferred reseller or directly contact the plugin authors via this form
⚠️ This plugin is licensed by VirtusLab Ltd. under the End User License Agreement (EULA). Unauthorized use, including usage without a valid license key or modification of the code in a way that breaches the integrity of the software, is strictly prohibited and will be treated as a violation of the EULA, with all associated legal consequences.
Table of Contents
- ✨ Features
- 🎨 Admin Panel Features
- ⚙️ How it works
- ⚠️ Important Considerations
- 🔧 Configuration
- 🧪 Testing
- 👨💻 For Developers: Advanced Customization
- 🔌 Supported Third-Party Plugins
- 📝 License
✨ Features
- Domain-based Data Isolation: Automatically scopes content types to the current user's domain.
- Media Library Segregation: Each domain has its own isolated section within the media library.
- User-Domain Association: Users can be associated with one or more domains through Strapi roles.
- Domain Selector: A convenient domain selector is added to the admin UI for users who belong to multiple domains.
- Super Admin Access: Super admins have the ability to view data across all domains.
- Configurable: Easily configure which content types should be domain-aware.
🎨 Admin Panel Features
- Domain Management UI: A dedicated settings page in the admin panel (
Settings > Multi-Domain Settings) allows administrators to create, view, update, and delete domains. - Domain Selector: Users belonging to multiple domains will see a "Select domain" option in the user menu (top right). This opens a modal allowing them to easily switch between their assigned domains, reloading the app context for the selected domain.
- "Unique by Domain" Custom Field: The plugin registers a new custom field type named "Unique by domain". When applied to a text field on a content type, it ensures that the value of that field is unique among all entries of that type within the same domain.
- Content Manager Integration: To prevent content from being created without a domain, the "Create new entry" button in the Content Manager is hidden when the "Super Admin" domain is selected.
⚙️ How it works
The plugin introduces a Domain collection type and associates other content types with it. When a user logs in, they are associated with a domain. All their actions within the Content Manager and Media Library are then scoped to that domain. This is achieved by decorating Strapi's core services and controllers to automatically add domain filters to database queries.
⚠️ Important Considerations
Users & Permissions Plugin Roles
When using the Users & Permissions plugin with multi-domain functionality, there's an important limitation to be aware of:
The public role does not work with multi-domain by default because the plugin cannot recognize which domain the request belongs to without proper authentication context.
Solutions:
Add
auth: falseto routes: For routes that need to be publicly accessible across domains, you can disable authentication by addingauth: falseto the route configuration.Use API tokens assigned to domains: Ensure all requests include an API token that is properly assigned to a specific domain. This allows the plugin to identify the correct domain context for the request.
Example:
// In your route configuration
module.exports = {
routes: [
{
method: 'GET',
path: '/my-public-endpoint',
handler: 'my-controller.myPublicMethod',
config: {
auth: false, // Disables authentication for this route
},
},
],
};Without these configurations, public requests may fail or return unexpected results because the multi-domain plugin cannot determine the appropriate domain context.
🔧 Configuration
To configure the plugin, create a plugins.js (or plugins.ts) file in your Strapi project's config directory.
File: config/plugins.js
module.exports = {
'multi-domain': {
enabled: true,
config: {
/**
* The code of default domain is used to create a domain for super admins.
* By default, it uses the super admin role code from Strapi.
*/
defaultDomain: {
name: 'Super Admin',
// code is an unique identifier for the domain
// by default it's 'strapi-super-admin'
customFields: {},
},
/**
* A set of content-type uids that should not be scoped by domain.
* By default, all content-types are scoped except for some internal Strapi models.
* @returns {Set<string>}
*/
getOmitContentTypes: () => new Set(['api::my-global-content-type.my-global-content-type']),
/**
* Optional async hook to validate domain customFields before create/update.
* Throw an error to reject the operation.
*/
customFieldValidation: async (customFields) => {
if (!customFields?.myRequiredField) {
throw new Error('myRequiredField is required');
}
},
/**
* A list of content-api endpoints to exclude from domain logic.
* e.g., ['/api/my-public-endpoint']
* @returns {string[]}
*/
getExcludedContentAPIEndpoints: () => [],
getExtraProxyDecorators: () => [
{
condition: (modelName) => modelName === 'api::article.article',
handler(target, targetElement, ctx, prop, modelName) {
// This handler will apply only to the 'article' content type
return async (queryParams, ...restParams) => {
const newQueryParams = {
...queryParams,
where: {
...(queryParams?.where || {}),
// Add some custom filter based on domain or user logic
customFilter: true,
},
};
// Call the original database method with modified params
return targetElement(newQueryParams, ...restParams);
};
},
},
],
},
},
};Configuration Options
defaultDomain(object): Configures the domain that is automatically created for super admins.name(string): The display name for the default domain.customFields(object): A JSON object for any custom data you want to associate with the domain.
getOmitContentTypes(function): Returns aSetof content-type UIDs that should be excluded from multi-domain. Use this for global, non-domain-specific content.getExcludedContentAPIEndpoints(function): Returns an array of URL paths for Content API endpoints that should be publicly accessible and not scoped by domain.customFieldValidation(async function): An optional async validation hook called before a domain is created or updated. It receives the domain'scustomFieldsobject and should throw an error (or return a rejected promise) if validation fails. By default it is a no-op (() => Promise.resolve()).getExtraProxyDecorators(function): An advanced feature that allows you to add custom logic to the database query proxy. This is useful for implementing complex, domain-specific business rules. It should return an array of objects, each with aconditionand ahandlerfunction.condition(function): Takes amodelNameand returnstrueif the handler should be applied.handler(function): A function that wraps the original database method, allowing you to modify its behavior. It receives thetargetrepository, the originaltargetElementmethod, the Koactx, the methodpropname, and themodelName.
Here's an example of how you might use getExtraProxyDecorators to add an additional filter to a specific content type:
// config/plugins.js
module.exports = {
'multi-domain': {
enabled: true,
config: {
/**
* The code of default domain is used to create a domain for super admins.
* By default, it uses the super admin role code from Strapi.
*/
defaultDomain: {
name: 'Super Admin',
// code is an unique identifier for the domain
// by default it's 'strapi-super-admin'
customFields: {},
},
/**
* A set of content-type uids that should not be scoped by domain.
* By default, all content-types are scoped except for some internal Strapi models.
* @returns {Set<string>}
*/
getOmitContentTypes: () => new Set(['api::my-global-content-type.my-global-content-type']),
/**
* A list of content-api endpoints to exclude from domain logic.
* e.g., ['/api/my-public-endpoint']
* @returns {string[]}
*/
getExcludedContentAPIEndpoints: () => [],
getExtraProxyDecorators: () => [
{
condition: (modelName) => modelName === 'api::article.article',
handler(target, targetElement, ctx, prop, modelName) {
// This handler will apply only to the 'article' content type
return async (queryParams, ...restParams) => {
const newQueryParams = {
...queryParams,
where: {
...(queryParams?.where || {}),
// Add some custom filter based on domain or user logic
customFilter: true,
},
};
// Call the original database method with modified params
return targetElement(newQueryParams, ...restParams);
};
},
},
],
},
},
};🧪 Testing
To run the tests for this plugin, navigate to the plugin's directory and run the following command:
This will execute the Jest test suite. To run tests in watch mode, you can use yarn test:watch.
👨💻 For Developers: Advanced Customization via shared/pluginIds.ts
The shared/pluginIds.ts file is a centralized module for constants that are used across both the server-side and client-side parts of the plugin. While it's not part of the standard configuration, it offers a powerful way for developers to perform advanced customizations or re-brand the plugin's core concepts.
Warning: Modifying these values is an advanced procedure. It can have widespread effects and potentially break the plugin's functionality if not done carefully and consistently.
What can be achieved by changing this file?
By changing the values in this file, you can fundamentally alter the plugin's identifiers. For example, if your business logic uses the term "Brand" instead of "Domain," you could update modelId to 'brand' to make the content type, API routes, and database relationships align with your terminology.
Here's a breakdown of the constants and what you can control by changing them:
pluginId: The official ID of the plugin (multi-domain). Changing this will alter the plugin's root path in Strapi, including settings URLs and permission domains.
Note: This value is derived from the
strapi.nameproperty inpackage.jsonand should always be kept in sync with it.
modelId: The singular ID for the domain content type (default:'domain'). Changing this renames the content type itself, which affects the database table, API endpoints, and the name of the relationship field added to other content types.modelServiceId: The identifier for the domain service (default:'domain'). This should typically be kept in sync withmodelId.userField: The property name on theuserobject where associated domain information is stored (default:'domain').modelRoute: The base route for the domain management API (default:'domains'). This is derived frommodelId, so changingmodelIdwill automatically update this.COOKIE_NAME: The name of the cookie used to store the user's currently selected domain (default:'domain'). You might change this to avoid naming conflicts with other cookies in your application.
🔌 Supported Third-Party Plugins
This plugin provides domain-scoped data isolation for the following third-party plugins:
strapi-plugin-navigation
Navigation items and navigations are automatically scoped to the current domain.
strapi-plugin-comments
Comments and comment reports are automatically scoped to the current domain. Since the Authorization header is reserved for the domain API token, author identification for comments is handled via the X-Author-Authorization header:
X-Author-Authorization: Bearer <users-permissions-jwt>This header is processed on comment endpoints that require author context: creating, updating, and deleting comments. If the token is valid, the resolved Strapi user is set as the comment author. Without this header, the comment must include an author object in the request body with id, name, and email fields.
strapi-plugin-reactions
Reactions and reaction types are automatically scoped to the current domain. Since the Authorization header is reserved for the domain API token, author identification for reactions is handled via the X-Author-Authorization header:
X-Author-Authorization: Bearer <users-permissions-jwt>This header is processed on reaction endpoints: listing, creating, deleting, and toggling reactions. If the token is valid, the resolved Strapi user is set as the reaction author. The reactions plugin also supports its own X-Reactions-Author header for custom (non-Strapi) users. When X-Reactions-Author is present, X-Author-Authorization is ignored — custom author identification takes precedence.
📝 License
All rights reserved. Copyright (c) VirtusLab Ltd.
