# Strapi contributor docs > How Strapi is built, and how to contribute to it This file contains all documentation content in a single document following the llmstxt.org standard. ## Strapi (WIP) import Type from '@site/docs/api/components/type'; ## Strapi :::info Current state: **Stable** ::: The Strapi class is the main object used in Strapi projects. An instance of Strapi class is available as a global in any Strapi project: `global.strapi`. ### Class: Strapi #### `new Strapi(opts)` - `opts`: Object Options that can be used on Strapi startup - `autoReload`: Boolean **Default:** true - If false, deactivate auto reload - If you modify any file in your Strapi project, it reloads your nodejs app - If any content-type is changed, it will reload the nodejs app - `serveAdminPanel`: Boolean **Default:** true - Should the admin panel be loaded and serve as a web client - The admin panel build will not be delivered if false - `appDir`: String **Default:** `process.cwd()` - The directory relative or absolute path where Strapi will write every file (schemas, generated APIs, controllers or services) - `distDir`: String **Default:** appDir value - The directory relative or absolute path where Strapi will read configurations, schemas and any compiled code Instances of the Strapi class can be created using the new keyword. Strapi extends the [Container](./container) class. ```javascript const strapiInstance = new Strapi(); ``` #### `strapi.dirs` - Object Stored paths of file system. - `dirs.dist`: [StrapiPathObject](#strapipathobject) - Build folder - `dirs.app`: [StrapiPathObject](#strapipathobject) - Sources folder - `dirs.static`: Object Define path to directories involving web client display - `public`: String Path to the folder to serve publicly (like files, images, etc..) ##### StrapiPathObject - Object A set of paths to specific Strapi project parts. - `root`: String Root path - `src`: String Sources route path to project files - `api`: String Path to the folder containing project developers' API files (content-types, controllers, services, routes, etc..) - `components`: String Path to the folder containing project developers' components - `policies`: String Path to the folder where the Strapi project developers' policies are stored - A set of functions that check the state of the data and prevent the access to the API accordingly - `middlewares`: String Path to the folder where the Strapi project developers' middlewares are stored - A set of function that wrap around routes and requests - `config`: String Path to the folder containing project developers' config files #### `strapi.isLoaded` {/* prettier-ignore */} - Boolean - `true`: Everything (all `register` and `bootstrap` functions available in your strapi project) has been loaded - `false`: There is something loading Note: `register` functions are called before the `bootstrap` functions. #### `strapi.reload()` Reload the app. This function defines itself at the construction of the Strapi class. #### `strapi.server` - [StrapiServer](./strapi-server) Strapi server object. #### `strapi.fs` - [StrapiFS](StrapiFS) Wrapper around [FS NodeJS module](https://nodejs.org/docs/latest-v18.x/api/fs.html). #### `strapi.eventHub` - [EventHub](EventHub) The `strapi.eventHub` object is used to manipulate events within a Strapi project. It is an instance of the built-in EventEmitter class from Node.js, which provides a simple way to emit and listen for events. The `strapi.eventHub` object is created using the `createEventHub()` function in the [EventHub](EventHub) module of the Strapi core. This function returns a new instance of the EventHub class, which extends the EventEmitter class and adds some additional functionality specific to Strapi. ##### Examples: ```javascript // Listen for a 'user.updated' event and log the data strapi.eventHub.on('user.updated', (data) => { console.log(`User ${data.id} has been updated`); }); // Emit a 'user.created' event with some data strapi.eventHub.emit('user.created', { username: 'johndoe', email: 'johndoe@example.com' }); ``` In this example, we are emitting a `user.created` event with some data attached to it, and then listening for a user.updated event and logging the data. These events can be used to trigger actions within the Strapi application or to communicate with external systems. For more information on how to use the EventEmitter class and its methods, see the [Node.js documentation](https://nodejs.org/docs/latest-v18.x/api/events.html#class-eventemitter). #### `strapi.startupLogger` - [StartupLogger](StartupLogger) Object containing predefined logger functions. Used for Strapi startup. (do not use as a logger elsewhere) #### `strapi.log` - [Winston](https://github.com/winstonjs/winston#creating-your-own-logger) A logger provided by Strapi that uses the Winston logging library. It is the result of calling the `winston.createLogger()` function with the configuration defined by the user of the Strapi application. The logger provides various methods for logging messages at different levels of severity, including error, warn, info, verbose, debug, and silly. The logging level can be set via the configuration to control which messages are logged. ##### Examples ```javascript // Log an error message strapi.log.error('Failed to start server', { error: err }); // Log a warning message strapi.log.warn('Server is running in development mode'); // Log an informational message strapi.log.info(`Server started on port ${PORT}`); // Log a verbose message strapi.log.verbose('Application state', { user: currentUser }); // Log a debug message strapi.log.debug('API request received', { method: req.method, path: req.path }); // Log a silly message strapi.log.silly('Entered loop', { count: i }); ``` In these examples, we are logging messages at different levels of severity, including error, warn, info, verbose, debug, and silly. We are also passing in metadata as an object in the second parameter of each logging method. The messages logged by strapi.log will be output according to the logging configuration set by the user of the Strapi application. This configuration determines which messages are logged and where they are logged (e.g. console, file, etc.). #### `strapi.cron` - [CronService](Cron) Module to schedule cron jobs for Strapi project. It is an instance of a custom Cron object. #### `strapi.telemetry` - [TelemetryService](Telemetry) The `strapi.telemetry` property provides access to the telemetry service instance. This service collects anonymous usage data about your Strapi application to help the Strapi team improve the product. By default, the telemetry service is enabled, but you can disable it by setting the telemetryDisabled property to true in your application's package.json file, or by setting the `STRAPI_TELEMETRY_DISABLED` environment variable to true. You can also disable telemetry programmatically by setting the isDisabled property of the `strapi.telemetry` instance to true. #### `strapi.requestContext` {/* prettier-ignore */} - Object Context Storage - `run(store, cb)`: Function - `store`: Any Value that should be retrieved - `cb`: Function Callback - `get()` Function The request context stores the ctx object from KoaJS on each request. This allows users to have access to the context from anywhere through the Strapi instance. #### `strapi.customFields` {/* prettier-ignore */} - Object - `register(customField)`: Function Register a new custom field This property is a shortcut to `strapi.get('custom-fields').add(customField)`. ##### Examples ```javascript strapi.customFields.register({ name: 'color', plugin: 'color-picker', type: 'string', }); ``` #### `strapi.config` - Object Shortcut to `strapi.get('config')`. See the [config container](#config). #### `strapi.services` - Object[] Shortcut to `strapi.get('services').getAll()`. See the [services' container](#services). #### `strapi.service(uid)` - `uid`: String Shortcut to `strapi.get('services').get(uid)`. See the [services' container](#services). #### `strapi.controllers` - Object[] Shortcut to `strapi.get('controllers').getAll()`. See the [controllers' container](#controllers). #### `strapi.controller(uid)` - `uid`: String Shortcut to `strapi.get('controllers').get(uid)`. See the [controllers' container](#controllers). #### `strapi.contentTypes` - Object[] Shortcut to `strapi.get('content-types').getAll()`. See the [content-types' container](#content-types). #### `strapi.contentType(name)` - `name`: String Shortcut to `strapi.get('content-types').get(name)`. See the [content-types' container](#content-types). #### `strapi.policies` - Object[] Shortcut to `strapi.get('policies').getAll()`. See the [policies' container](#policies). #### `strapi.policy(name)` - `name`: String Shortcut to `strapi.get('policies').get(name)`. See the [policies' container](#policies). #### `strapi.middlewares` - Object[] Shortcut to `strapi.get('middlewares').getAll()`. See the [middlewares container](#middlewares). #### `strapi.middleware(name)` - `name`: String Shortcut to `strapi.get('middlewares').get(name)`. See the [middlewares container](#middlewares). #### `strapi.plugins` - Object[] Shortcut to `strapi.get('plugins').getAll()`. See the [plugins' container](#plugins). #### `strapi.plugin(name)` - `name`: String Shortcut to `strapi.get('plugins').get(name)`. See the [plugins' container](#plugins). #### `strapi.hooks` - Object[] Shortcut to `strapi.get('hooks').getAll()`. See the [hooks' container](#hooks). #### `strapi.hook(name)` - `name`: String Shortcut to `strapi.get('hooks').get(name)`. See the [hooks' container](#hooks). #### `strapi.apis` - Object[] Shortcut to `strapi.get('apis').getAll()`. See the [apis container](#apis). #### `strapi.api(name)` - `name`: String Shortcut to `strapi.get('apis').get(name)`. See the [apis container](#apis). #### `strapi.auth` - Object Shortcut to `strapi.get('auth')`. See the [auth' container](#auth). #### `strapi.contentAPI` - Object Shortcut to `strapi.get('content-api')`. See the [content-api container](#content-api). #### `strapi.sanitizers` - Object Shortcut to `strapi.get('sanitizers')`. See the [sanitizers' container](#sanitizers). #### `strapi.validators` - Object Shortcut to `strapi.get('validators')`. See the [validators' container](#validators). #### `strapi.start()` - Returns: Promise :::info TODO ::: #### `strapi.destroy()` - Returns: Promise :::info TODO ::: #### `strapi.sendStartupTelemetry()` :::info TODO ::: #### `strapi.openAdmin({ isInitialized })` - Returns: Promise :::info TODO ::: #### `strapi.postListen()` - Returns: Promise :::info TODO ::: #### `strapi.listen()` - Returns: Promise :::info TODO ::: #### `strapi.stopWithError()` :::info TODO ::: #### `strapi.stop(exitCode)` :::info TODO ::: #### `strapi.loadAdmin()` - Returns: Promise :::info TODO ::: #### `strapi.loadPlugins()` - Returns: Promise :::info TODO ::: #### `strapi.loadPolicies()` - Returns: Promise :::info TODO ::: #### `strapi.loadAPIs()` - Returns: Promise :::info TODO ::: #### `strapi.loadComponents()` - Returns: Promise :::info TODO ::: #### `strapi.loadMiddlewares()` - Returns: Promise :::info TODO ::: #### `strapi.loadApp()` - Returns: Promise :::info TODO ::: #### `strapi.loadSanitizers()` - Returns: Promise :::info TODO ::: #### `strapi.registerInternalHooks()` :::info TODO ::: #### `strapi.register()` - Returns: Promise :::info TODO ::: #### `strapi.bootstrap()` - Returns: Promise :::info TODO ::: #### `strapi.load()` - Returns: Promise :::info TODO ::: #### `strapi.reload()` :::info TODO ::: #### `strapi.runLifecyclesFunctions()` - Returns: Promise :::info TODO ::: #### `strapi.getModel(uid)` - `uid`: String :::info TODO ::: #### `strapi.db.query(uid)` - `uid`: String :::info TODO ::: ### Strapi containers The strapi containers are accessible via `strapi.get('name-of-the-container')`. #### `config` {/* prettier-ignore */} - Object - `get(path, defaultValue)`: Function - `path`: String - `defaultValue`: Any - Returns: Any - The value located at `path` or, if undefined, `defaultValue`. - `set(path, value)`: Function - `path`: String - Where the value should be stored - `value`: Any - `has(path)`: Function - `path`: String - Returns: Boolean - Does the `path` match a value stored in the config container. - `launchedAt`: Number **Default:** `Date.now()` Date in milliseconds when the server has started - `serveAdminPanel`: Boolean **Default:** `true` See [Strapi constructor](#new-strapiopts) options - `autoReload`: Boolean **Default:** `false` See [Strapi constructor](#new-strapiopts) options - `environment`: String - process.env.NODE_ENV - `uuid`: String - string extracted from `package.json` located in `strapi.uuid` - `packageJsonStrapi`: Object - object extracted from `package.json` located in `strapi` (except uuid) - `info`: Object - everything stored in the `package.json` - `strapi`: String - Current version of Strapi Every file stored under the `config` folder will be injected in this config container object. #### `services` :::info TODO ::: #### `controllers` :::info TODO ::: #### `content-types` :::info TODO ::: #### `policies` :::info TODO ::: #### `middlewares` :::info TODO ::: #### `plugins` :::info TODO ::: #### `hooks` :::info TODO ::: #### `apis` :::info TODO ::: #### `auth` :::info TODO ::: #### `content-api` :::info TODO ::: #### `sanitizers` :::info TODO ::: #### `validators` :::info TODO ::: --- ## API (WIP) import Type from '@site/docs/api/components/type'; ## API :::info Current state: **Stable** ::: The Strapi API module permits to generate a Strapi API object that wrap all the functionalities around Strapi endpoints ### Module: API #### `createAPI(strapi, opts)` :::info TODO ::: --- ## Container import Type from '@site/docs/api/components/type'; :::info Current state: **Stable** ::: The container module permits to generate containers. ### Module: container #### class Container #### `new Container()` - Returns: Container ```javascript const container = new Container(); container.add('config', { get: (configName) => {}, set: (configName, value) => {}, }); const dbConfig = container.get('config').get('database'); ``` #### `container.add(name, resolver)` - `name`: String UID of the content - `resolver`: Function | Any - As a function, the function will be executed when the first get method is called on this content. The result of this function will define the content of this UID. - `resolver(context, args)` - `context`: Container The container instance - `args`: Any Anything to be used by the resolver function - As anything else, this value will be resolved when getting this specified content through its UID. Register a new content to be accessed inside the container. If the name is already used, it will throw an error. ```javascript const container = new Container(); container.add('config', (container, args) => {}); // or container.add('services', {}); ``` #### `container.get(name, args)` - `name`: String UID of the content - `args`: Any Value that will be passed to the resolver (if function) Get the value stored for a specific `name`. ```javascript const container = new Container(); container.add('config', { db: 'sqlite' }); const config = container.get('config'); // config.db === 'sqlite' ``` ⚠️ If the **resolver**, used in the register function, is a **function**, the value will be the result of this resolver function with `args` as parameter on the first call to `get`. Please pay attention that the resolver result value isn't awaited. So if resolver returns a promise, the value stored will be a promise. ```javascript const container = new Container(); container.add('boolean', (bool) => bool); // First call - The value is resolved through the resolver above "(bool) => bool" container.get('boolean', true); // true // Any further call will use the previously set value container.get('boolean'); // true // Even if we try to push a new value container.get('boolean', false); // true ``` --- ## Cron Service import Type from '@site/docs/api/components/type'; ## Cron :::info Current state: **Stable** ::: The Strapi Cron Service provides a way to add, remove, start, and stop cron jobs in a Strapi application. ### Module: Cron Service #### createCronService() The `createCronService()` function returns an object that provides methods to manage cron jobs. ### Methods #### `cron.add(tasks)` - `tasks`: Object - Returns: `this` Adds one or more cron tasks to the service. - Each key of the `tasks` object is the name of the task. - Each value of the `tasks` object can be either a function, or an object with two properties: `task` and `options`. - If the value is a function, it is used as the task to be executed when the cron expression is met. - The key will be considered as the cron expression - If the value is an object, its `task` property is used as the task function, and its `options` property is used as the schedule: a cron `string`, a one-shot `Date` (or timestamp `number`), `{ rule, tz?, start?, end? }`, or a node-schedule-style recurrence object (`{ dayOfWeek, hour, minute, … }`). `start`/`end` may be `Date`, timestamp, or ISO string. The task is called as `task({ strapi }, fireDate)`. ##### Example ```javascript const { createCronService } = require('packages/core/strapi/lib/services/cron.js'); const cron = createCronService(); const task = () => { console.log('Task executed!'); }; cron.add({ myTask: { task, options: '*/5 * * * *', // Executes every 5 minutes. }, '*/1 * * * *': () => console.log('A minute has passed.'), }); ``` #### `cron.remove(name)` - `name`: String - Returns: `this` Removes a cron task from the service. - The `name` parameter is the name of the task to remove. ##### Example ```javascript const { createCronService } = require('packages/core/strapi/lib/services/cron.js'); const cron = createCronService(); const task = () => { console.log('Task executed!'); }; cron.add({ myTask: { task, options: '*/5 * * * *', // Executes every 5 minutes. }, }); cron.remove('myTask'); ``` #### `cron.start()` - Returns: `this` Starts the cron service. - Schedules all the cron jobs. ##### Example ```javascript const { createCronService } = require('packages/core/strapi/lib/services/cron.js'); const cron = createCronService(); const task = () => { console.log('Task executed!'); }; cron.add({ myTask: { task, options: '*/5 * * * *', // Executes every 5 minutes. }, }); cron.start(); ``` #### `cron.stop()` - Returns: `this` Stops the cron service. - Cancels all the scheduled jobs. ##### Example ```javascript const { createCronService } = require('packages/core/strapi/lib/services/cron.js'); const cron = createCronService(); const task = () => { console.log('Task executed!'); }; cron.add({ myTask: { task, options: '*/5 * * * *', // Executes every 5 minutes. }, }); // Start the scheduled cron jobs cron.start(); // Stops the cron jobs cron.stop(); ``` #### `cron.destroy()` - Returns: `this` Destroys the cron service. - Calls the `stop()` method. - Clears the list of cron jobs. ##### Example ```javascript const { createCronService } = require('packages/core/strapi/lib/services/cron.js'); const cron = createCronService(); const task = () => { console.log('Task executed!'); }; cron.add({ myTask: { task, options: '*/5 * * * *', // Executes every 5 minutes. }, }); // Start the scheduled cron jobs cron.start(); // Stops the cron jobs and remove all scheduled tasks cron.destroy(); ``` ### Properties #### `cron.jobs` {/* prettier-ignore */} - Array - Object - `job`: [Cron](https://github.com/Hexagon/croner) - Croner job, with node-schedule aliases `invoke`, `cancel`, `nextInvocation`, and `reschedule` - `options`: String | Number | Date | Object - Cron string, timestamp, one-shot `Date`, `{ rule, tz?, start?, end? }`, or a recurrence object - `name`: String - The name of the task associated to the job An array of the cron jobs added to the service. --- ## EventHub (WIP) import Type from '@site/docs/api/components/type'; ## Strapi Event Hub :::info Current state: **Stable** ::: Strapi Event Hub module - description to be done ### Module: EventHub #### `createEventHub()` :::info TODO ::: --- ## Startup Logger import Type from '@site/docs/api/components/type'; ## API :::info Current state: **Stable** ::: This module is a simple logger for starting up Strapi with some useful information. ### Module: Startup Logger #### `logStats()` This log will display information about the instance of Strapi. The time launched, how many times it took and important configuration information. ``` Project information ┌────────────────────┬──────────────────────────────────────────────────┐ │ Time │ Wed Jan 01 2000 00:00:01 GMT+0200 (Central Euro… │ │ Launched in │ 2000 ms │ │ Environment │ development │ │ Process PID │ 42 │ │ Version │ 4.9.0 (node v18.12.1) │ │ Edition │ Enterprise │ │ Database │ postgres │ └────────────────────┴──────────────────────────────────────────────────┘ ``` #### `logFirstStartupMessage()` This log will display the first time Strapi project is launched. It will ask the user to create its first admin user in the admin panel. #### `logDefaultStartupMessage()` Default message to display when the Strapi server is started. ``` Actions available Welcome back! To manage your project 🚀, go to the administration panel at: http://localhost:1337/admin To access the server ⚡️, go to: http://localhost:1337 ``` #### `logStartupMessage({ isInitialized })` - `isInitialized`: Boolean Has the Strapi project already been initialized? Will display the correct start-up message according to the specified boolean. :::note Can be disabled by setting the server config value: ```javascript // in config/server.js { logger: { startup: { enabled: false; } }, // ...the rest of your server config } ``` ::: --- ## StrapiFS (WIP) import Type from '@site/docs/api/components/type'; ## Strapi File System :::info Current state: **Stable** ::: The Strapi FS module is a wrapper around FS NodeJS module to manipulate local files. ### Module: StrapiFS #### `createStrapiFs(strapi)` #### `strapiFs.writeAppFile(optPath, data)` :::caution Deprecated ::: #### `strapiFs.writePluginFile(plugin, optPath, data)` :::caution Deprecated ::: #### `strapiFs.removeAppFile(optPath)` :::caution Deprecated ::: #### `strapiFs.appendFile(optPath, data)` --- ## Strapi server (WIP) import Type from '@site/docs/api/components/type'; ## Strapi Server :::info Current state: **Stable** ::: The Strapi server module permits to generate a Strapi http server. ### Module: Strapi server #### `createServer(strapi)` - `strapi`: [Strapi](Strapi) - Returns: StrapiServer ```javascript const server = createServer(strapi); server.listRoutes(); ``` #### `StrapiServer.app` - [KoaJS](https://devdocs.io/koa/index) Strapi projects are using KoaJS to run the NodeJS server. #### `StrapiServer.router` - [@koa/router](https://github.com/ZijianHe/koa-router#router-) Strapi projects are using a dependency of KoaJS called @koa/router. #### `StrapiServer.httpServer` - [http.Server](https://nodejs.org/docs/latest-v18.x/api/http.html) The Strapi's HTTP server. #### `StrapiServer.api(name)` - `name`: String - Returns: [StrapiAPIs](#strapiapis) Getter for apis available in Strapi #### `StrapiServer.use(...args)` - [KoaApp.use](https://devdocs.io/koa/index#appusefunction) Shortcut for Koa `app.use(...args)` method. #### `StrapiServer.routes(routes)` :::info TODO ::: #### `StrapiServer.mount()` :::info TODO ::: #### `StrapiServer.initRouting()` :::info TODO ::: #### `StrapiServer.initMiddlewares()` :::info TODO ::: #### `StrapiServer.listRoutes()` :::info TODO ::: #### `StrapiServer.listen(...args)` :::info TODO ::: #### `StrapiServer.destroy()` :::info TODO ::: #### `StrapiAPIs` - Object - `content-api`: [API](API) - API used by external requesters - `admin`: [API](API) - API used by admin panel Strapi APIs is a map of all APIs available inside the Strapi project. --- ## Telemetry Service import Type from '@site/docs/api/components/type'; ## Telemetry :::info Current state: **Stable** ::: The telemetry service is responsible for collecting and sending anonymous usage data to Strapi. This service is disabled by default, but can be enabled or disabled via configuration. ### Usage Information The collected usage data is used to help Strapi improve the product by identifying areas of improvement, tracking feature adoption, and measuring performance. You can learn more about the usage data that is collected by visiting the following link: https://docs.strapi.io/developer-docs/latest/getting-started/usage-information.html ### Module: Telemetry Service #### createTelemetryInstance() - strapi: [Strapi](Strapi) - A strapi instance. The `createTelemetryInstance()` function returns an instance of the Telemetry service. ##### Examples ```javascript const createTelemetryInstance = require('path/to/telemetry'); const telemetry = createTelemetryInstance(strapi); ``` ### Methods #### `telemetry.register()` Registers the telemetry instance. ##### Examples ```javascript telemetry.register(); ``` #### `telemetry.bootstrap()` Bootstraps the telemetry instance. ##### Examples ```javascript telemetry.bootstrap(); ``` #### `telemetry.destroy()` Destroys the telemetry instance. ##### Examples ```javascript telemetry.destroy(); ``` #### `telemetry.send(event, payload)` - `event`: String - The event to be sent. - `payload`: [TelemetryPayload](#telemetrypayload) - The payload to be sent with the event. - Returns: Promise Sends telemetry event with the given payload. ##### Examples ```javascript telemetry.send('event_name', { key: 'value' }); ``` ### Types #### `TelemetryPayload` {/* prettier-ignore */} - Object - `eventProperties`: Object An object that contains additional information about the event. - `userProperties`: Object An object that defines the identity of the user who triggered the event. - `groupProperties`: Object An object that defines properties of the application or environment in which the event occurred. Examples of event properties in Strapi include model, containsRelationalFields, displayedFields, kind. These properties are specific to the event and are used to provide additional context about what happened. User properties can include information such as the user's operating system, node version, and hostname. These properties are typically used to group events by user or to filter events based on certain user characteristics. Group properties can include information such as the language(s) used in the application, the database being used, and the number of locales. These properties are typically used to group events by application version, environment, or other characteristics. --- ## Package map import PackageGraph from '@site/src/components/PackageGraph'; import PackageTable from '@site/src/components/PackageTable'; The map shows every workspace package under `packages/` and the dependencies between them. It is generated at build time from the root `package.json` workspaces and each package's `package.json`. Only internal dependencies are drawn: production by default, dev and peer on demand. ### All packages The same data as a table. _Deps_ counts the internal production dependencies of a package; _Used by_ counts the internal packages that depend on it in production. --- ## Server lifecycle This page describes the boot sequence of the server runtime in `@strapi/core`. Read it before you add code to an internal provider, a loader, or a `register` or `bootstrap` function. ### Phases at a glance ```mermaid flowchart TD A["createStrapi()"] --> B["new Strapi(opts)"] B --> B1["loadConfiguration()"] B1 --> B2["registerInternalServices()"] B2 --> B3["provider.init() for each provider"] B3 --> C["strapi.start()"] C --> D["load()"] D --> E["register()"] E --> F["bootstrap()"] F --> G["listen()"] G --> H["postListen()"] H -.->|SIGINT or SIGTERM| I["destroy()"] ``` Three kinds of participants receive lifecycle calls: 1. **Internal providers.** `Strapi` calls them directly, in the order of the `providers` array. 2. **Modules.** Each plugin and each API is a module. `Strapi` calls them through the `modules` registry. 3. **The application.** The loader imports `dist/src/index.js` into `strapi.app`. `Strapi` calls its `register`, `bootstrap` and `destroy` functions if they exist. ### Construction `createStrapi(options)` resolves the application and dist directories and calls `new Strapi(...)`. Then it attaches the signal handlers (`destroyOnSignal`) and assigns the instance to `global.strapi`. The constructor does three things, in this order: 1. It loads the configuration into `strapi.internal_config`. 2. It calls `registerInternalServices()`. This adds the core services to the container, for example `config`, `logger`, `server`, `eventHub`, `documents` and `db`. Most entries are factories. The container calls a factory on the first `get`. For example, the container creates the `Database` instance on the first access to `strapi.db`. 3. It calls `provider.init?.(strapi)` for each internal provider. `init` is synchronous. Construction does not read plugins, APIs or content types. ### Internal providers A provider is an object with optional `init`, `register`, `bootstrap` and `destroy` functions. The `providers` array sets the call order for every phase. | Order | Provider | `init` | `register` | `bootstrap` | `destroy` | | ----- | ------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------- | | 1 | `registries` | Adds the 14 registries to the container. | Runs `loadApplicationContext()`. Creates the two content-type sync hooks and adds core handlers to them. Adds an internal database migration. | | | | 2 | `admin` | Adds `admin`, a factory that requires `@strapi/admin/strapi-server`. | Loads admin artifacts under `admin::`, then calls the admin `register`. | Calls the admin `bootstrap`. | Calls the admin `destroy`. | | 3 | `ai` | Adds `ai`. | | | | | 4 | `contentStructure` | Adds `content-structure`. | | Validates the groups file and logs the group count. | | | 5 | `coreStore` | Adds the core store model to the `models` registry. Adds `coreStore`. | | | | | 6 | `sessionManager` | Adds `sessionManager`. | | Throws if `admin.auth.secret` is missing, unless `admin.serveAdminPanel` is `false`. | | | 7 | `webhooks` | Adds the webhook model to `models`. Adds `webhookStore` and `webhookRunner`. | | Reads the webhooks from the database and adds each one to the runner. | | | 8 | `telemetry` | Adds `telemetry`. | Calls `telemetry.register()`. | Calls `telemetry.bootstrap()`. | Calls `telemetry.destroy()`. | | 9 | `cron` | Adds `cron`. | | Adds `server.cron.tasks` if `server.cron.enabled` is not `false`. Starts the cron service. | Calls `cron.destroy()`. | | 10 | `mcp` | Adds `ai.mcp`. | | Starts the MCP server if it is enabled. Logs errors and does not throw. | Stops the MCP server if it runs. | The order has effects. `coreStore` and `webhooks` call `strapi.get('models')` in `init`. This works only because `registries` is first in the array. ### Register phase `strapi.register()` runs these steps: 1. `strapi.ee.init(...)` initializes the Enterprise Edition helper. 2. Each provider runs `register`, in array order. The `registries` provider runs the loaders. The `admin` provider loads the admin artifacts and runs the admin `register`. 3. `strapi.get('modules').register()` runs the `register` function of each module, one after the other, in insertion order. 4. `strapi.app.register({ strapi })` runs, if the application defines it. 5. `convertCustomFieldType()` replaces each custom field attribute type with its underlying type. At the end of this phase, the registries are full. The database is not ready. `strapi.db` exists, but it has no models. `strapi.db.query(uid)` throws `Model not found`. The HTTP routes do not exist yet. #### Loaders `loadApplicationContext()` starts eight loaders with `Promise.all`: | Loader | Reads | Writes to | | ------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `src-index` | `dist/src/index.js`. Only `register`, `bootstrap` and `destroy` keys are valid. | `strapi.app` | | `sanitizers` | Nothing | `sanitizers` registry: empty `content-api` lists for `input`, `output` and `query` | | `validators` | Nothing | `validators` registry: empty `content-api` lists for `input` and `query` | | `plugins` | Enabled plugins, `config/plugins.js`, `src/extensions` | `plugins` registry, which creates `plugin::` modules | | `apis` | `dist/src/api/*` | `apis` registry, which creates `api::` modules | | `components` | `dist/src/components//.json` | `components` registry, with UID `.` | | `middlewares` | `dist/src/middlewares/*.js` and the internal middlewares | `middlewares` registry, under `global::` and `strapi::` | | `policies` | `dist/src/policies/*.js` | `policies` registry, under `global::` | When the `plugins` and `apis` loaders add a module, the `modules` registry creates it and calls `module.load()` at once. `load()` adds the module content types, services, policies, middlewares and controllers to their registries under the module namespace. It also stores the module config under the namespace. Routes stay on the module object. `server.initRouting()` reads them in the bootstrap phase. :::caution The loaders run at the same time. Each loader awaits file-system calls before it adds entries. The `plugins` and `apis` loaders add modules to the same `modules` registry, and the code does not set an order between them. The `modules` registry runs `register`, `bootstrap` and `destroy` in insertion order. Do not write a plugin that must run before or after an API module. ::: All loaders finish before the first module `register` runs. Thus, in `register`, a plugin can read all content types, from plugins and from APIs. The i18n plugin uses this: its `register` adds the `locale` and `localizations` attributes to every content type. The `admin` `register` runs before any plugin or API `register`, because providers run before modules. ### Bootstrap phase `strapi.bootstrap()` runs these steps: 1. It configures the global HTTP proxy from `server.proxy`. 2. It builds the database models from all content types and components, plus the entries of the `models` registry. 3. It calls `strapi.db.init({ models })`. 4. It reads the previous content-type schemas from the core store, if the core store table exists. 5. It calls the `strapi::content-types.beforeSync` hook with `{ oldContentTypes, contentTypes }`. 6. It calls `strapi.db.schema.sync()`. If the schema changed, it removes orphan morph types. 7. It runs a one-time repair of unidirectional join tables. A core store flag records that the repair ran. 8. On Enterprise Edition, it checks the license. 9. It calls the `strapi::content-types.afterSync` hook. 10. It saves the current content-type schemas in the core store. 11. It calls `server.initMiddlewares()` and `server.initRouting()`. 12. It registers the Content API permission actions. 13. It runs the `bootstrap` function of each module. 14. It runs the `bootstrap` function of each provider. 15. It runs `strapi.app.bootstrap({ strapi })`, if the application defines it. :::note The sync hooks run in steps 5 and 9, before module `bootstrap`. Add sync hook handlers in `register`. A handler that a module adds in `bootstrap` does not run during the current boot. ::: Module `bootstrap` runs before provider `bootstrap`. Thus, when a plugin `bootstrap` runs, the webhook runner has not read the stored webhooks, the cron service has not started, and the admin `bootstrap` has not run. ### Start and listen `strapi.start()` calls `load()` if `strapi.isLoaded` is `false`. Then it calls `listen()`. `listen()` uses `server.socket` if it is set. Otherwise it uses `server.host` and `server.port`. After the server listens, `postListen()` logs the startup message and sends the `didStartServer` telemetry event without awaiting it. In development, it opens the admin panel in a browser, unless `admin.autoOpen` is `false` or an admin user already exists. If `start()` catches an error, it calls `stopWithError()`. This logs the error and calls `stop()`. `stop()` calls `destroy()` without awaiting it, sends `stop` to the parent process when `autoReload` is on, and calls `process.exit(1)`. ### Destroy phase `strapi.destroy()` runs these steps: 1. The `destroy` function of each module. 2. The `destroy` function of each provider. 3. `strapi.app.destroy({ strapi })`, if the application defines it. 4. `server.destroy()`. 5. `eventHub.destroy()`, which removes all listeners and subscribers. 6. `db.destroy()`. 7. `process.removeAllListeners()`. 8. `delete global.strapi`. This lets a test create a new instance. `destroyOnSignal` calls `destroy()` on the first `SIGTERM` or `SIGINT` and then calls `process.exit()`. It ignores the next signals. ### Idempotency guards - Each module records which lifecycle functions ran. A second call to `register`, `bootstrap` or `destroy` on the same module throws, for example `Register for plugin::i18n has already been called`. - `start()` skips `load()` when `isLoaded` is `true`. `load()` has no guard of its own. A second `load()` throws from the module guards. - The `modules`, `plugins` and `apis` registries throw when a name is added twice. - Provider functions have no guard. ### Code map | Concern | Path | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Phases, `start`, `load`, `register`, `bootstrap`, `destroy` | [`packages/core/core/src/Strapi.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/Strapi.ts) | | `createStrapi()` | [`packages/core/core/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/index.ts) | | Signal handling | [`packages/core/core/src/utils/signals.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/utils/signals.ts) | | Provider type | [`packages/core/core/src/providers/provider.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/provider.ts) | | Provider order | [`packages/core/core/src/providers/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/index.ts) | | Registries provider | [`packages/core/core/src/providers/registries.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/registries.ts) | | Loaders entry | [`packages/core/core/src/loaders/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/index.ts) | | Module wrapper and guards | [`packages/core/core/src/domain/module/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/domain/module/index.ts) | | Module fan-out | [`packages/core/core/src/registries/modules.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/modules.ts) | | Directory layout (`dist/src/...`) | [`packages/core/core/src/configuration/get-dirs.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/configuration/get-dirs.ts) | | Content types to database models | [`packages/core/core/src/utils/transform-content-types-to-models.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/utils/transform-content-types-to-models.ts) | | Middleware and route setup | [`packages/core/core/src/services/server/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/server/index.ts) | ### Related - [Architecture overview](./index.md) - [Container and registries](./03-container-and-registries.md) - [Extension points](./04-extension-points.md) - [`@strapi/core` package](../packages/core/core/index.md) - [`@strapi/database` package](../packages/core/database/index.md) --- ## Container and registries The `Strapi` instance is a container. The container holds core subsystems and registries. Registries hold the artifacts that plugins, APIs and the application define. This page describes both layers and the UID rules. ### The container `Strapi` extends `Container`. The container is a small service locator with three methods. | Method | Behavior | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `add(name, resolver)` | Stores the resolver under `name`. Throws `Cannot register already registered service ` if `name` exists. Returns the container, so calls can chain. | | `get(name, args?)` | Returns the cached value if `name` was resolved before. Otherwise, if the resolver is a function, calls `resolver(container, args)` once and caches the result. If the resolver is not a function, caches it as the value. Throws `Could not resolve service ` if `name` is unknown. | | `has(name)` | Returns `true` if `name` is registered or resolved. | The container has no `set` and no `remove`. After `add`, nothing can replace an entry. Every resolved value is a singleton. A `TODO` comment in `container.ts` notes that there is no per-call instantiation. :::caution The container always calls a function resolver. To store a function as the value, wrap it in a factory: `strapi.add('myFn', () => myFn)`. ::: Controlled replacement happens one level down, in the registries. See [Registry operations](#registry-operations). #### Container keys | Origin | Keys | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Strapi.registerInternalServices()` | `config`, `query-params`, `content-api`, `content-api-schema-registry`, `auth`, `server`, `fs`, `eventHub`, `startupLogger`, `logger`, `fetch`, `features`, `requestContext`, `customFields`, `entityValidator`, `entityService`, `documents`, `db`, `reload`, `content-source-maps` | | `registries` provider | `content-types`, `components`, `services`, `policies`, `middlewares`, `hooks`, `controllers`, `modules`, `plugins`, `custom-fields`, `apis`, `models`, `sanitizers`, `validators` | | Other internal providers | `admin`, `ai`, `content-structure`, `coreStore`, `sessionManager`, `webhookStore`, `webhookRunner`, `telemetry`, `cron`, `ai.mcp` | | `@strapi/admin` server `register` | `ai.admin` | The key style is mixed: camelCase (`eventHub`), kebab-case (`content-types`) and dotted (`ai.mcp`). The container treats a dot as a normal character. Many `strapi.*` getters read a container key, and some names differ: | Getter | Container key | | --------------------- | -------------------------------------------------------------- | | `strapi.log` | `logger` | | `strapi.store` | `coreStore` | | `strapi.contentAPI` | `content-api` | | `strapi.customFields` | `customFields` (a service), not `custom-fields` (the registry) | | `strapi.ai.mcp` | `ai.mcp` | Some getters read a registry instead: `strapi.services`, `strapi.controllers`, `strapi.contentTypes`, `strapi.components`, `strapi.policies`, `strapi.middlewares`, `strapi.plugins`, `strapi.hooks` and `strapi.apis` call `getAll()` on the matching registry. ### Registries The `registries` provider adds 14 registries to the container in its `init` step. `strapi.get('')` returns the registry object. | Key | Stores | Key format | Methods | | --------------- | ---------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------- | | `content-types` | Content-type schemas | `.` or `admin::` | `keys`, `get`, `getAll(ns)`, `set`, `add(ns, map)`, `extend` | | `components` | Component schemas | `.` | `keys`, `get`, `getAll()`, `set`, `add(map)` | | `services` | Service factories and instances | namespaced UID | `keys`, `get`, `getAll(ns)`, `set`, `add(ns, map)`, `extend` | | `controllers` | Controller factories and instances | namespaced UID | `keys`, `get`, `getAll(ns)`, `set`, `add(ns, map)`, `extend` | | `policies` | Policies | namespaced UID | `keys`, `get`, `has`, `getAll(ns)`, `set`, `add(ns, map)`, `resolve` | | `middlewares` | Middleware factories | namespaced UID | `keys`, `get`, `getAll(ns)`, `set`, `add(ns, map)`, `extend` | | `hooks` | Hook objects | namespaced UID | `keys`, `get`, `getAll(ns)`, `set`, `add(ns, map)`, `extend` | | `modules` | Runtime modules (plugins and APIs) | `plugin::`, `api::` | `get`, `getAll(prefix)`, `add`, `register`, `bootstrap`, `destroy` | | `plugins` | Plugin modules | `` | `get`, `getAll`, `add` | | `apis` | API modules | `` | `get`, `getAll`, `add` | | `custom-fields` | Custom field definitions | `plugin::.` or `global::` | `get`, `getAll`, `add` | | `models` | Extra database models (core store, webhooks, others) | none, it is a list | `add`, `get` | | `sanitizers` | Sanitizer lists by path | lodash path, for example `content-api.input` | `get`, `add`, `set`, `has` | | `validators` | Validator lists by path | lodash path | `get`, `add`, `set`, `has` | #### Registry operations The method names are shared, but the behavior differs per registry. - **`add`** registers many entries under one namespace. Most registries throw on a duplicate UID. The `hooks` registry does not check duplicates: its `add` calls `set`. The `components` registry takes a map keyed by full UID, with no namespace argument. - **`get`** returns one entry. For `services` and `controllers`, the first `get` calls the factory with `{ strapi }` and caches the instance. For an unknown UID, most registries return `undefined`. `custom-fields` throws. `sanitizers` and `validators` return an empty array. The `policies` registry also accepts `{ pluginName, apiName }` and tries `plugin::.` or `api::.` when the full name does not match. - **`getAll(namespace)`** filters by namespace. For `services` and `controllers`, it returns lazy getters, so it does not instantiate anything. - **`set`** writes one entry and overwrites the old one. For `services` and `controllers`, it also removes the cached instance. The `components` registry is the exception: its `set` throws on a duplicate. - **`extend(uid, fn)`** throws if the UID is unknown. For `services` and `controllers`, it calls `get` (which instantiates the factory), then caches `fn(instance)` as the new instance. The factory does not change. For `middlewares` and `hooks`, it replaces the stored value with `fn(value)`. For `content-types`, it calls `fn(schema)` and ignores the return value, so `fn` must change the schema in place. :::caution For `services` and `controllers`, `set` after `extend` drops the extension, because `set` removes the cached instance. ::: :::caution In the `sanitizers` and `validators` registries, `add(path, fn)` pushes to the list at `path`. If no list exists at `path`, `get` returns a new empty array that is not stored, and the entry is lost. The loaders create the `content-api` lists before any plugin code runs. ::: ### Namespaces and UIDs `registries/namespace.ts` builds UIDs with one rule: - If the namespace ends with `::`, the name follows it directly: `global::` + `is-owner` gives `global::is-owner`. - Otherwise, a dot separates them: `plugin::upload` + `file` gives `plugin::upload.file`. `@strapi/types` encodes the same rule. Scoped namespaces (`api::`, `plugin::`) use `.`. The other namespaces (`admin`, `strapi`, `global`) use `::`. | Artifact | Example UID | | -------------------------------- | ---------------------------------------------------------------------------- | | API content type or service | `api::article.article` | | Plugin content type | `plugin::upload.file` | | Plugin service | `plugin::users-permissions.user` | | Admin content type | `admin::user` | | Component | `shared.seo` (category and name, no namespace) | | Application policy or middleware | `global::is-owner` | | Internal middleware | `strapi::cors` | | Core hook | `strapi::content-types.beforeSync` | | Custom field | `plugin::color-picker.color`, or `global::` when the plugin is unknown | A module reads its own artifacts without the namespace. `strapi.plugin('upload').service('upload')` reads `plugin::upload.upload` from the `services` registry. `module.services` returns the map with the namespace removed from the keys. The content-types registry also checks that each key equals the schema `info.singularName`. It throws if they differ. ### Folder roles in `@strapi/core` | Folder | Role | Signal | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `src/providers/` | Internal lifecycle units. They add subsystems to the container and take part in `register`, `bootstrap` and `destroy`. | Exports `init`, `register`, `bootstrap` or `destroy`. | | `src/registries/` | Stores for named artifacts, with namespace rules and replacement methods. | Exports `add`, `get`, `getAll`, and sometimes `set` or `extend`. | | `src/loaders/` | Read the file system and the enabled plugins, then fill the registries. | Called from `loadApplicationContext()`. | | `src/domain/` | Wrappers that turn raw input into runtime objects: `module` (raw module to runtime module) and `content-type` (definition to schema). | Called by registries. | | `src/services/` | Implementations of core subsystems: Document Service, event hub, webhooks, HTTP server, entity validator, Content API helpers and others. | Mounted in the container by `Strapi` or by a provider, or imported directly. | :::caution "Services" has two meanings. The `services` registry (`src/registries/services.ts`) holds the services of plugins, APIs and the application. You read it with `strapi.service(uid)`. The folder `src/services/**` holds core implementation code. Most of it is mounted in the container or imported directly. It is not in the `services` registry. ::: ### Code map | Concern | Path | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Container | [`packages/core/core/src/container.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/container.ts) | | Internal services and getters | [`packages/core/core/src/Strapi.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/Strapi.ts) | | Registries added to the container | [`packages/core/core/src/providers/registries.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/registries.ts) | | Registry list | [`packages/core/core/src/registries/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/index.ts) | | Namespace rule | [`packages/core/core/src/registries/namespace.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/namespace.ts) | | Services registry | [`packages/core/core/src/registries/services.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/services.ts) | | Content-types registry | [`packages/core/core/src/registries/content-types.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/content-types.ts) | | Policies registry | [`packages/core/core/src/registries/policies.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/policies.ts) | | Custom-fields registry | [`packages/core/core/src/registries/custom-fields.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/custom-fields.ts) | | Module wrapper | [`packages/core/core/src/domain/module/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/domain/module/index.ts) | | Content-type wrapper | [`packages/core/core/src/domain/content-type/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/domain/content-type/index.ts) | | Admin artifacts under `admin::` | [`packages/core/core/src/loaders/admin.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/admin.ts) | | Namespace types | [`packages/core/types/src/internal/namespace.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/types/src/internal/namespace.ts) | | Container type | [`packages/core/types/src/core/container.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/types/src/core/container.ts) | ### Related - [Server lifecycle](./02-server-lifecycle.md) - [Extension points](./04-extension-points.md) - [Glossary](./11-glossary.md) - [`@strapi/core` package](../packages/core/core/index.md) --- ## Extension points Plugins and applications change Strapi behavior through a small set of mechanisms. Each mechanism applies at a specific moment of the [server lifecycle](./02-server-lifecycle.md). This page lists them in that order. | Moment | Mechanism | Typical use | | ------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Plugin loading (register phase) | Plugin selection, plugin config merge, `src/extensions` | Turn plugins on, configure them, change a plugin before Strapi creates its module. | | Module load (register phase) | Raw module artifacts go into the registries | A plugin or API declares content types, services, controllers, policies, middlewares and routes. | | `register` functions | Registry `set` and `extend`, content-type changes, sync hook handlers, custom fields | Change schemas or replace artifacts before the database starts. | | `bootstrap` functions | Document middlewares, database lifecycle subscribers, event hub listeners, webhook events | Add behavior that needs the database or the server. | ### Plugin selection The plugins loader builds the list of enabled plugins from three sources: 1. **Always-enabled plugins.** A fixed list in `get-enabled-plugins.ts`: content-manager, content-type-builder, email, upload, i18n, content-releases and review-workflows. A declaration with `enabled: false` in `config/plugins` does not turn them off. 2. **Installed plugins.** Dependencies of the application whose `package.json` has `strapi.kind: 'plugin'`. 3. **Declared plugins.** Entries in `config/plugins.js`, merged with `config/env//plugins.js`. An entry with `resolve` points to a local folder or a module. Plugin names must be kebab-case. The loader throws otherwise. For each enabled plugin, the loader resolves the `strapi-server` export of the package. If the export is missing, it uses `./strapi-server.js`. It skips a plugin that has no server entry. ### Plugin config merge Each plugin can export `config.default` (an object, or a function that receives `{ env }`) and `config.validator`. The loader then: 1. reads `.config` from the user plugin config; 2. merges it with `defaultsDeep`: user values win, and defaults fill the missing keys; 3. calls `config.validator(config)`. If it throws, the loader throws `Error regarding config: `; 4. stores the result in `plugin.config`. At module load, the config service stores this object under the module namespace. Read it with `strapi.plugin('').config('')` or `strapi.config.get('plugin::.')`. ### The `src/extensions` folder After the config merge, and before Strapi creates the plugin module, the loader applies application overrides from `/src/extensions`: 1. **Schema overrides.** `src/extensions//content-types//schema.json` is merged into the plugin content type with a shallow spread (`{ ...original, ...override }`). A top-level key such as `attributes` replaces the original key as a whole. If the content type does not exist, the loader adds it. 2. **Server override.** `src/extensions//strapi-server.js` exports a function. The loader calls it with the plugin object and uses the return value as the new plugin object. This mechanism applies to plugins only. It does not apply to APIs or to the admin. Because the override runs before module creation, every later step sees the changed plugin: registries, lifecycles and routes. ### Registry replacement In `register` or `bootstrap`, code can replace or wrap entries in the registries. See [Container and registries](./03-container-and-registries.md#registry-operations) for the exact rules. | Registry | Replace | Wrap | | ------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------- | | `services`, `controllers` | `set(uid, factory)` replaces the factory and clears the instance. | `extend(uid, fn)` instantiates the service and caches `fn(instance)`. | | `middlewares`, `hooks` | `set(uid, value)` | `extend(uid, fn)` stores `fn(value)`. | | `content-types` | `set(uid, schema)` | `extend(uid, fn)` calls `fn(schema)`. `fn` must change the schema in place. | | `policies` | `set(uid, policy)` | Not supported. | | `components` | Not supported. `set` throws on a duplicate. | Not supported. | Content types can also change without the registry methods. In `register`, the database is not initialized, so changes to `strapi.contentTypes[uid].attributes` reach the database schema. The i18n plugin adds the `locale` and `localizations` attributes to every content type this way. ### Hooks registry A hook is a named list of handlers. `strapi.hook(uid)` returns the hook object from the `hooks` registry, or `undefined`. Hook objects come from the `hooks` module of `@strapi/utils`. Each object has `register(handler)`, `delete(handler)`, `getHandlers()` and `call(...)`. The factory sets how `call` runs the handlers: | Factory | `call` behavior | | -------------------------------- | ---------------------------------------------------------------------------------------------- | | `createAsyncSeriesHook` | Runs handlers one after the other with the same context. | | `createAsyncSeriesWaterfallHook` | Runs handlers one after the other. Each handler receives the return value of the previous one. | | `createAsyncParallelHook` | Runs all handlers at the same time. Each handler receives a deep clone of the context. | | `createAsyncBailHook` | Runs handlers one after the other and returns the first result that is not `undefined`. | Core defines two hooks. Both are parallel hooks. The `registries` provider creates them in its `register` step. - `strapi::content-types.beforeSync` runs before the database schema sync. - `strapi::content-types.afterSync` runs after it. Both receive `{ oldContentTypes, contentTypes }`. Core uses them to migrate data when draft and publish or i18n is turned on or off for a content type. The i18n, content-releases and review-workflows plugins also add handlers. Add handlers in `register`, because the hooks run before module `bootstrap`. :::caution The local `Hook` type in `registries/hooks.ts` declares a `handlers` property. The objects from `@strapi/utils` expose `getHandlers()` instead. ::: :::note The admin panel has its own hooks, created in `StrapiApp` and registered with `registerHook`, for example `Admin/CM/pages/ListView/inject-column-in-table`. They run in the browser. They are not related to the server hooks registry. ::: ### Database lifecycles `@strapi/database` runs lifecycle subscribers around each entity manager query: `create`, `createMany`, `update`, `updateMany`, `delete`, `deleteMany`, `findOne`, `findMany` and `count`. Each query has a `before*` and an `after*` action. There are two ways to add a subscriber: - `strapi.db.lifecycles.subscribe(subscriber)`. A function subscriber receives all events. An object subscriber has action keys (`beforeCreate`, `afterUpdate` and so on) and an optional `models` list of UIDs. - A `lifecycles` file next to a content-type schema, for example `src/api/article/content-types/article/lifecycles.js`. The built-in models subscriber calls it. A second built-in subscriber sets `createdAt` and `updatedAt`. Each subscriber gets its own `event.state` object, which carries data from the `before*` call to the `after*` call of the same query. `strapi.db.lifecycles.disable()` and `enable()` turn all subscribers off and on. The data-transfer package uses them. ### Document Service middlewares `strapi.documents.use(middleware)` adds a middleware to the Document Service. A middleware is `(ctx, next) => result`. `ctx` holds `uid`, `contentType`, `action` (the method name, for example `create` or `publish`) and `params`. The function returns an unregister function. - Middlewares run for every content type. Filter on `ctx.uid` and `ctx.action`. - A middleware can change `ctx.params` before it calls `next()`, and it can change or replace the result. - Middlewares run before the repository opens its transaction. - The i18n, upload, content-manager (history), content-releases and review-workflows packages add middlewares. Read [Document write path](./05-document-write-path.md) for where middlewares sit in a write. ### Event hub listeners `strapi.eventHub.on(name, listener)` adds a listener for one event. `strapi.eventHub.subscribe(fn)` adds a subscriber that receives every event as `(name, ...args)`. `emit` awaits each subscriber and each listener in order. It does not catch errors: if one throws, the next ones do not run for that event. Core emits the document events after the transaction commits. See [Document write path](./05-document-write-path.md#events-and-webhooks). To offer an event to webhooks, a plugin calls `strapi.get('webhookStore').addAllowedEvent(key, value)`, usually in `bootstrap`. :::caution Several unsubscribe helpers use `list.splice(list.indexOf(item), 1)`. If `item` is not in the list, `indexOf` returns `-1` and `splice(-1, 1)` removes the last entry. This applies to `eventHub.off()`, to the function that `strapi.documents.use()` returns and to the function that `strapi.db.lifecycles.subscribe()` returns. `eventHub.once(name, listener)` registers a wrapper, but the wrapper calls `off(name, listener)` with the original listener. The original is not in the list, so the call removes the last listener for that event. If the wrapper is the last listener, it removes itself and the call looks correct. If another listener was added after it, that listener is removed and the wrapper stays. A local run of the code confirmed this: with `once` then `on`, two emits called the `once` listener twice and the other listener zero times. ::: ### Three hook-like mechanisms compared | | Hooks registry | Database lifecycles | Document Service middlewares | | ---------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Package | `@strapi/core` registry, hook objects from `@strapi/utils` | `@strapi/database` | `@strapi/core` Document Service | | How to add | `strapi.hook(uid).register(fn)` | `strapi.db.lifecycles.subscribe(...)` or a content-type `lifecycles` file | `strapi.documents.use(fn)` | | Trigger | An explicit `hook.call()` by the owner. Core calls each sync hook once per boot. | Each entity manager query | Each Document Service method call | | Filter | One hook per UID | By action, and optionally by model UID | None. Check `ctx.uid` and `ctx.action`. | | Input | Depends on the hook. Core sync hooks pass `{ oldContentTypes, contentTypes }`. | Database params with row `id` values | Document params with `documentId`, `status` and `locale` | | Change input | Parallel hooks pass a clone, so no. Waterfall hooks pass return values forward. | Yes. Change `event.params` in `before*`. | Yes. Change `ctx.params` before `next()`. | | Change output | Waterfall and bail hooks return a value. | Change the `event.result` object in `after*`. It is the object the caller receives. | Yes. Return a different value. | | Transaction | Not related to transactions | Inside the caller transaction, if one exists | Outside the repository transaction, unless the caller opened one | | Calls per document operation | None | Many. For example, a publish deletes and creates rows, and each create runs a `findOne`. | One per public method call | ### Code map | Concern | Path | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Enabled plugins | [`packages/core/core/src/loaders/plugins/get-enabled-plugins.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/plugins/get-enabled-plugins.ts) | | User plugin config files | [`packages/core/core/src/loaders/plugins/get-user-plugins-config.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/plugins/get-user-plugins-config.ts) | | Config merge and `src/extensions` | [`packages/core/core/src/loaders/plugins/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/plugins/index.ts) | | Hooks registry | [`packages/core/core/src/registries/hooks.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/hooks.ts) | | Hook factories | [`packages/core/utils/src/hooks.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/utils/src/hooks.ts) | | Sync hooks creation | [`packages/core/core/src/providers/registries.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/registries.ts) | | Sync hook core handlers | [`packages/core/core/src/migrations/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/migrations/index.ts) | | Database lifecycles | [`packages/core/database/src/lifecycles/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/lifecycles/index.ts) | | Built-in subscribers | [`packages/core/database/src/lifecycles/subscribers/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/lifecycles/subscribers/index.ts) | | Document middleware manager | [`packages/core/core/src/services/document-service/middlewares/middleware-manager.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/middlewares/middleware-manager.ts) | | Event hub | [`packages/core/core/src/services/event-hub.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/event-hub.ts) | | Webhook allowed events | [`packages/core/core/src/services/webhook-store.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/webhook-store.ts) | | Example: i18n `register` | [`packages/plugins/i18n/server/src/register.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/i18n/server/src/register.ts) | ### Related - [Server lifecycle](./02-server-lifecycle.md) - [Container and registries](./03-container-and-registries.md) - [Document write path](./05-document-write-path.md) - [Glossary](./11-glossary.md) --- ## Document write path This page follows one write through the server. It explains the layers and the order in which they run. It is not an API reference for the Document Service. ### Documents and rows A document is one logical entry. In the database, one document can have many rows in the content-type table. - `documentId` identifies the document. All rows of the document share it. The database layer generates it with `createId` from `@paralleldrive/cuid2`, as the default value of the `documentId` attribute. - `id` identifies one row. A row `id` can change: each publish deletes the old published rows and creates new ones. - `publishedAt` separates versions. On a content type with draft and publish (D&P), `publishedAt: null` marks the draft row and a date marks the published row. Without D&P, every row has a `publishedAt` date and there are no drafts. - `locale` separates languages. The i18n plugin adds a `locale` attribute to every content type. It is private when the content type is not localized. A localized content type with D&P can have one draft row and one published row per locale. When the data of a new row already has a `documentId` (for example a new locale or a publish), the entries service looks for an existing row with the same `documentId`, the same locale (if localized) and the same state (draft or published, if D&P). It throws an `ApplicationError` if it finds one. :::note Use `documentId` for stable references. Do not store a row `id` of a D&P content type, because publish replaces the published row. ::: ### The layers | Order | Layer | Code | What it does | | ----- | --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | Factory | `document-service/index.ts` | `strapi.documents(uid)` returns one cached instance per content type. The instance wraps the repository with the middleware manager. | | 2 | Middlewares | `document-service/middlewares/` | Runs the built-in `databaseErrorsMiddleware` first, then the middlewares added with `strapi.documents.use()`, in order. | | 3 | Repository | `document-service/repository.ts` | Each public method runs in `strapi.db.transaction()`. It turns the params into an internal query and calls the entries service. It schedules events. | | 4 | Entries service | `document-service/entries.ts` | Resolves relation `documentId` values to row `id` values, checks uniqueness, runs the entity validator, writes components, then calls `strapi.db.query(uid)`. | | 5 | Entity manager | `@strapi/database` `entity-manager/` | Runs the database lifecycles, applies attribute defaults, inserts or updates the row, attaches relations and reads the row back. | | 6 | Event manager | `document-service/events.ts` | After the commit, reads the row with a deep populate, sanitizes it and emits the event on the event hub. | | 7 | Webhook runner | `services/webhook-runner.ts` | Queues the event and sends an HTTP `POST` to each enabled webhook for that event. | The built-in middleware converts invalid date, time, datetime and relation errors from the database layer into `ValidationError`. ### A create, step by step ```mermaid sequenceDiagram participant Caller participant MW as Document middlewares participant Repo as Repository participant Entries as Entries service participant EM as Entity manager participant TX as Transaction context participant Hub as Event hub participant WH as Webhook runner Caller->>MW: strapi.documents(uid).create(params) MW->>Repo: next() calls create(ctx.params) Note over Repo: strapi.db.transaction() opens Repo->>Repo: param pipeline for status and locale Repo->>Entries: entries.create(queryParams) Entries->>Entries: uniqueness check and entity validator Entries->>EM: strapi.db.query(uid).create(data) EM->>EM: beforeCreate, insert, relations, findOne, afterCreate EM-->>Repo: row Repo->>TX: emitEvent registers an onCommit callback Note over TX: transaction commits, callbacks start Repo-->>MW: document MW-->>Caller: document TX->>Hub: emit entry.create with model, uid, entry Hub->>WH: listener adds a job to the queue WH->>WH: POST to each enabled webhook ``` The middlewares run outside the repository transaction. The transaction opens when the middleware chain calls the repository method. If the caller already runs inside `strapi.db.transaction()`, the repository reuses that transaction. ### Parameter pipeline Each repository method builds its query with a pipeline of small transforms. For `create`, the order is: 1. `validateParams` checks filters, sort, fields and populate against the schema. It rejects a user-provided `lookup`. When `api.documents.strictParams` is `true`, it also checks `status`, `locale` and pagination, and rejects unknown root params. 2. `filterDataPublishedAt` sets `data.publishedAt` to `null` if the caller sent one. Callers cannot set it directly. 3. `setStatusToDraft` forces `status: 'draft'` on D&P content types. 4. `statusToData` writes `publishedAt`: `null` for a draft, the current date for published, and always the current date without D&P. 5. `defaultLocale` sets the default locale from the i18n plugin when the content type is localized and no locale is given. 6. `localeToData` writes the locale into `data` on localized content types. It rejects `'*'`. Read methods use the same idea. `statusToLookup` and `localeToLookup` write conditions into an internal `lookup` object, and `transformParamsToQuery` merges `lookup` into the `where` clause. ### Create, update and publish **`create`** writes one row. On a D&P content type, this is always the draft. The repository schedules `entry.create`. If the caller passed `status: 'published'`, it then calls the inner `publish` function and returns the published row. **`update`** targets the draft row on a D&P content type. It looks for the row with the `documentId` and the requested locale. - If the row exists, the entries service updates it, and the repository schedules `entry.update`. - If the row does not exist, but the document exists in another locale, the repository creates a row for the new locale. It copies the non-localized fields from an existing row. It schedules `entry.create`, not `entry.update`. - If the document does not exist, `update` returns `null`. If the caller passed `status: 'published'` and a row was written, `update` then calls the inner `publish` function. **`publish`** exists only on D&P content types. For the requested locales, it: 1. reads the draft rows (with a deep populate) and the current published rows; 2. loads the relations that point to the current published rows; 3. deletes the current published rows; 4. sets `firstPublishedAt` on each draft if the content type has that field and the value is empty; 5. creates a new published row from each draft, with `publishedAt` set to the current date; 6. points the loaded relations to the new published rows; 7. schedules one `entry.publish` per new row. The draft rows stay. After a publish, the document has a draft row and a published row with the same content. :::caution When `create` or `update` calls the inner `publish` function, the call does not pass through the middlewares again. A middleware sees one call with `action: 'create'` or `action: 'update'`, not a separate `publish` call. ::: The other D&P methods follow the same model. `unpublish` deletes the published rows. `discardDraft` replaces the draft rows with copies of the published rows. `delete` removes all rows of the requested locales and throws if the caller passes `status: 'draft'`. ### Validation The entries service calls the entity validator before each insert or update. The validator builds a Yup schema from the content type. It receives `isDraft`, which is `true` when `data.publishedAt` is empty. For drafts, it skips `required`, `min`, `minLength` and `regex` checks. When `api.documents.strictRelations` is `true`, it also enforces required relations and media on non-draft writes. Database lifecycles run after the entity validator. Data that a `beforeCreate` or `beforeUpdate` subscriber adds is not validated. ### Transactions Each public repository method runs inside `strapi.db.transaction()`. The transaction context uses `AsyncLocalStorage`. A nested call reuses the open transaction and does not commit it. Only the outermost call commits or rolls back. Database lifecycles run inside the transaction. If the transaction rolls back later, a side effect in a lifecycle subscriber, such as an HTTP call, is not undone. ### Events and webhooks The repository does not emit events directly. `emitEvent` calls `strapi.db.transaction(({ onCommit }) => ...)` and registers a callback. The callback runs only after the outermost transaction commits. It does not run on rollback. The transaction context starts the commit callbacks but does not await them. The Document Service call can return before the event reaches the listeners. When the callback runs, the event manager: 1. reads the row again with a deep populate, except for `entry.delete` and `entry.unpublish`, where the row is gone; 2. sanitizes it with `defaultSanitizeOutput`, which removes private fields; 3. calls `strapi.eventHub.emit(eventName, { model, uid, entry })`. | Method | Events | | -------------- | ------------------------------------------------------------------------------------------------- | | `create` | `entry.create`, then `entry.publish` if `status: 'published'` | | `update` | `entry.update`, or `entry.create` for a new locale, then `entry.publish` if `status: 'published'` | | `clone` | `entry.create` per new row | | `delete` | `entry.delete` per deleted row | | `publish` | `entry.publish` per new published row | | `unpublish` | `entry.unpublish` per deleted published row | | `discardDraft` | `entry.draft-discard` per new draft row | The webhook runner adds an event hub listener for an event when the first webhook for that event is registered. The listener only adds a job to an in-memory queue with a concurrency of 5. For each job, the runner sends a `POST` to each enabled webhook. The body is `{ event, createdAt, model, uid, entry }`. The headers are the `server.webhooks.defaultHeaders`, the webhook headers, `X-Strapi-Event` and `Content-Type: application/json`. Each request has a 10-second timeout. The runner does not retry. In the event path, it also does not report failed deliveries: `run()` turns network errors and non-2xx responses into a returned status object, and `executeListener()` does not read that object. When a webhook is created or updated, the webhook store rejects events that are not in its allowed list. Core allows the six `entry.*` events. Plugins add their own events with `addAllowedEvent`. ### Code map | Concern | Path | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Factory and middleware wiring | [`packages/core/core/src/services/document-service/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/index.ts) | | Middleware manager | [`packages/core/core/src/services/document-service/middlewares/middleware-manager.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/middlewares/middleware-manager.ts) | | Database errors middleware | [`packages/core/core/src/services/document-service/middlewares/errors.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/middlewares/errors.ts) | | Repository methods | [`packages/core/core/src/services/document-service/repository.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/repository.ts) | | Transaction wrapper | [`packages/core/core/src/services/document-service/common.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/common.ts) | | D&P transforms | [`packages/core/core/src/services/document-service/draft-and-publish.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/draft-and-publish.ts) | | i18n transforms | [`packages/core/core/src/services/document-service/internationalization.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/internationalization.ts) | | Entries service | [`packages/core/core/src/services/document-service/entries.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/entries.ts) | | Event manager | [`packages/core/core/src/services/document-service/events.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/events.ts) | | Entity validator | [`packages/core/core/src/services/entity-validator/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/entity-validator/index.ts) | | `documentId` default | [`packages/core/core/src/utils/transform-content-types-to-models.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/utils/transform-content-types-to-models.ts) | | Entity manager | [`packages/core/database/src/entity-manager/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/entity-manager/index.ts) | | Transactions | [`packages/core/database/src/transaction-context.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/transaction-context.ts) | | Event hub | [`packages/core/core/src/services/event-hub.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/event-hub.ts) | | Webhook runner | [`packages/core/core/src/services/webhook-runner.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/webhook-runner.ts) | | Webhook store and allowed events | [`packages/core/core/src/services/webhook-store.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/webhook-store.ts) | | Document Service types | [`packages/core/types/src/modules/documents/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/types/src/modules/documents/index.ts) | ### Related - [Extension points](./04-extension-points.md) - [Container and registries](./03-container-and-registries.md) - [Glossary](./11-glossary.md) - [`@strapi/core` package](../packages/core/core/index.md) - [`@strapi/database` package](../packages/core/database/index.md) --- ## HTTP request path This page follows one HTTP request through the server, from the Koa app to the response body. It covers the Content API (`/api/...`) and the admin API. What happens inside the Document Service is in [Document write path](./05-document-write-path.md). Tokens and sessions are in [Sessions and JWT](./08-authentication.md). ### Overview ```mermaid sequenceDiagram participant Client participant G as Global middlewares participant Chain as Route chain participant Auth as Auth strategies participant Ctrl as Controller participant Svc as Service participant DS as Document Service Client->>G: GET /api/articles Note over G: logger, errors, security, cors, body and the others G->>Chain: router matches method and path Chain->>Chain: set ctx.state.route Chain->>Auth: authenticate(ctx) with the strategies of the route type Auth-->>Chain: ctx.state.auth holds strategy, credentials, ability Chain->>Auth: verify(auth, config.auth) Chain->>Chain: policies, then route middlewares Chain->>Ctrl: bound action find(ctx) Ctrl->>Ctrl: validateQuery, sanitizeQuery Ctrl->>Svc: find(sanitizedQuery) Svc->>DS: findMany and count DS-->>Svc: documents Svc-->>Ctrl: results and pagination Ctrl->>Ctrl: sanitizeOutput, transformResponse Ctrl-->>Chain: returns data and meta Chain->>Chain: return-body middleware sets ctx.body Chain-->>G: resolves G-->>Client: JSON body, or data null and error ``` ### The Koa app and global middlewares `createServer` in [`services/server/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/server/index.ts) creates the Koa app with the `server.proxy.*` settings and `server.app.keys`. It adds response helpers such as `ctx.send` and `ctx.notFound` (see [Response and errors](#response-and-errors)). The first middleware stores `ctx` in an `AsyncLocalStorage` (`services/request-context.ts`). Code that has no `ctx` argument can read the current request. For example, the core controller reads the `strapi-response-format` header this way. In the bootstrap phase, `server.initMiddlewares()` reads the `middlewares` config (`config/middlewares.ts` in the app). Each entry is a name (`'strapi::cors'`), an object `{ name, config }`, or an object `{ resolve, config }` that loads a package or a path. Names come from the `middlewares` registry: `strapi::` for internal middlewares, `global::` for `src/middlewares`, and `plugin::` or `api::` for module middlewares. Each factory is called once as `factory(config, { strapi })`, and the result is added with `app.use()` in list order. The new-project template uses this order: | Middleware | What it does | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `strapi::logger` | Logs method, URL, duration and status after the rest of the chain completes. | | `strapi::errors` | Catches the errors of every later middleware and writes the error body. Returns 404 when nothing set a status. | | `strapi::security` | Sets security headers (CSP, HSTS, frame options) with `koa-helmet`. | | `strapi::cors` | Handles CORS with `@koa/cors`. The default origin is `*`. | | `strapi::poweredBy` | Sets the `X-Powered-By` header. | | `strapi::query` | Replaces the Koa query getter with a `qs` parser, so `filters[title][$eq]=x` becomes a nested object. | | `strapi::body` | Parses JSON, form and multipart bodies with `koa-body`. Skips the GraphQL endpoint. Deletes uploaded temp files after the rest of the chain completes. | | `strapi::session` | Adds `koa-session`. Throws at boot if `app.keys` is empty. | | `strapi::favicon` | Serves the favicon. | | `strapi::public` | Registers two routes: `GET /` redirects to the admin URL, and a `GET` route serves the `public` folder (except `/uploads`). | Without a `middlewares` config, Strapi uses the same list with `strapi::session` before `strapi::query`. The boot fails if `strapi::errors`, `strapi::security`, `strapi::cors`, `strapi::query`, `strapi::body`, `strapi::public` or `strapi::favicon` is missing. `strapi::compression`, `strapi::ip`, `strapi::responseTime` and `strapi::responses` exist but are not in the default list. :::note `strapi::query`, `strapi::session` and `strapi::public` return no handler, so the list gets a pass-through handler at their position. `strapi::query` patches `app.request`. `strapi::public` registers routes. `strapi::session` calls `strapi.server.use()` while the list is resolved, which is before the listed handlers are added. Its position in the list does not change where `koa-session` runs. ::: The Koa order is the order of the `app.use()` calls: the request context, then any `strapi.server.use()` call from a `register` function, then the configured list, then any `strapi.server.use()` call from a `bootstrap` function. `listen()` mounts the router after all of these. A middleware added in a `register` function runs before `strapi::errors`, so `strapi::errors` does not catch its errors. ### Routers and route registration The server has one root router and two API routers. `listen()` calls `mount()`, which attaches both API routers to the root router and adds the root router to the Koa app. - The content API router has the prefix `api.rest.prefix` (default `/api`). - The admin API router has no prefix. Each admin router sets its own prefix. - The root router holds `/_health` (always 204) and routes passed as an array to `strapi.server.routes()`, such as the `strapi::public` routes. In the bootstrap phase, `server.initRouting()` calls [`register-routes.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/server/register-routes.ts). It registers admin routes, then API routes, then plugin routes, then the OpenAPI route. A router can be an object or a factory. Strapi calls a factory with `{ strapi }` and writes the result back on the module. Then `strapi.server.routes(router)` sends the router to the API router that matches `router.type`. | Source | `type` | Router prefix | Example | | ---------------------------------------- | -------------------- | --------------------------- | ---------------------------------------------- | | Admin package (`strapi.admin.routes`) | `admin` if not set | `/admin` if not set | `POST /admin/login` | | App API (`src/api//routes`) | always `content-api` | the router `prefix`, if any | `GET /api/articles` | | Plugin router with `type: 'content-api'` | `content-api` | `/` if not set | `GET /api/upload/files` | | Plugin router without `type` | `admin` | `/` if not set | `GET /content-manager/collection-types/:model` | Plugin routes given as a plain array get the type `admin` and the prefix `/`. A route that has a `config.prefix` key skips the router prefix and goes on the API router directly. The users-permissions plugin sets `prefix: ''` on its auth routes, so the login route is `POST /api/auth/local`, not `/api/users-permissions/auth/local`. For each route, the registration step also: - sets `route.info` to `{ apiName }` or `{ pluginName }` (`pluginName: 'admin'` for the admin package); - generates `config.auth.scope` when the handler is a string (see [Route config](#route-config)); - for API and plugin routes, merges the extra params from `strapi.contentAPI.addQueryParams()` and `addInputParams()` into `route.request`. The route manager in [`routing.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/server/routing.ts) validates each route with a Yup schema and adds `info.type` from the API router (`api` on the root router). An invalid route stops the boot with `Invalid route config`. An unknown handler, policy or middleware stops it with `Error creating endpoint `. ### Route config | Key | Meaning | | --------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `method`, `path` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `ALL`) and Koa router path. | | `handler` | A string `'.'`, a full UID such as `'api::article.article.find'`, a function, or an array of functions. | | `config.auth` | `false` for a public route, or `{ scope?, strategies? }`. | | `config.policies` | Policy names or `{ name, config }` objects. | | `config.middlewares` | Middleware UIDs, `{ name, config }` objects, or functions. | | `request`, `response` | Zod schemas. See [Validation and sanitization](#validation-and-sanitization). | For a string handler, the generated scope is the handler prefixed with the module namespace, unless the handler already starts with it. In `src/api/article`, `article.find` gets the scope `api::article.article.find`. In the admin package, `admin.init` gets `admin::admin.init`. A scope set in the route wins. The scope has the same format as a Content API permission action, which is how the permission check matches a route. ### The route chain [`compose-endpoint.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/server/compose-endpoint.ts) builds one `koa-compose` chain per route, in this order: 1. The route info middleware copies the route to `ctx.state.route`. 2. `authenticate` runs the auth strategies. 3. `authorize` calls `verify` on the strategy that authenticated the request. 4. The policies middleware runs each policy in order. 5. The route middlewares run in order. 6. The return-body middleware waits for the controller and sets `ctx.body` from its return value if `ctx.body` is still empty. 7. The controller action runs. Strapi resolves the policies, the route middlewares and the controller action when it registers the route, not per request. Configuration errors show at boot, and the route chain does no registry lookup per request. ### Authentication and authorization The `auth` container service ([`services/auth/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/auth/index.ts)) stores strategies by route type. A strategy has a `name`, an `authenticate(ctx)` function and an optional `verify(auth, config)` function. `authenticate` does this: - If `config.auth` is `false`, it calls the next middleware and sets nothing. - Otherwise it tries the strategies registered for `route.info.type`, in registration order. `config.auth.strategies` can restrict them by name. - If a strategy returns `authenticated: true`, it sets `ctx.state.isAuthenticated` and `ctx.state.auth = { strategy, credentials, ability }` and stops. - If a strategy returns an `error`, it responds 401 with that error at once. - If no strategy authenticates, it responds 401 `Missing or invalid credentials`. `verify` skips routes with `auth: false`, throws `UnauthorizedError` when `ctx.state.auth` is empty, and otherwise calls the `verify` function of the strategy, if it has one. #### Content API Two strategies are registered for `content-api`. The admin package registers `content-api-token` in its `register` function, which runs as an internal provider before plugins. The users-permissions plugin then registers `users-permissions`. - **`content-api-token`** looks up the bearer token as an API token. Its `verify` allows every scope for a full-access token, only scopes that end with `find` or `findOne` for a read-only token, and the token permissions for a custom token. - **`users-permissions`** validates a users-permissions JWT, loads the user and builds an ability from the role permissions. Without a token, it builds an ability from the permissions of the Public role. If the Public role has no permissions, it does not authenticate the request. Its `verify` requires `ability.can(scope)` for each scope. A route without a scope requires a logged-in user. After `initRouting()`, `strapi.contentAPI.permissions.registerActions()` registers one action per controller action that a `content-api` route uses. When Strapi binds a controller action, it tags the function with the route type (the `Symbol.for('__type__')` property). The tag limits the actions in the Roles and API token settings to the actions that a Content API route can reach. The permission engine ignores a permission whose action is not registered. #### Admin API The admin package registers two strategies for `admin`: `admin` validates a session access token, and `admin-token` validates an admin API token. Both set `ctx.state.user` and `ctx.state.userAbility`. The `admin` strategy has no `verify`, and `admin-token` only checks that the token exists and has not expired. The generated scope is not checked on admin routes. Admin routes authorize with policies instead. Most admin routes list `admin::isAuthenticatedAdmin`. Routes that need an RBAC action also list `admin::hasPermissions` with that action. Plugins use their own policies, for example `plugin::content-manager.hasPermissions`. These policies read `ctx.state.userAbility`. :::caution `authorize` wraps the rest of the chain. When a later policy, middleware or controller throws `UnauthorizedError` or `ForbiddenError`, `authorize` replaces it with a generic 401 or 403 body and drops the message and details. `PolicyError` is the exception. It goes to `strapi::errors` with its message, so a policy can return a public reason. ::: ### Policies and route middlewares | | Policies | Route middlewares | | ----------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------- | | Runs | After `authorize`, before route middlewares | After policies, before the controller | | Signature | `(policyContext, config, { strapi })` | Koa `(ctx, next)`, made by `factory(config, { strapi })` | | Result | `true` or `undefined` continues. Any other value throws `PolicyError` (403). | Calls `next()`. Can act before and after the controller. | | Name lookup | Full name, or a short name resolved in the route plugin or API namespace | Full UID only (`plugin::users-permissions.rateLimit`) | | Config validation | The policy `validator` runs when the route is registered | The factory runs when the route is registered | Use a policy for a yes or no decision. Use a route middleware when you need to change the request or the response. ### Controller resolution For a string handler, `getAction` splits the string at the last dot into a controller name and an action name. It then looks up the controller in the `controllers` registry: 1. If `info.pluginName` is `admin`: `admin::`. 2. If `info.pluginName` is another plugin: `plugin::.`. 3. If `info.apiName` is set: `api::.`. 4. If nothing matched: the controller name as a full UID. This is how core routes, which use `'.find'`, resolve. If the action is not a function, the boot fails with `Handler not found ""`. Otherwise the route stores `controller[action].bind(controller)`. A function or array handler is used as is, gets no scope and no type tag. The first registry `get` instantiates the controller. Because the route keeps the bound function, a `set` or `extend` on the controller after `initRouting()` does not change the mounted route. Change controllers in a `register` function, because `initRouting()` runs in the bootstrap phase before any `bootstrap` function. See [Container and registries](./03-container-and-registries.md) for the registry operations. ### Core controllers `createCoreController(uid, cfg)` in [`factories.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/factories.ts) returns a controller factory. The factory builds the base controller for the content type, copies the base actions that `cfg` does not define, and sets the base controller as the prototype. A custom action can call the core action with `super.find(ctx)` and use `this.sanitizeQuery`, `this.sanitizeOutput` and `this.transformResponse`. The collection type `find` action in [`core-api/controller/collection-type.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/core-api/controller/collection-type.ts): 1. calls `validateQuery(ctx)` and `sanitizeQuery(ctx)`; 2. calls `strapi.service(uid).find(query)`, where `uid` is the content type UID; 3. calls `sanitizeOutput(results, ctx)`; 4. returns `transformResponse(results, { pagination })`, which is `{ data, meta }`. `create` and `update` also throw `ValidationError` when `body.data` is not an object, then call `validateInput` and `sanitizeInput` on `body.data`. `create` sets status 201. `delete` sets status 204 and returns nothing. The core service (`createCoreService`) calls the Document Service: `strapi.documents(uid).findMany()`, `findOne()`, `create()`, `update()` or `delete()`. Its params default to `status: 'published'`, so the Content API reads published documents unless the query asks for drafts. For `create` and `update`, the same default makes the Document Service publish the document. The core service passes no auth to the Document Service. The route chain and the controller apply permissions before and after the call. ### Validation and sanitization The core controller helpers call `strapi.contentAPI.validate` and `strapi.contentAPI.sanitize` ([`services/content-api/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/content-api/index.ts)), which wrap the functions in `@strapi/utils`. They pass `ctx.state.auth`, `ctx.state.route` and `api.rest.strictParams`. - **`validateQuery`** checks `filters`, `sort`, `fields` and `populate` against the schema and throws `ValidationError` (400). It also rejects filters and sorts on relations whose target the caller cannot `find`. With `strictParams`, it rejects unknown top-level query keys. - **`sanitizeQuery`** returns a copy of the query without private fields, invalid keys and restricted relations. With `strictParams`, it also drops unknown top-level keys. - **`validateInput`** rejects `id`, `documentId`, non-writable attributes, unknown attributes and restricted relations. - **`sanitizeInput`** removes the same data instead of throwing. - **`sanitizeOutput`** removes passwords, private attributes and relations that the caller cannot `find`. Validation runs first, so a client gets a clear 400 for bad input. Sanitization then removes what the caller may not read or write. Plugins add steps through the `sanitizers` registry (`content-api.input`, `content-api.output`) and the `validators` registry (`content-api.input`). The Zod schemas in `route.request` and `route.response` are not a request validator. `createCoreRouter` builds them with `CoreContentTypeRouteValidator` from the content type schema. At runtime, Strapi only uses them to validate or sanitize the extra params added with `addQueryParams` and `addInputParams`, and to accept those keys in strict mode. `@strapi/openapi` uses them to generate the OpenAPI document. The Document Service runs the entity validator on every write. See [Document write path](./05-document-write-path.md#validation). Admin controllers do not use `strapi.contentAPI`. The content-manager controllers build a permission checker from `ctx.state.userAbility` and use it to sanitize the query and the output. ### Response and errors A controller writes the response in one of three ways: - It returns a value. The return-body middleware sets `ctx.body` if it is empty. - It sets `ctx.body` and `ctx.status`. - It calls a helper: `ctx.send(data, status)`, `ctx.created(data)`, `ctx.deleted(data)`, or an error helper such as `ctx.badRequest(message, details)` or `ctx.notFound()`. Strapi generates one error helper per 4xx and 5xx status of `node:http`. The `strapi::errors` middleware in [`middlewares/errors.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/middlewares/errors.ts) handles the rest: - If no middleware set a status, it responds 404. - An `ApplicationError` from `@strapi/utils` gets its status from its class: `UnauthorizedError` 401, `ForbiddenError` and `PolicyError` 403, `NotFoundError` 404, `PayloadTooLargeError` 413, `RateLimitError` 429, `NotImplementedError` 501, and 400 for the others, such as `ValidationError`. - An `http-errors` error keeps its status. - Any other error is logged. If it carries a 4xx status, the status and message are kept. Otherwise the response has a generic message and status 500 (or the 5xx status of the error), so internal details do not leak. `strapi::errors` and the error helpers on `ctx` write the same body: `{ data: null, error: { status, name, message, details } }`. A controller that sets `ctx.body` directly can use another shape. ### Related - [Server lifecycle](./02-server-lifecycle.md) - [Container and registries](./03-container-and-registries.md) - [Extension points](./04-extension-points.md) - [Document write path](./05-document-write-path.md) - [Sessions and JWT](./08-authentication.md) - [Glossary](./11-glossary.md) - [`@strapi/core` package](../packages/core/core/index.md) - [`@strapi/strapi` package](../packages/core/strapi/index.md) - [`@strapi/admin` package](../packages/core/admin/index.md) - [`@strapi/plugin-users-permissions` package](../packages/plugins/users-permissions/index.md) - [`@strapi/utils` package](../packages/core/utils/index.md) --- ## Schema sync At each boot, Strapi compares the content-type and component schemas with the database and changes the tables to match. This page explains the inputs, the conversion to database metadata, the diff, the hooks around it and the migrations. It also lists what Strapi never does automatically. Read it before you change the schema code of `@strapi/database`, the content-type transform in `@strapi/core`, or a sync hook handler. ### Overview Schema sync runs in the bootstrap phase. At that point, the registries are final. See [Server lifecycle](./02-server-lifecycle.md#bootstrap-phase) for the full list of bootstrap steps. ```mermaid flowchart TD CT["content-types and components registries"] --> T["transformContentTypesToModels()"] T --> I["db.init(): metadata.loadModels(), validateDatabase()"] MR["models registry"] --> I I --> O["read old content types from the core store"] O --> BS["beforeSync hook"] BS --> Q{"pending migrations?"} Q -->|yes| MG["user migrations, then internal migrations"] MG --> SS Q -->|no| H{"snapshot hash equal?"} H -->|"no, or no snapshot"| SS["syncSchema(): 3-way diff, DDL, new snapshot"] H -->|yes| R["repairs"] SS --> R R --> AS["afterSync hook"] AS --> W["save content types in the core store"] ``` `db.init()` runs before Strapi reads the old content types, because the core store uses `db.query('strapi::core-store')`, and a query needs the metadata. ### Where schemas come from - The `apis` loader reads `src/api//content-types//schema.json`. It sets `collectionName` to `info.singularName` when the schema has none. Plugins declare content types in their server entry. - The `content-types` registry passes each schema to `createContentType()`. This function adds `createdAt`, `updatedAt`, `publishedAt`, `createdBy` and `updatedBy` to every content type. It adds `firstPublishedAt` only when that experimental option is on. - The `components` loader reads `src/components//.json`. A component must declare `collectionName`. If it does not, the loader stops Strapi. - Plugins change schemas in `register`. The i18n plugin adds `locale` and `localizations` to every content type. At the end of the register phase, Strapi replaces each custom field type with its underlying type. - The `models` registry holds database models that are not content types, for example the core store (`strapi_core_store_settings`) and the webhooks (`strapi_webhooks`). `publishedAt` exists on every content type, also when draft and publish (D&P) is off. The code comment says that without D&P, entries are always published. The i18n plugin adds `locale` to every content type, localized or not. Thus, when a content type turns D&P or i18n on or off, the data changes but the table shape does not. The [sync hooks](#sync-hooks) move that data. ### From schemas to database models [`transformContentTypesToModels()`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/utils/transform-content-types-to-models.ts) converts content types and components into `@strapi/database` models. The database layer knows scalar attributes and relations only. It does not know components, dynamic zones or media, so the transform rewrites them as relations: | Schema attribute | Model attribute | Storage | | ---------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | | `media` | `morphOne` or `morphMany` to `plugin::upload.file`, `morphBy: 'related'` | The join table of the `related` attribute of the file model (`files_related_mph`) | | `component` | `oneToOne` (single) or `oneToMany` (repeatable) to the component UID | The `_cmps` join table | | `dynamiczone` | `morphToMany` | The same `_cmps` table. `component_type` holds the component UID. | The transform also does these things: - It adds `id` (`increments`) to every model and `documentId` to each content type. Components have no `documentId`. - It throws when the snake-case name of an attribute is `id` or `document_id`. - It adds a `_cmps` model for each content type or component that has a component or dynamic zone attribute. The table has the columns `entity_id`, `cmp_id`, `component_type`, `field` and `order`, a foreign key to the owner with `ON DELETE CASCADE`, and a unique index on the four link columns. - It adds a `_documents_idx` index on `document_id`, `locale` and `published_at` to each content type. The model declares this index so that schema sync owns it. The internal migration that created it before is now a no-op. ### Metadata `db.init({ models })` calls `metadata.loadModels()` and then `validateDatabase()`. `loadModels()` does these steps: 1. It shortens table names with the identifiers service. A name longer than 55 characters gets a compressed form with a hash. Suffixes always use their short form, for example `lnk`, `mph`, `idx`, `uq` and `pk`. 2. It skips attributes with `unstable_virtual`. The i18n `localizations` attribute is virtual, so it creates no column and no table. 3. It sets a snake-case column name on each scalar attribute. 4. It calls `createRelation()` for each relation. This adds join-table models to the metadata and join-column data to the attributes. It throws on an invalid shape, for example an unknown target, or a `oneToMany` that is the owner side of a bidirectional relation. 5. It throws when two models have the same table name. The storage rules for relations are in `metadata/relations.ts`: - By default, a relation uses a join table `__lnk`. It has two id columns, a foreign key on each with `ON DELETE CASCADE`, and a unique index on the pair. A `oneToMany` or `manyToMany` relation adds an order column. A bidirectional `manyToOne` or `manyToMany` relation also adds an inverse order column. - `useJoinTable: false` on a `oneToOne` or `manyToOne` owner creates a join column `_id` on the owner table, with a foreign key `ON DELETE SET NULL`. Core uses this for `createdBy` and `updatedBy`. - In a bidirectional relation, only the owner side (`inversedBy`) creates storage. The `mappedBy` side reuses it. - `morphToOne` stores a target id and a target type in two columns of the owner table. `morphToMany` creates a `
__mph` join table with id, type, `field` and `order` columns. `morphOne` and `morphMany` store nothing. See [Polymorphic relations](../packages/core/database/01-relations/polymorphic-relations.md). `validateDatabase()` checks one case: a bidirectional relation where both sides use `inversedBy`. It emits a warning that names the side to change, based on which link table is empty. `metadataToSchema()` in `schema/schema.ts` turns the metadata into a plain `Schema`: a list of tables with columns, indexes and foreign keys. It maps attribute types to Knex column types. For example, `string`, `email`, `password`, `enumeration` and `uid` become `string`, `json` and `blocks` become `jsonb`, and `datetime` becomes `datetime` without time zone, with precision 6. A `unique` or `primary` column option adds an index. ### The schema diff [`db.schema.sync()`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/schema/index.ts) chooses between a full diff and a fast exit: 1. If a migration is pending, it runs the migrations and then `syncSchema()`. It does not check the hash. 2. If no snapshot is stored, it runs `syncSchema()`. 3. It hashes the target schema (SHA-256 of the JSON, with tables sorted by name). If the hash equals the stored hash, it returns `UNCHANGED` and does not inspect the database schema. The sort makes the hash independent of the order in which content types were registered. 4. Otherwise, it runs `syncSchema()`. The snapshot is in the `strapi_database_schema` table, with the columns `schema` (JSON), `hash` and `time`. Each write deletes the old rows first, so the table holds one snapshot. `syncSchema()` compares three schemas: | Schema | Variable | Source | Meaning | | -------- | ---------------- | ------------------------------------- | ---------------------------------- | | Live | `databaseSchema` | `dialect.schemaInspector.getSchema()` | What the database has now | | Previous | `previousSchema` | The stored snapshot | The target schema of the last sync | | Target | `userSchema` | `metadataToSchema(metadata)` | What the current schemas require | [`schema/diff.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/schema/diff.ts) applies the same rules to tables, columns, indexes and foreign keys: - In the target and not in the database: added. - In the target and in the database: compared, and updated if different. - In the database, not in the target, and in the previous snapshot: removed. - In the database, not in the target, and not in the previous snapshot: ignored. Strapi did not create this object, so it can be a table or a column that a user added. The code comment says: "it is a user custom table that we should not touch". The column comparison checks the type (through the dialect `getSqlType()`), `notNullable`, `defaultTo` and `unsigned`. It does not compare type arguments such as length or precision. The index comparison checks columns and type. The foreign key comparison checks columns, the referenced table and columns, `onDelete` and `onUpdate`. When the status is `CHANGED`, [`schema/builder.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/schema/builder.ts) applies the diff. Then `syncSchema()` stores the target schema as the new snapshot, also when nothing changed. The builder works in this order: 1. It calls `dialect.startSchemaUpdate()`. 2. It reads the current indexes and foreign keys of the updated tables. On PostgreSQL, it also reads the current column types. It does this before the transaction. The code comment says this avoids transaction timeouts on PostgreSQL. 3. In one Knex transaction, it creates the added tables, then their foreign keys. It adds foreign keys after all tables exist, so a foreign key can point to a table that the same run creates. 4. If `forceMigration` is on, it drops the foreign keys of the removed tables, then the tables. 5. For each updated table, it drops foreign keys, drops indexes, drops columns, alters columns, adds columns, and then recreates the indexes and foreign keys. It drops foreign keys first to avoid foreign key errors when columns change. 6. It calls `dialect.endSchemaUpdate()`. If the status is `CHANGED`, `Strapi.bootstrap()` then removes orphan morph rows (`component_type` and `related_type` pivots). A one-time repair of unidirectional join tables follows. See [Server lifecycle](./02-server-lifecycle.md#bootstrap-phase). ### Sync hooks The `registries` provider creates `strapi::content-types.beforeSync` and `strapi::content-types.afterSync` in its `register` step. Both are parallel hooks: all handlers run at the same time, and each one receives a deep clone of `{ oldContentTypes, contentTypes }`. `oldContentTypes` holds the schemas that the previous boot saved in the core store. On the first boot, the core store table does not exist, so `oldContentTypes` is `undefined` and the core handlers return at once. The core handlers are in [`packages/core/core/src/migrations`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/migrations/index.ts). Each hook has one core handler that runs its steps in series: | Hook | Step | Condition | Action | | ------------ | ---------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------ | | `beforeSync` | i18n disable | The content type was localized and is not now | Deletes entries that are not in the default locale. Sets `locale` to `null` on the rest. | | `beforeSync` | D&P disable | D&P was on and is off now | Deletes the rows where `published_at` is `null` (the drafts). | | `afterSync` | i18n enable | The content type was not localized and is now | Sets the default locale where `locale` is `null`. | | `afterSync` | D&P enable | D&P was off and is on now | Calls `strapi.documents(uid).discardDraft()` for each entry, in batches, to create drafts. | | `afterSync` | firstPublishedAt | The experimental option is on | Copies `publishedAt` into `firstPublishedAt` for published documents that have no value. | The disable steps delete data that the new configuration does not allow. They run before the schema changes. The enable steps can need a column that sync adds, for example `first_published_at`, so they run after sync. The hooks do not disable database lifecycles. The D&P enable step goes through the Document Service, so the subscribers and middlewares that exist at that time run for each entry. Only the data-transfer package calls `strapi.db.lifecycles.disable()`. Plugins also add handlers: i18n (`afterSync`, permissions of newly localized types), content-releases (both hooks, Enterprise Edition) and review-workflows (`afterSync` data migrations). Add handlers in `register`, because the hooks run before module `bootstrap`. See [Extension points](./04-extension-points.md#hooks-registry). ### Migrations Two providers share one runner, [`migrations/runner.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/migrations/runner.ts). Strapi used Umzug before. The internal runner replaced it and reuses the same storage tables. | | User migrations | Internal migrations | | -------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Source | `.js` and `.sql` files in `database/migrations` of the app, sorted by name | The list in `internal-migrations/index.ts`, plus `db.migrations.providers.internal.register()` | | Tracking table | `strapi_migrations` | `strapi_migrations_internal` | | Runs when | A migration is pending and `database.settings.runMigrations` is `true` (default) | A migration is pending | | `down` | Exported by a `.js` file. A `.sql` file has none. | Defined by each migration | Each migration runs in its own transaction (`wrapTransaction`). The runner records the name after the migration succeeds. If a migration throws, the runner throws `Migration (up) failed` and the boot fails. With `database.settings.useTypescriptMigrations`, Strapi reads user migrations from the compiled output folder. Migrations run inside `db.schema.sync()`: after `beforeSync` and before the diff. User migrations run before internal migrations. Thus, a migration sees the database that the previous boot left, plus the `beforeSync` changes. The tables and columns of the new schemas do not exist yet. The internal list holds the v4 to v5 upgrade migrations, for example identifier shortening, `document_id`, `locale` and `published_at`. Core registers `core::5.0.0-discard-drafts` in the `registries` provider. To add an internal migration, see [Migrations](../packages/core/database/03-migrations.md). ### Dialects `createConnection()` maps `connection.client` to a Knex driver: `sqlite` to `better-sqlite3`, `mysql` to `mysql2` and `postgres` to `pg`. MariaDB uses the `mysql` client and dialect. Each dialect provides a schema inspector and capability flags. The diff and the builder read these flags instead of checking the client name, with a few exceptions noted below. | Behavior | SQLite | PostgreSQL | MySQL and MariaDB | | ----------------------------------- | ---------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | | Foreign keys in the diff | No (`usesForeignKeys()` is `false`) | Yes | Yes | | Foreign keys inside `CREATE TABLE` | Yes (`canAlterConstraints()` is `false`) | No, added after all new tables exist | No, added after all new tables exist | | `unsigned` in the column comparison | No | No | Yes | | During a schema update | `pragma foreign_keys = off` | Nothing | `foreign_key_checks = 0` and `sql_require_primary_key = 0` | | New `increments` column on a table | `integer` plus a primary key | `increments` | `increments` | | Type conversions with custom SQL | None | `time` to `datetime` and back, with a logged warning | None | `getSqlType()` maps a Knex type to the name that the inspector reads back. For example, Knex creates `double` and `decimal` columns as `float` on SQLite, and the inspector reads `float` back. So the SQLite dialect maps both types to `float`. Without this mapping, the diff would report a type change at every sync. The builder also checks the client name directly for PostgreSQL and MySQL. On PostgreSQL, it runs the custom conversion SQL before the standard `ALTER`. On MySQL, dropping a foreign key can also drop an index with the same name. Strapi gives each foreign key and its index the same name, so the builder removes the dropped foreign keys from its list of existing indexes before it drops indexes. ### Safety rules Strapi never does these things automatically: - **Drop untracked objects.** A table, column, index or foreign key that is not in the previous snapshot stays. On the first sync there is no snapshot, so Strapi drops nothing and starts to track the target schema. - **Drop reserved or persisted tables.** The diff never removes `strapi_migrations`, `strapi_migrations_internal` and `strapi_database_schema`. It also keeps the tables that Enterprise Edition features list in the core store key `persisted_tables`, for example the audit logs table. This keeps their data after a downgrade to Community Edition. - **Detect renames.** The diff matches objects by name. A renamed attribute or `collectionName` is a drop plus an add. When `forceMigration` is on, the data of the old column or table is lost. - **Convert data on a type change.** The builder alters the column with Knex `.alter()`. Only the two PostgreSQL conversions above use custom SQL. - **Run `down` migrations.** No code in the boot path calls `db.migrations.down()`. - **Check the live database when the hash matches.** If someone changes a table by hand, Strapi does not repair it until the target schema changes or a migration runs. The `database.settings.forceMigration` setting is `true` by default. When it is `false`, the builder does not drop tables, columns or indexes. It still drops the foreign keys of updated tables. :::caution Core deletes content automatically in two cases. When a content type turns D&P off, the next boot deletes its drafts. When it turns localization off, the next boot deletes its entries that are not in the default locale. The `beforeSync` handlers do this before the schema sync. ::: :::note `schema/diff.ts` reads `persisted_tables` through the global `strapi.store`. This is the only link from the diff to `@strapi/core`. Keep it in mind if you change the diff or test it in isolation. ::: ### Code map | Concern | Path | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bootstrap, hooks call, repairs | [`packages/core/core/src/Strapi.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/Strapi.ts) | | Attributes added to content types | [`packages/core/core/src/domain/content-type/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/domain/content-type/index.ts) | | Sync hooks creation | [`packages/core/core/src/providers/registries.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/registries.ts) | | Metadata and relations | [`packages/core/database/src/metadata/relations.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/metadata/relations.ts) | | Metadata to schema | [`packages/core/database/src/schema/schema.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/schema/schema.ts) | | Snapshot storage | [`packages/core/database/src/schema/storage.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/schema/storage.ts) | | Migration providers | [`packages/core/database/src/migrations/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/migrations/index.ts) | | Dialect base class | [`packages/core/database/src/dialects/dialect.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/dialects/dialect.ts) | | Identifier shortening | [`packages/core/database/src/utils/identifiers/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/utils/identifiers/index.ts) | ### Related - [Server lifecycle](./02-server-lifecycle.md) - [Container and registries](./03-container-and-registries.md) - [Extension points](./04-extension-points.md) - [Glossary](./11-glossary.md) - [`@strapi/database` package](../packages/core/database/index.md) - [Migrations](../packages/core/database/03-migrations.md) - [Polymorphic relations](../packages/core/database/01-relations/polymorphic-relations.md) - [`@strapi/core` package](../packages/core/core/index.md) - [i18n plugin](../packages/plugins/i18n/index.md) --- ## Sessions and JWT This page summarizes how authentication works in this repository for: - Admin (back office) - Content API via users-permissions plugin (UP) It is intentionally concise for contributors working in the monorepo. ### SessionManager Core provides a `SessionManager` that issues: - Short‑lived access tokens (JWT, used in `Authorization: Bearer `) - Refresh/session tokens (JWT, stored/handled differently per origin) Per-origin configuration is defined at bootstrap time with: - `jwtSecret` - `accessTokenLifespan` (seconds) - `maxRefreshTokenLifespan`, `idleRefreshTokenLifespan` (seconds) - `maxSessionLifespan`, `idleSessionLifespan` (seconds) Each origin stores session rows in `admin::session` (hidden content-type) using the table/collection `strapi_sessions`, containing `userId`, `sessionId`, `deviceId`, `origin`, `expiresAt`, `absoluteExpiresAt`, `status`, `type`. #### Public API (per origin) - `generateRefreshToken(userId, deviceId?, { type?: 'refresh' | 'session' })` - `rotateRefreshToken(refreshToken)` - `generateAccessToken(refreshToken)` - `validateAccessToken(token)` - `invalidateRefreshToken(userId, deviceId?)` - `isSessionActive(sessionId)` Locations: - Core service: `packages/core/core/src/services/session-manager.ts` - Types: `packages/core/types/src/modules/session-manager.ts` ### Admin authentication Admin defines an origin `admin` with config under `admin.auth.sessions.*`. - Login/Register/Reset password create a refresh/session token and set an httpOnly cookie `strapi_admin_refresh`. - The response includes the short‑lived access token in `data.token`. - Clients use `Authorization: Bearer ` for subsequent admin requests. Endpoints (admin server): - `POST /admin/login` - `POST /admin/register` and `POST /admin/register-admin` - `POST /admin/reset-password` - `POST /admin/access-token` — rotates the refresh cookie and returns `{ data: { token } }` - `POST /admin/logout` — clears cookie and revokes refresh tokens; body may include `{ deviceId }` to revoke a single device family - `GET /admin/users/me/sessions` — lists the current admin user's active sessions (device label, signed-in time, last used) - `DELETE /admin/users/me/sessions/:sessionId` — revokes a single session owned by the current user - `DELETE /admin/users/me/sessions` — revokes all sessions; query `?keepCurrent=true` preserves the session backing the current request ("log out of other devices") Optional request fields on login/register: - `deviceId` (UUID). When provided, enables device-scoped revocation. - `rememberMe` (boolean). When true, uses the long-lived refresh family and sets a persistent cookie; otherwise uses session family (session cookie). Configuration: - `admin.auth.secret` — JWT secret for symmetric algorithms (HS256, HS384, HS512) - `admin.auth.sessions.options.algorithm` — JWT algorithm (default: 'HS256') - `admin.auth.sessions.options.privateKey` — Private key for asymmetric algorithms (RS256, RS512, ES256, etc.) - `admin.auth.sessions.options.publicKey` — Public key for asymmetric algorithms (RS256, RS512, ES256, etc.) - `admin.auth.sessions.options.*` — Any other JWT options (issuer, audience, subject, etc.) - `admin.auth.sessions.accessTokenLifespan` (default 1800) - `admin.auth.sessions.maxRefreshTokenLifespan` (default 30 days) - `admin.auth.sessions.idleRefreshTokenLifespan` (default 14 days) - `admin.auth.sessions.maxSessionLifespan` (default 1 day) - `admin.auth.sessions.idleSessionLifespan` (default 2 hours) **Deprecated:** - `admin.auth.options.*` — Use `admin.auth.sessions.options.*` instead - Cookie options: - `admin.auth.cookie.name` (default `jwtToken`) — name of the non-httpOnly access-token cookie used for session logins (when `rememberMe` is false) and EE SSO handoff. Rename to avoid collisions with another app on a shared parent domain that sets a `jwtToken` cookie. Requires an admin rebuild after change (value is inlined into the admin bundle at build time). - `admin.auth.cookie.domain` (or `admin.auth.domain`) — applied to `strapi_admin_refresh` and to the non-httpOnly access-token cookie written by the admin client (and EE SSO). Defaults to a host-only cookie when unset. Requires an admin rebuild after change (value is inlined into the admin bundle at build time). Set a parent domain to share the admin session across subdomains; leave unset to keep each host isolated. - `admin.auth.cookie.path` (default `/admin`) — applied to `strapi_admin_refresh` and to the non-httpOnly access-token cookie written by the admin client (and EE SSO). Requires an admin rebuild after change (value is inlined into the admin bundle at build time). Use a distinct path per instance when hosting multiple Strapis under the same parent domain. - `admin.auth.cookie.sameSite` (default `lax`) — applied to `strapi_admin_refresh` Key files: - Bootstrap/config: `packages/core/admin/server/src/bootstrap.ts` - Routes: `packages/core/admin/server/src/routes/authentication.ts`, `packages/core/admin/server/src/routes/users.ts` - Controllers: `packages/core/admin/server/src/controllers/authentication.ts`, `packages/core/admin/server/src/controllers/authenticated-session.ts` - Strategy (Bearer access token validation): `packages/core/admin/server/src/strategies/admin.ts` ### Content API (users-permissions) The users-permissions (UP) plugin supports two modes, controlled by `plugin::users-permissions.jwtManagement`: - `legacy-support` (default): issues long‑lived `jwt` configured by `plugin::users-permissions.jwt`. - `refresh`: uses `SessionManager` to issue short‑lived access tokens (`jwt`) and separate `refreshToken`. When `jwtManagement` is `refresh`: - Login/Register/Provider callback responses include `{ jwt, refreshToken }`. - New endpoints: - `POST /api/auth/refresh` — body `{ refreshToken }`, returns `{ jwt }` and rotates the refresh token - `POST /api/auth/logout` — logs out the session backing the current request by default; optional body fields: - `{ scope: 'all' }` — revokes every session for the user (previous default behavior before session management) - `{ deviceId }` — revokes all sessions for a specific device family - `GET /api/auth/sessions` — lists the authenticated user's active sessions - `DELETE /api/auth/sessions/:sessionId` — revokes a single session owned by the authenticated user Configuration keys: - `plugin::users-permissions.jwtManagement`: `'legacy-support' | 'refresh'` - `plugin::users-permissions.sessions.accessTokenLifespan` - `plugin::users-permissions.sessions.maxRefreshTokenLifespan` - `plugin::users-permissions.sessions.idleRefreshTokenLifespan` - `plugin::users-permissions.sessions.maxSessionLifespan` - `plugin::users-permissions.sessions.idleSessionLifespan` Key files: - Plugin bootstrap/config: `packages/plugins/users-permissions/server/src/bootstrap/index.js`, `packages/plugins/users-permissions/server/src/config.js` - Controller: `packages/plugins/users-permissions/server/src/controllers/auth.js` - Routes: `packages/plugins/users-permissions/server/src/routes/content-api/auth.js` - JWT service: `packages/plugins/users-permissions/server/src/services/jwt.js` ### Session Revocation on Credential Changes For security, **password changes and resets automatically invalidate all active refresh/session tokens** across all devices: - **Admin**: When an admin user changes their password via `PUT /admin/users/me` (with `currentPassword` and `password`), all admin sessions are revoked, including the current session. The user must re-authenticate. - **Admin**: When an admin resets their password via `POST /admin/reset-password`, all existing admin sessions are revoked before issuing a new session. - **Content API (refresh mode)**: When a user changes their password via `POST /api/auth/change-password` or resets it via `POST /api/auth/reset-password`, all users-permissions sessions are revoked. A new refresh token is issued for the current request. This behavior ensures that compromised refresh tokens cannot be used after a password change, mitigating persistent session hijacking attacks. ### Notes - Access tokens are always used in the `Authorization` header as `Bearer `. - Admin refresh tokens are stored in an httpOnly cookie and are not exposed to JavaScript. - Device-bound sessions allow targeted logout by `deviceId`. - Session list responses expose `lastActiveAt` as the timestamp of the last refresh-token rotation for that session (not per-request activity tracking). --- ## Enterprise Edition EE features documented so far: - [Review workflows](../packages/core/review-workflows/01-backend.md) - [Audit logs](../packages/core/admin/ee/audit-logs.md) - [`useLicenseLimits` hook](../packages/core/admin/ee/hooks/use-license-limits.md) - [Content releases](../packages/core/content-releases/index.md) ### Promoting EE features in CE projects Every time a new EE feature is added in Strapi, in the settings menu, you should add the following condition to ensure that the feature promotes itself in CE: `packages/core/admin/admin/src/hooks/useSettingsMenu/index.js` ```js ... ...(!window.strapi.features.isEnabled(window.strapi.features.NEW_EE_FEATURE) && window.strapi?.flags?.promoteEE ? [ { intlLabel: { id: 'Settings.new-ee-feature.page.title', defaultMessage: 'NEW EE FEATURE', }, to: '/settings/purchase-new-ee-feature', id: 'new-ee-feature', licenseOnly: true, }, ] : []), ... ``` --- ## Future Flags In Strapi, we have incoming features that are not yet ready to be shipped to all users, but we aim to keep them updated with our codebase. Additionally, we want to offer community users the opportunity to provide early feedback on these new features or changes. To achieve this, we use future flags, which enable unstable features **at your own risk**. These flags may change or be removed, and they may contain breaking changes. A future flag gates a feature that has not shipped yet. Enabling one means the feature is likely to change or be removed, and parts of it may still be under development. The flags Strapi declares are listed in `FeaturesFutureFlags` ([`features.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/types/src/core/config/features.ts)): | Flag | Gates | | ------------------------------- | ------------------------------------------------------------- | | `experimental_firstPublishedAt` | The `firstPublishedAt` attribute on draft and publish content | | `unstableNextDesignSystem` | Building the admin panel against the next design system | The type also accepts any other string key, so a plugin can read its own flag without changing `@strapi/types`. ### How to enable a future flag Add the flag under `future` in the `config/features.(js|ts)` file of your Strapi application. Create the file if it does not exist. ```ts // config/features.ts export default ({ env }) => ({ future: { unstableNextDesignSystem: true, experimental_firstPublishedAt: env.bool('STRAPI_FEATURES_FUTURE_FIRST_PUBLISHED_AT', false), }, }); ``` ### How to add and read a future flag Developers who introduce an unstable feature add its flag to `FeaturesFutureFlags`. The features config is part of the config object, so `strapi.config.get('features')` returns it. To check a flag, use the API for your side of the code: - **Server**: `strapi.features.future.isEnabled('flagName')`. The `features` service is created in [`services/features.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/features.ts). There is no `strapi.future` object. - **Admin panel**: `window.strapi.future.isEnabled('flagName')`. The admin reads the features config that was passed in when the admin panel was built ([`browserStrapi.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/admin/admin/src/utils/browserStrapi.ts)). Both return `true` only when the flag is set to `true`. ### Permanent flags are not future flags A flag at the top level of `features` (for example `useLegacyMediaLibrary`) is supported configuration, not an unstable preview. Read it with `strapi.features.isEnabled('flagName')` on the server and `window.strapi.featureFlags.isEnabled('flagName')` in the admin panel. --- ## Glossary This glossary defines terms as the server code uses them. Each entry points to the code or to the architecture page that explains it. :::caution Five words have more than one meaning in the codebase: - **Services.** The `services` registry holds plugin, API and application services. The folder `packages/core/core/src/services/` holds core implementation code. See [core service](#core-service) and [service](#service). - **Modules.** A runtime module wraps a plugin or an API. `Modules.*` in `@strapi/types` is a namespace of TypeScript contracts for core subsystems. See [module](#module) and [`Modules.*` types](#modules-types). - **Hooks.** The hooks registry holds server hook objects. The admin panel has its own hooks. React hooks are a third meaning. Database lifecycles are sometimes called hooks too. See [hook](#hook) and [database lifecycle](#database-lifecycle). - **Providers.** Internal providers are lifecycle units in `packages/core/core/src/providers/`. Provider packages in `packages/providers/` implement email and upload. See [provider](#provider). - **Lifecycles.** Module lifecycle functions are `register`, `bootstrap` and `destroy`. Database lifecycles run around queries. See [lifecycle function](#lifecycle-function). ::: ### Runtime #### Container The class that `Strapi` extends. It stores resolvers by name with `add`, resolves them once with `get` and caches the result. It throws on a duplicate name and has no `set` or `remove`. Code: `packages/core/core/src/container.ts`. See [Container and registries](./03-container-and-registries.md). #### Provider An internal lifecycle unit of the server runtime, with optional `init`, `register`, `bootstrap` and `destroy` functions. The `providers` array in `packages/core/core/src/providers/index.ts` sets the call order. Not the same as a provider package in `packages/providers/`, which implements an email or upload backend. See [Server lifecycle](./02-server-lifecycle.md#internal-providers). #### Registry An object that stores named artifacts of one kind, such as services, content types or policies. The `registries` provider adds 14 registries to the container. Most registries have `add`, `get` and `getAll`. Some also have `set` and `extend`. Code: `packages/core/core/src/registries/`. #### Core service A subsystem implemented in `packages/core/core/src/services/`, for example the Document Service, the event hub, the webhook runner or the HTTP server. `Strapi` or a provider mounts it in the container. It is not in the `services` registry. #### Service An entry in the `services` registry, defined by a plugin, an API or the application. The registry stores a factory and calls it with `{ strapi }` on the first `get`. Read it with `strapi.service(uid)`. #### Module The runtime wrapper around a plugin or an API. `createModule(namespace, rawModule, strapi)` builds it. When the `modules` registry adds it, it calls `module.load()`, which puts the module artifacts into the registries. The module also runs the `register`, `bootstrap` and `destroy` functions of the raw module, once each. Code: `packages/core/core/src/domain/module/index.ts`. #### Raw module The object that a plugin `strapi-server` entry or an API folder produces: `register`, `bootstrap`, `destroy`, `config`, `routes`, `controllers`, `services`, `contentTypes`, `policies` and `middlewares`. The loaders build it, and the `modules` registry wraps it. #### `Modules.*` types A TypeScript namespace in `@strapi/types` (`packages/core/types/src/modules/`). It holds the contracts of core subsystems, for example `Modules.Documents.Service` for `strapi.documents` or `Modules.EventHub.EventHub` for `strapi.eventHub`. It is not related to runtime modules. #### Namespace The prefix of a UID. Scoped namespaces are `api::` and `plugin::`. The other namespaces are `admin::`, `strapi::` and `global::`. Code: `packages/core/core/src/registries/namespace.ts`. #### UID The full key of an artifact in a registry. A scoped namespace is followed by a dot and the name, for example `api::article.article` or `plugin::upload.file`. The other namespaces are followed directly by the name, for example `admin::user`, `strapi::cors` or `global::is-owner`. A component UID has no namespace: `.`. See [Namespaces and UIDs](./03-container-and-registries.md#namespaces-and-uids). #### Loader A function in `packages/core/core/src/loaders/` that reads the file system or the enabled plugins and fills the registries. `loadApplicationContext()` runs the eight loaders with `Promise.all` during the `registries` provider `register` step. #### Lifecycle function One of `register`, `bootstrap` or `destroy`. Providers, modules, the admin and the application can each define them. `Strapi` calls them in a fixed order. See [Server lifecycle](./02-server-lifecycle.md). ### Extension #### Hook A named list of handlers in the `hooks` registry. `strapi.hook(uid)` returns it. The hook objects come from the `hooks` module of `@strapi/utils` (series, waterfall, parallel or bail). Core defines `strapi::content-types.beforeSync` and `strapi::content-types.afterSync`. See [Hooks registry](./04-extension-points.md#hooks-registry). #### Database lifecycle A subscriber that `@strapi/database` calls before and after each entity manager query, for example `beforeCreate` or `afterDelete`. Add one with `strapi.db.lifecycles.subscribe()` or with a `lifecycles` file next to a content-type schema. See [Database lifecycles](./04-extension-points.md#database-lifecycles). #### Document middleware A function `(ctx, next)` that wraps every Document Service method call. Add one with `strapi.documents.use()`. `ctx` holds `uid`, `contentType`, `action` and `params`. See [Document Service middlewares](./04-extension-points.md#document-service-middlewares). #### Event hub The in-process publish and subscribe service at `strapi.eventHub`. `on` adds a listener for one event name. `subscribe` adds a function that receives every event. `emit` awaits each subscriber and listener in order. Code: `packages/core/core/src/services/event-hub.ts`. #### Webhook allowed events The list of event names that a webhook can subscribe to. The webhook store holds it as a map. Core adds the six `entry.*` events. Plugins add their own with `strapi.get('webhookStore').addAllowedEvent(key, value)`. Code: `packages/core/core/src/services/webhook-store.ts`. #### Custom field A field type that a plugin or the application registers with `strapi.customFields.register()`. It maps to a built-in type. At the end of the register phase, Strapi replaces each custom field attribute type with that built-in type. :::caution `strapi.customFields` is a small service in the container (key `customFields`). The registry that stores the definitions is `strapi.get('custom-fields')`. ::: ### Content model #### Content type A schema that defines a kind of entry, with `kind` set to `collectionType` or `singleType`. It lives in the `content-types` registry under a UID such as `api::article.article`. Strapi adds system attributes to each schema: timestamps, `publishedAt`, creator fields and, when enabled, `firstPublishedAt`. The i18n plugin adds `locale` and `localizations`. #### Component A reusable group of attributes. Its UID is `.`. The `components` registry stores it. A content type uses a component through an attribute of type `component`. #### Dynamic zone An attribute of type `dynamiczone`. It holds an ordered list of components, chosen from the list in the attribute `components` option. #### Core store A key-value table (`strapi_core_store_settings`, model `strapi::core-store`) that core and plugins use for settings. Core stores the previous content-type schemas there to compare them at the next boot. Access it with `strapi.store`. ### Document Service #### Document One logical entry of a content type. It can have many rows: one per locale and per state (draft or published). #### documentId The string that identifies a document. All rows of the document share it. The database layer generates it with `createId` from `@paralleldrive/cuid2`. A row also has a numeric `id`, which changes when publish replaces the published row. #### Draft and publish (D&P) A content-type option (`options.draftAndPublish`). With D&P, a row with `publishedAt: null` is the draft, and a row with a date is the published version. Without D&P, every row has a date, and the `publish`, `unpublish` and `discardDraft` methods are `undefined`. #### Lookup An internal object that the Document Service parameter pipeline builds from `status` and `locale`. `transformParamsToQuery` merges it into the `where` clause. The repository rejects a `lookup` param from the caller. #### Entries service The layer under the Document Service repository, in `document-service/entries.ts`. It resolves relation `documentId` values to row `id` values, checks uniqueness, runs the entity validator, writes components and calls `strapi.db.query(uid)`. #### Entity validator The service at `strapi.entityValidator` that validates create and update data against a content-type schema with Yup. It relaxes some rules for drafts. Code: `packages/core/core/src/services/entity-validator/`. #### Route validator The classes in `packages/core/core/src/core-api/routes/validation/`, for example `CoreContentTypeRouteValidator`. They build Zod schemas for the query params, the body and the response of core API routes from the content-type schema. They are separate from the entity validator. #### Entity Service The data API that the Document Service replaces. `strapi.entityService` is deprecated. New code uses `strapi.documents`. ### Code map | Concern | Path | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Container | [`packages/core/core/src/container.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/container.ts) | | Internal providers | [`packages/core/core/src/providers/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/index.ts) | | Registries | [`packages/core/core/src/registries/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/index.ts) | | Runtime module | [`packages/core/core/src/domain/module/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/domain/module/index.ts) | | `Modules.*` types | [`packages/core/types/src/modules/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/types/src/modules/index.ts) | | Namespace rule | [`packages/core/core/src/registries/namespace.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/namespace.ts) | | Loaders | [`packages/core/core/src/loaders/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/index.ts) | | Hook factories | [`packages/core/utils/src/hooks.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/utils/src/hooks.ts) | | Database lifecycles | [`packages/core/database/src/lifecycles/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/lifecycles/index.ts) | | Event hub | [`packages/core/core/src/services/event-hub.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/event-hub.ts) | | Custom fields service and registry | [`packages/core/core/src/services/custom-fields.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/custom-fields.ts), [`packages/core/core/src/registries/custom-fields.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/registries/custom-fields.ts) | | Content-type system attributes | [`packages/core/core/src/domain/content-type/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/domain/content-type/index.ts) | | Core store | [`packages/core/core/src/services/core-store.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/core-store.ts) | | Document Service | [`packages/core/core/src/services/document-service/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/document-service/index.ts) | | Entity validator | [`packages/core/core/src/services/entity-validator/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/entity-validator/index.ts) | | Route validator | [`packages/core/core/src/core-api/routes/validation/content-type.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/core-api/routes/validation/content-type.ts) | ### Related - [Architecture overview](./index.md) - [Container and registries](./03-container-and-registries.md) - [Extension points](./04-extension-points.md) - [Document write path](./05-document-write-path.md) --- ## Architecture overview This section explains how the parts of Strapi work together. It covers topics that cross package boundaries. For one package at a time, read the package pages. For the list of packages and their dependencies, read the [package map](./01-package-map.mdx). ### What the monorepo contains Strapi is a Yarn workspaces and Nx monorepo. The workspace packages are in `packages/`. The table shows the main parts. | Part | Main packages | What it does | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Server runtime | `@strapi/core` (`packages/core/core`) | Defines the `Strapi` class, the container, the internal providers, the registries and the loaders. Contains the core services: Document Service, event hub, webhooks, Koa HTTP server and others. | | Entry package | `@strapi/strapi` (`packages/core/strapi`) | Re-exports `@strapi/core` and the types. Contains the `strapi` CLI and the admin build tooling. Applications depend on this package. | | Admin panel | `@strapi/admin` (`packages/core/admin`) | Contains the React admin application (`admin/`) and its server part (`server/`). The server part holds admin users, roles, admin authentication and admin routes. | | Database layer | `@strapi/database` (`packages/core/database`) | A layer on top of Knex: model metadata, schema sync, migrations, entity manager, query builder, database lifecycles and transactions. Dialects: SQLite, PostgreSQL and MySQL. The MySQL dialect also handles MariaDB. | | Shared code | `@strapi/types`, `@strapi/utils`, `@strapi/permissions` | Shared TypeScript types, shared helpers (hooks, sanitize, validate, errors) and the permission engine. | | Core plugins | `packages/core/content-manager`, `content-type-builder`, `upload`, `email`, `content-releases`, `review-workflows`, and `packages/plugins/i18n` | Plugins that the plugin loader always enables. | | Other plugins | `packages/plugins/*` | Plugins that an application installs, for example `users-permissions`, `graphql` and `documentation`. | | Provider packages | `packages/providers/*` | Implementations for the email and upload plugins, for example `upload-aws-s3` or `email-sendgrid`. | :::caution The word "provider" has two meanings. `packages/providers/*` are email and upload implementations. `packages/core/core/src/providers/` are internal lifecycle units of the server runtime. Read the [glossary](./11-glossary.md) for other naming traps. ::: ### Server and admin split Strapi has two runtimes. The server is a Node.js process. The admin panel is a React application. It runs in the browser and calls the server over HTTP. A package that adds code to both runtimes declares two entry points in the `exports` field of its `package.json`: - `./strapi-server` contains the Node.js code. The plugin loader in `@strapi/core` resolves this export. If the export is missing, it uses `./strapi-server.js`. - `./strapi-admin` contains the React code. The admin build in `@strapi/strapi` collects this export from each enabled plugin and bundles it into the admin application. `@strapi/admin` uses the same two entry points. The server does not load `@strapi/admin/strapi-server` through the plugin loader. The internal `admin` provider loads it. The admin artifacts use the `admin::` namespace. ### The Strapi class `Strapi` extends a small `Container` class. The instance is the central hub of the server. Code receives it as a `strapi` parameter, for example in `register({ strapi })` or in a service factory `({ strapi }) => ({ ... })`. The instance gives access to: - core services, for example `strapi.documents`, `strapi.db`, `strapi.eventHub`, `strapi.server` and `strapi.log`; - registries, through `strapi.get('')` and shortcuts such as `strapi.service(uid)` or `strapi.contentType(uid)`. `createStrapi()` also assigns the instance to `global.strapi`. A code comment marks this for removal in a future major version. Some internal modules still read the global, for example the Document Service repository and the plugin loader. New code must receive `strapi` as a parameter. Read [Container and registries](./03-container-and-registries.md) for details. ### Lifecycle in one paragraph `createStrapi()` creates the instance. The constructor loads the configuration, adds the core services to the container and runs the `init` step of each internal provider. `strapi.start()` calls `load()` and then `listen()`. `load()` runs `register()` and then `bootstrap()`. During `register()`, the loaders read plugins, APIs, components, middlewares and policies into the registries, and each plugin and API runs its `register` function. During `bootstrap()`, the database initializes and syncs the schema, the HTTP server builds its middlewares and routes, and each plugin and API runs its `bootstrap` function. `destroy()` runs the `destroy` functions and closes the HTTP server and the database connection. Read [Server lifecycle](./02-server-lifecycle.md) for the exact order. ### Pages in this section | Page | Topic | | ------------------------------------------------------------ | ---------------------------------------------------------------- | | [Package map](./01-package-map.mdx) | Packages and their dependencies. | | [Server lifecycle](./02-server-lifecycle.md) | Boot order, providers, loaders and shutdown. | | [Container and registries](./03-container-and-registries.md) | Container rules, registries, namespaces and UIDs. | | [Extension points](./04-extension-points.md) | How plugins and applications extend Strapi. | | [Document write path](./05-document-write-path.md) | What happens when code creates, updates or publishes a document. | | [HTTP request path](./06-http-request-path.md) | What happens to a request from Koa to the response. | | [Schema sync](./07-schema-sync.md) | How schemas become database tables at startup. | | [Authentication](./08-authentication.md) | Sessions and JWT. | | [Enterprise Edition](./09-enterprise-edition.md) | How Enterprise features are gated. | | [Future flags](./10-future-flags.md) | Opt-in features. | | [Glossary](./11-glossary.md) | Terms and naming traps. | ### Reading tracks #### New backend contributor 1. This page. 2. [Server lifecycle](./02-server-lifecycle.md) 3. [Container and registries](./03-container-and-registries.md) 4. [HTTP request path](./06-http-request-path.md) 5. [Document write path](./05-document-write-path.md) 6. [Schema sync](./07-schema-sync.md) 7. [Glossary](./11-glossary.md) 8. The [`@strapi/core`](../packages/core/core/index.md) and [`@strapi/database`](../packages/core/database/index.md) package pages. #### New admin contributor 1. This page, mainly [Server and admin split](#server-and-admin-split). 2. [Package map](./01-package-map.mdx) 3. The [`@strapi/admin`](../packages/core/admin/index.md) package page. 4. [Authentication](./08-authentication.md) 5. [Enterprise Edition](./09-enterprise-edition.md) 6. [Glossary](./11-glossary.md) #### Plugin author-contributor 1. This page. 2. [Server lifecycle](./02-server-lifecycle.md), mainly the register and bootstrap phases. 3. [Extension points](./04-extension-points.md) 4. [Container and registries](./03-container-and-registries.md) 5. [HTTP request path](./06-http-request-path.md), for routes, policies and route middlewares. 6. [Document write path](./05-document-write-path.md), for document middlewares and events. 7. [Glossary](./11-glossary.md) ### Code map | Concern | Path | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Strapi` class | [`packages/core/core/src/Strapi.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/Strapi.ts) | | `createStrapi()` and `global.strapi` | [`packages/core/core/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/index.ts) | | `@strapi/strapi` re-export | [`packages/core/strapi/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/strapi/src/index.ts) | | Admin build: collect `strapi-admin` entries | [`packages/core/strapi/src/node/core/plugins.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/strapi/src/node/core/plugins.ts) | | Plugin loader: resolve `strapi-server` | [`packages/core/core/src/loaders/plugins/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/plugins/index.ts) | | Always-enabled plugins list | [`packages/core/core/src/loaders/plugins/get-enabled-plugins.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/loaders/plugins/get-enabled-plugins.ts) | | Admin provider | [`packages/core/core/src/providers/admin.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/providers/admin.ts) | | Admin React entry | [`packages/core/admin/admin/src/StrapiApp.tsx`](https://github.com/strapi/strapi/blob/develop/packages/core/admin/admin/src/StrapiApp.tsx) | | Admin server entry | [`packages/core/admin/server/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/admin/server/src/index.ts) | | Database class | [`packages/core/database/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/index.ts) | ### Related - [`@strapi/core` package](../packages/core/core/index.md) - [`@strapi/strapi` package](../packages/core/strapi/index.md) - [`@strapi/admin` package](../packages/core/admin/index.md) - [`@strapi/database` package](../packages/core/database/index.md) --- ## Contributing to Strapi import Contributing, { toc as ContributingTOC } from '@site/../CONTRIBUTING.md'; export const toc = ContributingTOC; --- ## Code of conduct import CodeOfConduct, { toc as CodeOfConductTOC } from '@site/../CODE_OF_CONDUCT.md'; export const toc = CodeOfConductTOC; --- ## Setup ### Overview This document explains at a high level how we create our app instance, run the e2e tests and what technology we're using for e2e tests. As well as a small section on writing tests. ### Get Started To run the e2e tests, you must first install the playwright browsers. ```shell npx playwright install chromium webkit ``` #### Running Enterprise (EE) tests locally To run the suite in **Enterprise** mode (or to let `yarn test:e2e` **auto-detect** EE when possible), put your license in the **e2e-specific env file**: 1. Copy `tests/e2e/.env.example` to **`tests/e2e/.env`** (path relative to the **monorepo root**). Do **not** put e2e-only variables in a `.env` at the monorepo root; the runner will not load it. 2. Set **`STRAPI_LICENSE`** to your license string (same role as the GitHub Actions secret `strapiLicense` on CI). The unified runner (`tests/scripts/run-tests.js`) loads **`tests/e2e/.env`** with `dotenv` before starting Playwright; it does **not** automatically load `.env` from the repository root. Yarn/npm **`cross-env` does not read `.env` files** — only that loader does. Then use: | Goal | Command | | ----------------------------------------------------------------------- | ------------------ | | Auto (EE if `STRAPI_LICENSE` is set, else CE) | `yarn test:e2e` | | Always CE (license stripped from the runner process so Strapi stays CE) | `yarn test:e2e:ce` | | Always EE runner mode (exits if `STRAPI_LICENSE` is missing) | `yarn test:e2e:ee` | Full precedence and CI behavior are documented in [CE vs EE and environment variables](#e2e-ce-ee-env) below. #### Default run Because we require a "fresh" instance to assert our e2e tests against, that is included in the testing script. After `npx playwright install chromium webkit` (and optional EE `.env` above), run: ```shell yarn test:e2e ``` This will spawn by default a Strapi instance per testing domain (e.g. content-manager) in `test-apps` where the an individual `playwright.config` will start the instance and run tests against. It will automatically link the dependencies from the instance to the monorepo because `test-apps` are not considered part of the monorepo but we want to be using the most recent version of strapi (published or development) therefore meaning our most recent code changes can be tested against. If you need to clean the test-apps folder because they are not working as expected, run `yarn test:e2e:clean` (that target invokes `run-e2e-tests.js clean`). #### Running specific tests To run only one domain, meaning a top-level directory in e2e/tests such as "admin" or "content-manager", use the `--domains` option. ```shell yarn test:e2e --domains=admin ``` To pass file filters or Playwright-only flags (`--project`, `--grep`, `--reporter`, `--debug`, …), put them **after** a `--` that follows the runner options. (If you use **npm** instead of yarn, add one more `--` before those args: `npm run test:e2e -- --domains=admin -- login.spec.ts`.) ```shell # run only login.spec.ts in the admin domain yarn test:e2e --domains=admin -- login.spec.ts ``` You should still scope with `--domains` when filtering by file; otherwise every domain may be invoked and domains without that file can fail with "no tests found". For **CI, scripts, or automation**, prefer `--reporter=line` after the inner `--` so the run exits without waiting on the HTML reporter. #### Running specific browsers To run only a specific browser (to speed up test development, for example) you can pass `--project` to playwright with the value(s) `chromium` or `webkit` ```shell yarn test:e2e --domains=admin -- login.spec.ts --project=chromium ``` To debug your tests with a browser instance and the playwright debugger, you can pass the `--debug` option like this: ```shell yarn test:e2e --domains admin -- --debug yarn test:e2e --domains admin -- login.spec.ts --debug ``` #### Concurrency / parallelization The runner uses `min(number of selected domains, concurrency)` **test apps** (`test-apps/e2e/test-app-*`), each bound to a port `8000 + index` within a batch. **`concurrency` defaults** to the number of domain folders under `tests/e2e/tests/` (not the count after `--domains`). Domains are chunked into batches of that size: domains in a batch run **in parallel**, batches run **one after another**. Spec files **inside** a domain are **serial** (`workers: 1`, `fullyParallel: false` in `playwright.base.config.js`). ```shell # run at most one domain at a time (simplest logs; fully serial domains) yarn test:e2e -c 1 # example: at most three domains at once, then the next three, etc. yarn test:e2e -c 3 ``` #### Env Variables to Control Test Config Some helpers have been added to allow you to modify the playwright configuration on your own system without touching the playwright config file used by the test runner. | env var | Description | Default | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | PLAYWRIGHT_WEBSERVER_TIMEOUT | Timeout (ms) for starting the Strapi server | 160000 (160s) | | PLAYWRIGHT_ACTION_TIMEOUT | Playwright action timeout (e.g. `click()`) | 10000 (10s) | | PLAYWRIGHT_EXPECT_TIMEOUT | `expect()` assertion timeout | 10000 (10s) | | PLAYWRIGHT_TIMEOUT | Per-test timeout | 90000 (90s) | | PLAYWRIGHT_OUTPUT_DIR | Base for traces/screenshots/videos; each domain uses a subfolder `-` under that base. Also used as the JUnit output directory when set; when unset, JUnit defaults to `test-apps/junit-reports`. | `../test-results` (relative to each generated test app config) | | PLAYWRIGHT_VIDEO | Set `true` to save videos on failed tests | false | | PLAYWRIGHT_REUSE_EXISTING_SERVER | If `true` (local only; **ignored when `CI` is set**), Playwright may skip starting Strapi when the test URL already responds — faster if you keep a matching server up, **risky** if edition or license differs from this run. | `false` | ### Strapi Templates The test-app you create uses a [template](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/installation/templates.html) under `tests/app-template`. There we store premade content schemas and customisations such as other plugins, custom fields, or endpoints. If you add anything to the template, be sure to add this information to [the docs](./01-app-template.md). ### Running tests with environment variables (needed to run EE tests) {#e2e-ce-ee-env} Create **`tests/e2e/.env`** next to the e2e tests (see **`tests/e2e/.env.example`**). **`tests/scripts/run-tests.js`** loads that file with `dotenv` when it exists, then **`tests/utils/e2e-edition.ts`** applies CE vs EE so the runner, Playwright, and Strapi agree. Do not rely on the **repository root** `.env` for e2e — it is not loaded unless you change the runner. Optional: **`STRAPI_DISABLE_LICENSE_PING=true`** in the same file can match CI EE jobs when your license is offline-only (see `.github/actions/run-e2e-tests/script.sh`). #### `STRAPI_E2E_EDITION` (recommended mental model) | Value | Meaning | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ce` | Community Edition: `STRAPI_DISABLE_EE=true` for Strapi; `STRAPI_LICENSE` is stripped from the runner env so children cannot boot as EE; EE-only specs are skipped. | | `ee` | Enterprise: `STRAPI_DISABLE_EE` cleared; `STRAPI_LICENSE` must be present (and valid for Strapi to boot as EE). | **Resolution order** (see `tests/utils/e2e-edition.ts`): 1. **`yarn test:e2e:ce`** → always CE (license removed from env for this process). 2. **`yarn test:e2e:ee`** → EE; **exits with an error** if `STRAPI_LICENSE` is missing. 3. **`STRAPI_E2E_EDITION`** set to **`ce`** or **`ee`** (after `tests/e2e/.env` is loaded, plus CI `script.sh` or your shell) — use that edition (`ee` without a license falls back to CE with a warning unless you used `test:e2e:ee`, which fails instead). 4. Else **auto**: EE if `STRAPI_LICENSE` is non-empty, else CE. **CI:** `.github/workflows/tests.yml` defines two jobs: - **`e2e_ce`** — no `STRAPI_LICENSE` secret; composite action runs with `runEE: false` → `script.sh` sets `STRAPI_E2E_EDITION=ce`. - **`e2e_ee`** — `env STRAPI_LICENSE: ${{ secrets.strapiLicense }}` and `runEE: true` → `STRAPI_E2E_EDITION=ee` and `STRAPI_DISABLE_LICENSE_PING=true`. CI runs **`script.sh`**, which exports **`STRAPI_E2E_EDITION`** (`ce` vs `ee`) before `yarn test:e2e`. **`e2e-edition.ts`** reads that (and **`STRAPI_LICENSE`**, **`STRAPI_DISABLE_LICENSE_PING`**, etc.) so the runner matches the job. Locally, the same applies: if you **`export STRAPI_E2E_EDITION=ce`** (or `ee`), that explicit value is respected when you run `yarn test:e2e`. If you want **pure auto** (license only: EE when `STRAPI_LICENSE` is set, else CE), **`unset STRAPI_E2E_EDITION`** so it is not set in your shell. **Local — Yarn scripts** (`package.json`): | Script | Behavior | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `yarn test:e2e` | Uses **`STRAPI_E2E_EDITION`** from the environment when set (`ce` / `ee`, per the table above); otherwise **auto**: EE if `STRAPI_LICENSE` is set in `tests/e2e/.env` (or exported), otherwise CE. | | `yarn test:e2e:ce` | Always **CE**; license is stripped from env so Strapi cannot start as EE. | | `yarn test:e2e:ee` | **Fails fast** without `STRAPI_LICENSE`; otherwise EE in the runner (Strapi still needs a valid license to boot as Enterprise). | For convenience you can still use **`yarn test:e2e:ce`** / **`yarn test:e2e:ee`** instead of setting **`STRAPI_E2E_EDITION`** yourself. Playwright’s `reuseExistingServer` is **off by default** (see **`PLAYWRIGHT_REUSE_EXISTING_SERVER`** in the **Env Variables to Control Test Config** table) so a process already listening on the test port is not mistaken for this run’s Strapi — edition and env match what `e2e-edition.ts` applied. ### Running tests with future flags If you are writing tests for an unstable future feature you will need to add `app-template/config/features.js`. Currently the app template generation does not take the config folder into consideration. However, the run-e2e-tests script will apply the features config to the generated app. See the documentation for [features.js](https://docs.strapi.io/dev-docs/configurations/features#enabling-a-future-flag) ### What is Playwright? Playwright enables reliable end-to-end testing for modern web apps. It's cross browser, cross platform and cross language. At Strapi we use it for Javascript automated testing. For more information check out their [docs](https://playwright.dev/docs/intro). If you're struggling with their APIs, then check out their specific [API documentation](https://playwright.dev/docs/api/class-playwright). ### What makes a good end to end test? This is the million dollar question. E2E tests typically test complete user flows that touch numerous points of the application it's testing, we're not interested in what happens during a process, only the user perspective and end results. Consider writing them with your story hat on. E.g. "As a user I want to create a new entity, publish that entity, and then be able to retrieve its data from the content API". Our E2E test suite should _at minimum_ cover the core business flows of the product and this is lead by the QA defined set for this. Consult with your QA if you're not sure. --- ## App Template ### Overview An app template lives under **`tests/app-template`** (shared with other test types). It provides customizations and utilities to allow the tests to run. If you change the template, run **`yarn test:e2e:clean`** and regenerate test apps so they pick up the new template. Here you can read about what content schemas the test instance has & the API customisations we've built (incl. why we built them). ### Update the app template :::info The app template should be realistic and structured in a way an actual user might create an app using Strapi ::: To update the app template: - Run `yarn test:e2e:clean` to remove existing test apps - Run `yarn test:e2e -c=1 -- --ui` to generate a test app (don't run any tests) - Follow the instructions to [import the existing data set](./02-data-transfer.md#importing-an-existing-data-packet) - With the test app server running on 1337 you can now login to the app - Make changes in the content-type builder - Copy the generated files in the test app to the app template Once the app template is updated: - Run `yarn test:e2e:clean` to remove existing test apps - Run `yarn test:e2e -c=1 -- --ui` to generate a new test app using the updated template (don't run any tests) - Follow the instructions to [import the existing data set](./02-data-transfer.md#importing-an-existing-data-packet) - Follow the instructions to [export the updated data set](./02-data-transfer.md#exporting-an-updated-data-packet) ### Content Schemas #### Article ```json { // ... "attributes": { "title": { "type": "string" }, "content": { "type": "blocks" }, "authors": { "type": "relation", "relation": "manyToMany", "target": "api::author.author", "inversedBy": "articles" } } // ... } ``` #### Author ```json { // ... "attributes": { "name": { "type": "string" }, "profile": { "allowedTypes": ["images", "files", "videos", "audios"], "type": "media", "multiple": false }, "articles": { "type": "relation", "relation": "manyToMany", "target": "api::article.article", "mappedBy": "authors" } } // ... } ``` #### Homepage (Single Type) ```json { // ... "attributes": { "title": { "type": "string" }, "content": { "type": "blocks" }, "admin_user": { "type": "relation", "relation": "oneToOne", "target": "admin::user" }, "seo": { "type": "component", "repeatable": false, "component": "meta.seo" } } // ... } ``` #### Product This collection type is internationalized. ```json { // ... "attributes": { "name": { "pluginOptions": { "i18n": { "localized": true } }, "type": "string", "required": true }, "slug": { "pluginOptions": { "i18n": { "localized": true } }, "type": "uid", "targetField": "name", "required": true }, "isAvailable": { "pluginOptions": { "i18n": { "localized": false } }, "type": "boolean", "default": true, "required": true }, "description": { "pluginOptions": { "i18n": { "localized": true } }, "type": "blocks" }, "images": { "type": "media", "multiple": true, "required": false, "allowedTypes": ["images", "files", "videos", "audios"], "pluginOptions": { "i18n": { "localized": false } } }, "seo": { "type": "component", "repeatable": false, "pluginOptions": { "i18n": { "localized": true } }, "component": "meta.seo" }, "sku": { "pluginOptions": { "i18n": { "localized": true } }, "type": "integer", "unique": true }, "variations": { "type": "component", "repeatable": true, "pluginOptions": { "i18n": { "localized": true } }, "component": "product.variations" } } // ... } ``` #### Match ```json { // ... "attributes": { "date": { "type": "date" }, "kit_man": { "type": "string" }, "opponent": { "type": "string", "required": true, "regex": "^(?!.*richmond).*" }, "lineup": { "type": "component", "repeatable": true, "component": "match.player" }, "most_valuable_player": { "type": "component", "repeatable": false, "component": "match.player" }, "sections": { "type": "dynamiczone", "components": ["match.player", "product.variations"] } } // ... } ``` #### Shop (Single Type) This single type is internationalized. ```json { // ... "attributes": { "title": { "pluginOptions": { "i18n": { "localized": true } }, "type": "string", "required": true }, "content": { "pluginOptions": { "i18n": { "localized": true } }, "type": "dynamiczone", "components": [ "page-blocks.product-carousel", "page-blocks.hero-image", "page-blocks.content-and-image" ], "required": true, "min": 2 }, "seo": { "type": "component", "repeatable": false, "pluginOptions": { "i18n": { "localized": true } }, "component": "meta.seo" } } // ... } ``` #### Upcoming Match (Single Type) ```json { // ... "attributes": { "title": { "type": "string" }, "number_of_upcoming_matches": { "type": "integer" }, "next_match": { "type": "date" } } // ... } ``` ### API Customisations #### Config found at `template/src/api/config` ##### Rate Limit ###### Usage ```ts async function toggleRateLimiting(page, enabled = true) { await page.request.fetch('/api/config/ratelimit/enable', { method: 'POST', data: { value: enabled }, }); } ``` ###### What does it do? This endpoint can be used to enable or disable the rate limiting middleware in strapi. When enabled login requests for each user are limited to 5 in 5 minutes. ###### Why do we have it? There are cases where we disable the rate limit to test multiple incorrect login attempts. ##### Admin Auto Open ###### Usage ```ts bootstrap({ strapi }) { strapi.service('api::config.config').adminAutoOpenEnable(false); }, ``` ###### What does it do? This endpoint can be used to enable or disable admin auto open. ###### Why do we have it? It can be frustrating to work with the e2e tests locally. If auto open is set to true a browser window will open each time you run the e2e tests as the strapi app starts for the first time. Because of this we disable it during the bootstrap phase of the test app instance. --- ## Data Transfer ### Overview This document explains how and why we use `@strapi/data-transfer` as means to reset and seed the database for end-to-end tests. It is not a comprehensive explanation of how to use `@strapi/data-transfer`. See the [Strapi documentation](https://docs.strapi.io/developer-docs/latest/developer-resources/data-management.html) to learn more about the feature. #### Why use Data Transfer? We could use custom API endpoints of the application, and whilst this isn't a poor solution, it would _most likely_ require some code writing to set up the data for the schema entries. However, in `4.6.0` Strapi released the `DTS` feature (DTS – Data Transfer System). This means any member of Strapi can export the data of their instance and we can import it programmatically to restore the database to this point in time and ensure a "pure" test environment. End-to-end fixtures live under `tests/e2e/data` as **unpacked export directories** (same layout as inside a Strapi `.tar`: `metadata.json`, `schemas/`, `entities/`, `links/`, `configuration/`, `assets/`, …). That way JSON and JSONL changes show up as normal text diffs in Git. Binary media stays under `assets/uploads/` (ignored by Prettier). #### Limitations of Data Transfer Making changes to the data set should still be done with care: unknown changes to the database can impact other tests. Review JSONL and metadata diffs when updating fixtures. ### Updating data for tests Each test should be isolated and not depend on another test. Data changes from one test should not leak into another test. For example, if you create a new entry for a content-type in one test case, it should not be present in the next test case. This makes tests more stable and easier to debug. #### The data transfer engine Since the Strapi CLI will use `@strapi/data-transfer` directly it will by default not import or export admin users, API tokens, or any other features that have been included in its exclusion list. For this reason, do NOT use the import or export command on the strapi test instance. A DTS engine has been created specifically for our tests cases. This allows us to redefine what should be included in the import or export for our tests. Helpers live in `tests/utils/dts-import.ts` (see also `tests/e2e/scripts/dts-export.ts` for exporting updated packets). #### Importing an existing data packet When you need to update the data packet for a new test, you will first need a Strapi app with the data currently used in end-to-end tests. When running the `yarn test:e2e` command, test app instances are created in `test-apps/e2e/test-app-{n}`. You can use one of the these apps to update the data. For **EE** features, use the same **`STRAPI_LICENSE`** as for e2e: set it in **`tests/e2e/.env`** (see [E2E setup → environment variables](./00-setup.md#e2e-ce-ee-env)) or **export** it in your shell for the manual `dts-import` / `dts-export` commands below (those scripts do not load `tests/e2e/.env` by themselves). Navigate to one of the test-apps and run `yarn install && yarn develop` Leave the development server running, and then run the following command to reset and seed the database with the current e2e data packet. The script expects the name of the data packet you want to import found in `tests/e2e/data`. ```shell STRAPI_LICENSE= npx ts-node /dts-import.ts with-admin ``` This script will include admin users and all the content-types specified in `tests/e2e/constants.ts` You should be able to login with the test app instance credentials. | Email | Password | | ---------------- | ----------- | | test@testing.com | Testing123! | Now that you have a Strapi instance with the same data that each e2e starts with, you can modify the data in the CMS to prepare for a new data export. > **Note:** If you change any of the content schemas (including adding new ones) be sure to [update the `app-template`](./01-app-template.md) otherwise DTS will fail to import the data for schemas that do not exist. #### Exporting an updated data packet Once you've created your new data from the test instance, you'll need to export it so it can be used in the end-to-end tests. Content types from the test app template are included automatically. If the data packet needs a new system or plugin content type, add it to the explicit `ALLOWED_CONTENT_TYPES` entries in `tests/e2e/constants.ts`. The script accepts the backup destination filename as an argument. Run it from the directory of the strapi instance you created earlier based on the test-app template. ```shell npx ts-node /dts-export.ts updated-data-packet ``` If you are exporting data for an EE feature you will need to run the script with the `STRAPI_LICENSE` env ```shell STRAPI_LICENSE= npx ts-node /dts-export.ts updated-data-packet ``` The script will create a file `updated-data-packet.tar`. Extract it into a directory (same layout as Strapi’s export) and add that directory under `tests/e2e/data` (for example `tests/e2e/data/updated-data-packet/`) so tests can import from the folder and you can review changes as text. Remove the `.tar` after extracting if you only keep the unpacked tree in Git. #### Importing the data packet in test scenarios There's an abstraction for importing the data programmatically during tests named `resetDatabaseAndImportDataFromPath` in `tests/utils/dts-import.ts`. Pass the name of a directory under `tests/e2e/data` (for example `with-admin`). You can still pass a path to a `.tar` file; the helper picks the file or directory source provider automatically. Typically, you'll want to run this **before** each test: ```ts import { test } from '@playwright/test'; import { resetDatabaseAndImportDataFromPath } from './utils/dts-import'; test.describe('Strapi Application', () => { test.beforeEach(async ({ page }) => { await resetDatabaseAndImportDataFromPath('with-admin'); await page.goto('/admin'); }); test('a user should be able to...', async ({ page }) => { // my test }); }); ``` --- ## Testing This page lists the test types of the monorepo and the command that runs each one. Run the commands at the repo root. ### Unit tests Unit test files live in `__tests__/` folders inside each package. Run them first, because they are the fastest. ```bash yarn test:unit yarn test:unit:watch yarn test:unit:update # update snapshots ``` ### Front-end tests Front-end tests cover the admin panel. Their files also live in `__tests__/` folders inside each package. ```bash yarn test:front # IS_EE=true: Enterprise Edition features enabled yarn test:front:ce # IS_EE=false: Community Edition only yarn test:front:update # update snapshots (EE) yarn test:front:update:ce # update snapshots (CE) ``` ### Type checking ```bash yarn test:ts # all packages, front-end and back-end ``` ### API integration tests The integration tests live in `tests/api/`. They need a generated Strapi test app. Always regenerate the app with `yarn test:generate-app` instead of reusing an old one. An old app causes misleading failures. ```bash yarn test:api # SQLite yarn test:api --db=postgres yarn test:api --db=mysql yarn test:api -u # update snapshots ``` [CONTRIBUTING.md](https://github.com/strapi/strapi/blob/develop/CONTRIBUTING.md) describes how to generate the app for each database and how to run the suite for the Enterprise Edition with `STRAPI_LICENSE`. ### CLI tests The CLI tests live in `tests/cli/`. They use Jest. They share the app template, the utilities and the runner (`tests/scripts/run-tests.js`) with the E2E tests. ```bash yarn test:cli yarn test:cli:debug # with debug output yarn test:cli:update # update snapshots ``` ### E2E tests The E2E tests use Playwright. The specs live in `tests/e2e/tests/`, in one folder per domain, such as `admin`, `content-manager` and `i18n`. ```bash yarn playwright install # one-time browser install yarn test:e2e --setup --concurrency=1 # run all domains one after the other yarn test:e2e --domains content-manager admin # run some domains yarn test:e2e --concurrency=3 # run three domains in parallel ``` If you change the app template or hit setup problems, run `yarn test:e2e:clean` to remove the generated test apps before the next run. Read these pages before you write or change an E2E test: - [E2E setup](./e2e/00-setup.md): Playwright install, Enterprise Edition runs, runner options and environment variables. - [E2E app template](./e2e/01-app-template.md): how the shared app template feeds the generated test apps. - [E2E data transfer](./e2e/02-data-transfer.md): the data transfer workflows that the tests rely on. ### Before you open a pull request Run at least: ```bash yarn test:unit && yarn test:front && yarn test:ts && yarn lint && yarn prettier:check ``` CI also runs the E2E tests on every pull request. They are slow. Use `--domains` to run only some domains locally. --- ## FE Coding Guidelines ### Coding Style #### Application Structure The FE of Strapi aims to be a domain driven application such as we heavily utilise the concept of nested routes: ```shell pages/ ├─ index.tsx ├─ settings/ │ ├─ pages/ │ │ ├─ index.tsx │ │ ├─ users/ │ │ ├─ roles/ │ │ │ ├─ create.tsx │ │ │ ├─ :id.tsx │ │ │ ├─ index.tsx ``` As part of this effort, assuming a component is not used anywhere else it should live as close as possible to the section of the app it's used in: ```shell pages/ ├─ index.tsx ├─ settings/ │ ├─ components/ │ │ ├─ SettingsNavigation.tsx │ ├─ hooks/ │ │ ├─ useSettings.ts │ ├─ pages/ │ │ ├─ index.tsx │ │ ├─ users/ │ │ ├─ roles/ │ │ │ ├─ create.tsx │ │ │ ├─ :id.tsx │ │ │ ├─ index.tsx ``` When components/hooks etc. are required higher in the tree, we can subsequently move them higher up as necessary. But we strive for colocation. #### Exports Prefer named exports over default exports. Why? Because Strapi is a _very_ large codebase. If you export something as `default` someone can import it with a different name: ```jsx // button.tsx const MyButton = (props) => ; ``` Above is a simple component for illustration purposes, but you'd be likely to use the `MyButton` component in _a lot_ of places, so what happens if you want to replace all instances with a new v2 Button? If the naming is not as consistent, it's typically a lot more frustrating & can lead to bugs. #### Utilities / Helpers When you need a utility function ask yourself a question – do I need this _anywhere_ else? If you don't (which is fairly typical) include it in the file you're using it. As a guide, we expect anything exported to be unit-tested and if a utility or hook is unique and bespoke to the component you're creating then it's advised to test that code in the context of said component, this is covered in the Testing guides below. If you do need to use the utility elsewhere then follow the same practices shown in [Application Structure](#application-structure) keep it as close to where it's used as possible. Avoid re-exporting them from a generic `index` file. However, you might find it useful to group them under a generic file name such as `strings.ts` which would include all the `string` utils you have. #### Components & Hooks With components and hooks, to improve code discoverability, opt for this pattern instead: ```shell hooks/ ├─ tests/ │ ├─ useContentManager.test.ts ├─ useContentManager.ts ``` #### Memoization Premature optimisation is the root of all evil. Performance of the application _is_ important however it should be measurable before we attempt to solve the issue as these functions _are not free._ Take this example: ```jsx const FormButton = () => { const handleSubmit = useCallback((entity) => { post('/entity/create', { entity, }); }, []); return ; }; const Button = ({ onClick, children }) => ; ``` The component we're passing to is _very_ simple & `useCallback` is unnecessary here. Because we do not need the function identity to be stable. If we were to use the callback in a `useEffect` hook then yes, we would want the function to be stable otherwise every render would cause the `useEffect` hook to run which could be problematic. So what about `useMemo`. Same thing, it's for heavy computational processes e.g. not this: ```jsx const Component = ({ entities = [] }) => { const entityLength = useMemo(() => entities.length, [entities]); //... }; ``` The above function isn't expensive but more importantly we don't know what impact it has without measuring it. Therefore, avoid using `useCallback` and `useMemo` unless you _know_ you need it. ### Dependencies Generally speaking, always opt to use native-browser APIs where possible over library code, the platform is powerful and leveraging it correctly will handle _a lot_ of edge cases you probably won't have considered. One example of this would be [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) which is a completely drop-in solution for `lodash/cloneDeep`. If you're not sure whether you can use a browser API feel free to use [caniuse.com](https://caniuse.com/), a powerful resource that gives you a breakdown of the availability of an API across browsers: By default, we use `browserslist` to define the browsers we support, they're defined as: ```shell last 3 major versions Firefox ESR last 2 Opera versions not dead ``` It's important to note that this informs the JS transpilation step by esbuild, so if a browser does not support it, it _may_ be polyfilled. #### A special note on `lodash` We target modern browsers, typically speaking these browsers have all the functionality lodash provides natively, albeit there may be some discrepancies between the functionality or API signature. Because of this and our general view of shipping as little javascript to the client as possible we should **never be adding a `lodash` function to our code**. In addition, when working on the codebase everyone should strive to remove the instances we currently have. A common use case is the `get` function: ```jsx import get from 'lodash/get'; const extractDeepAttribute = (schema: object, path: string[]) => { return get(schema, ['attributes', ...path], {}); }; ``` But if we zoom out we can begin to question a few things: - why are we extracting something so deep here? - are my data structures the issue? i.e. to complex - can i just use [`optionalChaining`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)? Typically, I think you'll find that rethinking how the code works will reveal an easier to manage data structure that from there you can probably use `optionalChaining`. If you're struggling to convert a `lodash` usage to native code, check out [this helpful guide](https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore). #### Adding new dependencies Sometimes you require a new dependency, it happens, it's common. Third party library code is _fantastic_ because typically it's somewhat battle tested in production environments and in some circumstances where the concept is complicated e.g. `fractional-indexing` it really is easier to use a library than re-write and maintain in our application. It's a case by case basis therefore, when adding a new dependency to the codebase you should do the following: - Discuss it with at least one other developer from a different squad – most appropriately through the PR - Add as a talking point to the next FE sync so other developers have visibility You'd be surprised how often a library can be replaced with a native API and through thinking and acting in this way we hope to keep our dependency surface as small as possible. ### Data fetching #### V4 Whilst we are actively developing V4 of the CMS we should _always_ be using `react-query`'s hooks to manage the fetching and mutation of data in conjunction with `useFetchClient`. This is because `react-query` caches our data-fetching meaning multiple calls are not necessarily made unless a mutation occurs as well as handling various intermittent states and events during the fetching lifecycle. :::note We're using `react-query` version 3, see the specific docs – https://tanstack.com/query/v3/docs/react/overview ::: There are a few guidelines to keep in mind when writing your query-keys - Use the URL fragments from the called URL e.g `/admin/content-manger/:id/stages -> ['content-manager', id, 'stages']` with the exception of `admin` - All ee calls should be prefixed with ee, to be able to invalidate it all at once - All dynamic variables need to be part of the dependency array You can read more on query-keys for `react-query` [here](https://tanstack.com/query/v3/docs/react/guides/query-keys). ##### Example ```jsx const useLicenseLimits = () => { const { get } = useFetchClient(); const { data: license } = useQuery(['ee', 'license-limit-info'], async () => { const { data: { data }, } = await get('/admin/license-limit-information'); return data; }); return { license }; }; ``` #### V5 In V5 we'll be looking at using `react-router-dom`'s loaders function to avoid data-waterfalls // render as you fetch. For mutations we'll be using `@redux/toolkit` and subsequently it's caching mechanisms making `react-query` redundant. :::note 🚧 This section will be filled in further upon delivery of v5. ::: ### Managing state #### Local / Global / URL state In the following paragraph we discuss three different types of state – Local which commonly is defined by using `React.useState`, Global which in our case involves using `redux` and URL based state e.g. a path parameter or search params. **Local State** This is the typical place to start when you need to make your app “react” to a change, for example, through event inputs e.g “A user presses a button and a modal opens”: ```jsx import { useState } from 'react'; const MyButton = () => { const [isOpen, setIsOpen] = useState(false); const handleClick = () => setIsOpen((currOpen) => !currOpen); return ( ); }; ``` Above is a simple example of using local state, it's isolated to the component and we're not typically passing this further down the tree. It's important to remember that you can very easily derive state from either your current state or props that are passed e.g. you render a table of entries and you want to know the count of entries that are “published” you could create a new state value, but that's not necessary, we can do a calculation because the root of the data is based off the props thus when the props update so will the count: ```jsx const MyTable = ({ entries = [] }) => { const publishedCount = entries.filter((e) => e.isPublished).length; return (

You have {publishedCount} published entries

); }; ``` Always aim to derive state as opposed to setting state where possible! **Global state (redux)** When's the right time to use global state? Well first if we look at how you can pass state around your app we first look at prop drilling. This can become verbose and inconvenient when you start to pass it down past the first child. Often composability can be incredibly useful for these scenarios where this: ```jsx import { useState } from 'react'; const App = () => { const [someState, setSomeState] = useState('some state'); return ( <>
); }; ``` can easily become this: ```jsx import { useState } from 'react'; const App = () => { const [someState, setSomeState] = useState('some state'); return ( <>
); }; ``` But in the above example, we're still passing it around. Other times composability will not help you because it doesn't make sense or perhaps the data is fetched from one part of an application and another requires it later / now. For data fetching you could consider using something like `react-query` because it has it's own data caching system. Other times such as the Content Manager's edit view, it's managing complex nested browser form state and in these circumstances you'd use a global state management solution like redux. The other plus side to a global state manager like redux is that it's contained, it's easily debuggable because all the state & changes can be monitored closely and reviewed piece by piece. It also enforces uni-directional data flows & unlocks powerful features such as “redo/undo”. **URL state** If we have local, derived & global state then why would we want URL state? Consider this, you've created a very complicated view of your a table, it has the exact columns you want to show and how to show them and you need to show this someone page 3 – but they're not near you. This is where URL state comes in. If we store the pagination state in the URL we can _easily_ share the exact page of a table. If we store the sorting & filtering of the table then anyone can see the view you're directly on. The bottom line is – global & local state do not transcend the machine you're working on. In all reality, this is typically how web apps worked before the javascript ecosystem “got big” and most importantly, we're just leaning on the platform as much as possible to do all our heavy lifting whilst giving our users a 10x better experience. #### Avoid effects Effects are tricky, when should you use them? React's documentation describes them as an escape hatch to step outside of the react paradigm to sync with “external” systems such as the network of DOM. If you're using them to make changes to your component code or respond to user-events then you've probably made a mistake in your code and you should review this. If you're struggling to work with effects and worry you're using too many of them, check out the react docs. Overall, we advise you avoid effects where you can, especially for data-fetching when you really get into it, it's not a trivial problem to circumvent and there's a reason why we recommend using a data-fetching library like `react-query` or `@redux/toolkit`. #### Using context effectively `React.Context` is another powerful tool to avoid prop drilling and employ a version of global state, after all, `redux` is based on `React.Context`. But knowing when to use it is important. We advise avoiding it for complex nested data-structures like the `content-manger` edit view because context has no selector pattern (at time of writing) which means one change to the context will re-render every component connected – this could be expensive. What it's great for is complex components using composable design, take this simplified example: ```jsx // Table.tsx import { useContext, createContext } from 'react' const TableContext = createContext() const useTableContext = () => useContext(TableContext) const Root = ({ isLoading, children }) => { const contextValue = useMemo(() => ({ isLoading }), [isLoading]) return ( {children} ) } const Loading = () => { const { isLoading } = useTableContext() if(!isLoading){ return null } return (
) } export const Table = { Root, Loading, } // something-else.tsx import { useQuery } from 'react-query' import { useFetchClient } from '@strapi/helper-plugin' import { Table } from './Table' const MyPage = () => { const { get } = useFetchClient() const { isLoading, data } = useQuery(async () => { const { data } = await get(ENDPOINT) return data }) return ( {data.map(datum => } ) } ``` In the above example we don't need to worry about when to render the loading state because the table extracts all this and we can focus on composing our views in a readable manor. #### Useful articles - https://kentcdodds.com/blog/application-state-management-with-react - https://react.dev/learn/you-might-not-need-an-effect ### Unit Testing :::note We'll write a section on E2E tests when we're there. ::: #### When & why should you write tests? As a rule of thumb you should add tests – whenever you add _new_ code. It's even better if you add them when refactoring and you notice the tests are either “light” or non-existent. They're incredibly valuable in stopping accidental regressions (which happen and that's okay). No test suite is perfect and there's always room to improve. If you're fixing a bug consider writing a test that addresses the issue you're trying to solve to assert it's failing & then you have an automated way to confirm your fix works as expected. #### What makes a good test? Good FE tests avoid testing implementation details, the tests often break when refactoring code in an arbitrary manor whilst not actually showing issues when a user interacts with the component. This is why we aim to test our components as if we're a user, it supports our ethos of test writing should give us confidence that our application will work when a _user_ uses it. Because we're using the Testing Library suite of tools, they have an incredibly [handy document](https://testing-library.com/docs/queries/about/#priority) surrounding what order of queries to try and use. The main takeaway would be to try and avoid using `getByTestId` where possible, it should be considered a last resort because it doesn't test any of the user implementations and more than likely you're testing code that is not visible to the user correctly and in that scenario we should be asking “why is this necessary?”. Also, avoid snapshotting the DOM except for _small_ circumstances, snapshotting is great for data-structures where it's easy to skim and understand, it's also great for analysing a very small component e.g. an `svg` with a single `path`. However, often we're testing very large complicated compositions of components and the snapshot of said component is typically very large and hard for a developer to skim and understand the impact of their changes. If we're being honest with ourselves, no one reviews should enormous snapshot changes either. #### Mocking Avoid it if you can. Although realistically, it's not always possible. Lets first look at API mocking; try to avoid mocking your “fetch” library whether that's `react-query` or `axios` or native fetch. It makes a brittle test and doesn't behave 1:1 as the library would with the network request. Instead, prefer using Mock Service Worker ([MSW](https://mswjs.io/)) to mock/intercept the API request calls. The largest benefit is because all we're doing is locating our API responses in one space that can be shared across tests to avoid reimplementation and therefore less work to edit them should the API response change. When looking at `module` mocking: ```jsx jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), useNotification: jest.fn(() => ({ toggleNotification: jest.fn(), })), })); ``` We should continue to avoid it where possible, in the above example there's no real reason to mock `useNotification`, it's more than likely the wrapper just needed the correct Provider to be added to avoid any errors and more importantly we're now not able to test our notification is visible to the user – testing it's fired is an implementation detail, how can we be sure it's visible? If you do mock a module try to create a manual mock instead under the `__mocks__` folder if possible. #### Useful Articles - https://kentcdodds.com/blog/testing-implementation-details - https://testing-library.com/docs/queries/about/#priority ### A11y Semantics are important for users, just because the application is “not on the web” does not mean our users are “fully abled” (do not have any type of disability). Therefore when building UI aim to use the correct semantic elements in the correct places, typically we use the `design-system` to build UI in the CMS, so most of the accessibility work is done there. But, accessibility is not easy – this is why we test from a user perspective as described in the “Testing” section. If you're not sure about accessibility patterns check out these resources: - https://www.w3.org/WAI/ARIA/apg/ - https://webaim.org/ - https://www.a11yproject.com/ ### Conventions #### Submitting a PR When submitting an FE related PR ensure you've done the following: - Follow the template, especially including a clear description, it's a history for future developers understanding the context around a decision. - Update or add tests for your relevant code, for more help on this consult the [Testing Section](#unit-testing) - Always consider whether adding contributor documentation is helpful for the work you've done e.g. you've created a new hook that can be used across the app - Get the relevant reviewers – engineers in your squad is a good start, but if you're working in a space of the codebase you're not as familiar with e.g. `entity-service` consider getting engineers who have more experience in the space. --- ## Typescript 🚧 coming soon 🚧 --- ## Working with the Design System ### Linking the Strapi Design System Follow these steps to use a local version of the Strapi design system with the Strapi monorepo In your copy of the design system run `yarn build` to generate the bundle. In the Strapi monorepo link your local copy of the design system with [`yarn link`](https://yarnpkg.com/cli/link#gatsby-focus-wrapper): ``` yarn link -r ../ ``` Running yarn build in `examples/getstarted` should now use your local version of the design system. To revert back to the released version of the design system use [`yarn unlink`](https://yarnpkg.com/cli/unlink#usage): ``` yarn unlink ../ ``` --- ## Writing docs This page is the source of truth for the conventions of these docs. It applies to people and to agents. The site is in `docs/` and the content is in `docs/docs/`. The file [`docs/AGENTS.md`](https://github.com/strapi/strapi/blob/develop/docs/AGENTS.md) summarizes the rules. ### Where content goes Readers pick a section by intent. Put a page where its reader looks for it. | Section | Reader intent | Folder | Examples | | ------------- | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------- | | Contributing | Do a task in the monorepo (how-to) | `docs/docs/contributing/` | Testing, TypeScript conventions, this page | | Architecture | Understand how several packages work together | `docs/docs/architecture/` | Authentication, future flags | | Packages | Understand or look up one package | `docs/docs/packages//` | `packages/core/database` is documented in `packages/core/database/` | | API reference | Look up the `Strapi` class and its parts | `docs/docs/api/` | Container, event hub | Design proposals (RFCs) are not in this repository. They happen in [GitHub Discussions](https://github.com/strapi/strapi/discussions/categories/rfcs). - Packages mirror repo paths. The docs of `packages/core/database` live in `docs/docs/packages/core/database/`. Anyone can find them without searching. - A topic that crosses package boundaries goes in Architecture. Link to it from the package pages that it concerns. - Prefix files and folders with `01-`, `02-` to set the sidebar order. The prefix is not part of the URL. An `index.md` is the landing page of its folder. - Do not set `slug`. The URL follows the file path. Only `docs/docs/api/` sets `slug`, to keep its existing URLs. ### Markdown or MDX `docs/docusaurus.config.ts` sets `markdown.format: 'detect'`. A `.md` file is CommonMark. A `.mdx` file is MDX. - Write `.md` by default. - Use `.mdx` only when the page imports a component, for example `import PackageTable from '@site/src/components/PackageTable'`. See `docs/docs/contributing/08-docs-health.mdx`. - Admonitions (`:::note`, `:::caution`) work in `.md`. - In MDX, `{` and `<` in text start an expression or a tag. Escape them or put them in code. - Never keep knowledge only inside a component. A component may render data, but the page must also state the facts in text. Readers on GitHub and agents read the source files and do not run the site. ### Frontmatter Every page starts with frontmatter. | Key | Required | Value | Notes | | --------------- | ------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `title` | Yes | String | The page title. | | `description` | For new pages | One or two factual sentences | Feeds `llms.txt`, the page metadata and the cards of generated index pages. Keep it under 150 characters, because `llms.txt` cuts longer text. The build does not check it. | | `sidebar_label` | No | String | A shorter label for the sidebar. | | `tags` | No | List of tag keys from `docs/docs/tags.yml` | An unknown tag fails the build. | | `package` | Package pages only | The npm name of a workspace package, for example `@strapi/database` | Must match the `name` in the `package.json` of a workspace under `packages/`. | | `status` | No | `stub`, `draft` or `needs-review` | No `status` means the page is trusted. | | `review_notes` | No | List of strings | Why the page is flagged. Shown in the banner and on the Docs health page. | Docusaurus keys such as `sidebar_position`, `hide_title` and `hide_table_of_contents` are also allowed. Use them only when you need them. The build throws when: - `status` is not one of the three values. - `review_notes` is not a list of strings. - `package` is not a workspace package under `packages/`. - Two pages set the same `package`. - `tags` contains a key that is not in `docs/docs/tags.yml`. The build only warns when a workspace package has no page. ```yaml --- title: Example page description: What this page explains, in one or two factual sentences. tags: - backend status: draft review_notes: - The retry rules are not verified against the code. --- ``` ### Page status A page with no `status` is trusted: someone checked it against the code. Any page that is not verified must carry a status. | Status | Meaning | Use it when | | -------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `stub` | Headings only, no real content. | You create a placeholder. `yarn scaffold:packages` sets it. | | `draft` | Content exists but is incomplete or not verified against code. | You write a page and did not check every claim, or you seed a page. | | `needs-review` | Existing content that is suspected to be stale. | You find outdated claims and cannot fix them now, or your change makes them stale. | `review_notes` explains the flag. Write one item per problem. Name the claim or the missing part, and where to check it. Do not write "TODO" or "outdated". A theme wrapper (`docs/src/theme/DocItem/Content/`) renders a banner above the page. The banner shows the status, the notes, and a link to [Docs health](./08-docs-health.mdx). That page lists every flagged page with its notes, and every package that has no page or only a stub. It is generated at build time. To promote a page to trusted: 1. Read the code that the page describes. 2. Check each claim: names, paths, flows, examples and commands. Run the commands and examples when you can. 3. Fix or delete what is wrong. Add what is missing. 4. Remove `status` and `review_notes` in the same pull request. 5. State in the pull request description what you verified. Never remove a status only to hide the banner. If you change code that a page describes, update the page. If you cannot, set `status: needs-review` and add a note that says what changed. ### Package pages Each workspace package under `packages/` has one page at `docs/docs//index.md`. Other pages of the package go in the same folder. Only `index.md` sets `package`. Package pages are plain `.md`. The scaffold script creates this template: ```md --- title: '@strapi/database' sidebar_label: database description: package: '@strapi/database' status: stub --- ## Purpose ## Key concepts ## Related ``` Each heading carries one italic guidance line while the page is a stub. Replace the guidance with content, and keep the three headings: - **Purpose**: what problem the package solves, and who relies on it. - **Key concepts**: the main abstractions a contributor needs before changing the code. Link to the sub-pages of the package. - **Related**: architecture pages and sibling packages. Do not repeat the dependency lists from the package header. The `package` key drives the package header. The theme wrapper renders it between the title and the content. It shows the npm name, version, private flag, a link to the folder on GitHub, the description from `package.json`, the internal dependencies and dependents, and a link to the [package map](../architecture/01-package-map.mdx). Do not repeat this data in the page text. To add docs for a new package: 1. Add the package under `packages/` with a `name` and a `description` in its `package.json`. 2. Run `yarn scaffold:packages` in `docs/`. The script is idempotent. It creates the missing pages and the `_category_.json` of each group folder. It never changes an existing file. 3. Fill in the page and set the honest status. A new group folder under `packages/` must be added to `PACKAGE_GROUPS` in `docs/plugins/workspace-packages/groups.mjs`, or the build throws. Workspaces outside `packages/`, such as `examples/`, have no package pages. ### Links - Link to pages with relative file links that include the extension: `[Package map](../architecture/01-package-map.mdx)`. The build checks them (`onBrokenLinks` and `onBrokenMarkdownLinks` are `throw`). They also work on GitHub and in editors, and they survive a URL change. - Broken anchors only warn. Read the warnings anyway. - Link to code with `https://github.com/strapi/strapi/blob/develop/` for a file and `https://github.com/strapi/strapi/tree/develop/` for a folder. The site cannot resolve paths outside `docs/docs/`, and the site deploys from `develop`. - Do not use line anchors. Lines move with every change, and the link then points at the wrong code without any error. Name the file and the symbol in the text instead. - Repo-root markdown that the site imports, such as `CONTRIBUTING.md`, may use repo-relative links. `docs/repo-root-markdown-links.ts` rewrites links to a page under `docs/docs/` (`.md` or `.mdx`) into site links that the build checks, and every other link into a GitHub `develop` URL. ### Diagrams Write diagrams as `mermaid` code fences. The config enables them (`markdown.mermaid: true`). The source is plain text, so reviewers can diff it and agents can read it. For an example, see `docs/docs/packages/core/permissions/index.md`. ````md ```mermaid graph LR A[Request] --> B[Policy] --> C[Controller] ``` ```` ## Images Prefer a mermaid diagram when a text diagram is enough. For other images: - Put the file in `docs/static/img//`, where `` is the package or topic folder, for example `docs/static/img/database/`. - Reference it with an absolute path and always write alt text: `![Reordering example](/img/database/reordering.png)`. - Keep the editable source (for example a `.drawio` or `.excalidraw` file) next to the exported image, with the same name. ## Tags Tags are cross-cutting labels such as `backend`, `frontend`, `rbac` and `testing`. The vocabulary is in [`docs/docs/tags.yml`](https://github.com/strapi/strapi/blob/develop/docs/docs/tags.yml). The config sets `onInlineTags: 'throw'`, so a tag that is not in that file fails the build. To add a tag, add an entry with a `label` and a `description`. Keep the list short. Do not add a tag that repeats a package folder name, because the folder is the navigation. Do not add a tag for a status, because `status` covers it. ## Components | Component | What it does | | --------------- | -------------------------------------------------------------------------- | | `PackageGraph` | Interactive map of the workspace packages and their internal dependencies. | | `PackageTable` | Server-rendered table of the packages, grouped by folder. | | `PackageHeader` | Package header, rendered from the `package` key. Pages do not import it. | | `DocsHealth` | Report of the flagged pages and of the packages without docs. | To add a component: 1. Create it in `docs/src/components//`. 2. Import it from an `.mdx` page: `import Name from '@site/src/components/Name';`. 3. Read data from repo files, such as `package.json` or frontmatter. Load it in a plugin, as `docs/plugins/workspace-packages/` does, and read it in the component with the hook in `docs/src/lib/workspace-packages.ts`. 4. State the same knowledge in the page text (see [Markdown or MDX](#markdown-or-mdx)). 5. Run `yarn tsc --noEmit` in `docs/`. ## Moving or renaming pages Old URLs must keep working. When you move, rename or delete a page: 1. Add an entry to `docs/redirects.ts`: `{ from: '/old/path', to: '/new/path' }`. Use site URLs, without numeric prefixes or extensions. Never list a `from` that is still a route. For a deleted page, redirect to the closest page. 2. Fix the relative links to the page. The build lists the ones you miss. 3. Fix the references outside `docs/`. Run `rg 'docs/docs/' --glob '!docs/**'` at the repo root. Typical places are `AGENTS.md`, `CONTRIBUTING.md`, `tests/e2e/README.md` and code comments. Redirects only work in a production build. `yarn start` does not serve them. Test them with `yarn build && yarn serve`. ## Local workflow Run these in `docs/`: ```bash yarn install # once yarn start # dev server with live reload yarn build # production build; fails on broken links and invalid frontmatter yarn serve # serve the production build (redirects work here) yarn scaffold:packages # create the missing package pages yarn tsc --noEmit # type-check the components and plugins ``` Format changed files from the repo root: `yarn prettier --write `. CI runs `yarn build` when a pull request changes `docs/` (job `docs_build` in `.github/workflows/tests.yml`). The deploy runs on each push to `develop`. ### Writing style - Use plain technical English. Prefer short words. - Use the active voice. Write one idea per sentence. - Write headings in sentence case. - Explain why, not only what. - Point to the code instead of restating it. Name the real path, command or symbol. - Do not use em dashes. Start a new sentence or use a comma. - Do not use filler or promotional words. ### Docs and agents `docs/` holds knowledge: what Strapi internals are and why they work that way. It serves people and agents. `AGENTS.md` at the repo root holds the rules for agents that work in the repo. `docs/AGENTS.md` holds the rules for editing these docs. `.ai/skills/` holds procedures that tell an agent how to do a task. The skill [`contributor-docs`](https://github.com/strapi/strapi/blob/develop/.ai/skills/contributor-docs/SKILL.md) is the procedure for this guide. A skill links to the docs and never copies them. Two copies drift apart. The site publishes `/llms.txt` and `/llms-full.txt`, built from the page sources. They list pages by title and description, so write a precise `description`. An agent that works in a checkout reads the `.md` sources under `docs/docs/` directly. Before it changes a package, it reads the page in `docs/docs/packages//` and checks the `status` of that page. --- ## Docs health import DocsHealth from '@site/src/components/DocsHealth'; This report is generated at build time from the `status` and `review_notes` frontmatter of every page, and from the `package` frontmatter of the package pages. - `stub`: headings only, no real content yet. - `draft`: content exists but is incomplete or not verified against the code. - `needs-review`: existing content suspected to be stale; `review_notes` names the claims to check. --- ## Contributing This section is for people who change the Strapi monorepo. It answers how to do things. To understand how Strapi works, read [Architecture](../architecture/index.md) and the [package pages](../packages/index.mdx). ### Start here - [Contributing to Strapi](./01-contributing-guide.mdx): the contribution process, from issue to pull request. - [Code of conduct](./02-code-of-conduct.mdx): the rules for taking part in the community. ### Testing - [Testing](./03-testing/index.md): the test types and the command that runs each one. - [E2E setup](./03-testing/e2e/00-setup.md): install Playwright and run the end-to-end tests. - [E2E app template](./03-testing/e2e/01-app-template.md): how the shared app template feeds the generated test apps. - [E2E data transfer](./03-testing/e2e/02-data-transfer.md): import and export the data set that the tests rely on. ### Conventions - [Frontend guidelines](./04-frontend-guidelines.md): how to write admin panel code. - [TypeScript](./05-typescript.md): typing conventions. - [Working with the design system](./06-design-system.md): using `@strapi/design-system` in the admin panel. ### These docs - [Writing docs](./07-writing-docs.md): where content goes, frontmatter, page status, links and the local workflow. - [Docs health](./08-docs-health.mdx): the pages flagged as stub, draft or needs-review. ### Proposing a change Design proposals (RFCs) happen in the [RFCs category of GitHub Discussions](https://github.com/strapi/strapi/discussions/categories/rfcs), not in this repository. --- ## @strapi/admin-test-utils ### Purpose Provides test environment setup, fixtures, and utilities for Strapi admin panel tests. Used internally by the monorepo to configure Jest, mock Node globals, and supply realistic test data. ### Key concepts - **Jest setup**: Environment setup, global test configuration, and Jest patches for DOM, fetch, and request stashing. Exports via multiple entry points: `.`, `/after-env`, `/environment`, `/file-mock`, `/global-setup`. See [src/](https://github.com/strapi/strapi/tree/develop/packages/admin-test-utils/src). - **Test fixtures**: Pre-built data for collection types, permissions, and metadata (admin, content-manager, content-type-builder, documentation). - **Node patches**: Globals, types, response/request body patches for test compatibility. ### Related - [Testing](../../contributing/03-testing/index.md): front-end test execution. --- ## @strapi/cloud-cli ### Purpose Provides CLI commands for Strapi app developers to manage Strapi Cloud projects, deployments, and environments. Integrated into the main Strapi CLI. ### Key concepts - **Cloud commands**: `deployProject`, `link`, `login`, `logout`, `createProject`, `listProjects`, `listEnvironments`, `linkEnvironment`. See [index.ts](https://github.com/strapi/strapi/tree/develop/packages/cli/cloud). - **Command builder**: `buildStrapiCloudCommands()` adds cloud subcommands to the Commander command object at startup. - **Config management**: Local config stores installation ID and authentication state. Services handle API calls to Strapi Cloud backend. ### Related - [Testing](../../../contributing/03-testing/index.md): CLI test infrastructure. --- ## create-strapi ### Purpose Provides an alternative entry point for Strapi app developers to create new projects. Delegates to `create-strapi-app`. Available via `npm init strapi@latest`, `yarn create strapi@latest`, and `npx create-strapi@latest`. ### Key concepts - **Wrapper package**: Delegates to create-strapi-app/bin at [index.js](https://github.com/strapi/strapi/tree/develop/packages/cli/create-strapi). Exists for npm init/pnpm create integration. - **Same functionality**: Offers the same project scaffolding and prompts as create-strapi-app. ### Related - [`create-strapi-app`](../create-strapi-app/index.md): primary package that provides the actual CLI. --- ## create-strapi-app ### Purpose Provides the official CLI for Strapi app developers to create new Strapi projects. Available via `yarn create strapi-app` or `npx create-strapi-app`. Handles project initialization, prompts for configuration, and can integrate with Strapi Cloud. ### Key concepts - **Entry point**: `run()` function processes CLI arguments and guides project creation. See [index.ts](https://github.com/strapi/strapi/tree/develop/packages/cli/create-strapi-app). - **Interactive prompts**: Collects project name, database choice, and optional Cloud integration via `prompts.ts`. - **Dual mode**: Supports local project creation and Strapi Cloud project creation with API integration. ### Related - [Testing](../../../contributing/03-testing/index.md): CLI test infrastructure. --- ## Audit Logs ### Summary Audit Logs provide a way to view the history of all user actions at Admin API level. This includes actions related to entries (including publish actions), media (including its folders), users, login & logout of admin users, components, roles, and permissions, you can see the list of all the default events [here](https://github.com/strapi/strapi/blob/main/packages/core/admin/ee/server/services/audit-logs.js#L9). ### Backend design The Audit Logs feature was built to take advantage of the eventHub. You can find the service and its core code at `packages/core/admin/ee/server/services`. #### Audit Logs Local Provider To save audit log data, we utilize an Audit Logs local provider responsible for interacting with the database. This provider should return an object with a `register` function, and this function should return an object with functions to handle `saveEvent`, `findMany`, `findOne`, and `deleteExpiredEvents`. #### Content types ##### strapi_audit_logs This content type stores all the audit logs. For each allowed event, we save an entry in the audit logs content type. #### Subscribing to all events The Audit Logs feature adds a subscriber to the [EventHub](../../core/event-hub.md), allowing it to listen to all events in the application. However, we don't save every event in the audit logs; we only save the default ones (see the defaultEvents array in the service file). #### Retention days As the number of events in our Audit Logs can grow significantly, we run a daily job at midnight to delete logs that are older than the retention days defined. By default, the retention days are set to 90 days, but this value can be changed. For enterprise self-hosted projects, users can set a configuration variable (admin.auditLogs.retentionDays) and use that one. For cloud projects, the retention days are determined by the license. In a cloud project, users can set a custom retention days in the configuration, but this value cannot exceed the retention days defined by the license. In both cases, if we want to set a custom retention days we can modify the Admin Panel API config file (`./config/admin.js`). You can find all the possible options as well as other configurations for the Admin Panel on the [documentation page](https://docs.strapi.io/dev-docs/configurations/admin-panel#available-options). #### Export Audit logs can be exported as a single CSV file from the list view. The export honours the active filters and requires both the `admin::audit-logs.read` and `admin::audit-logs.export` permissions. Every export also records an `audit-log.export` event. By default, the export refuses to start when the matching set exceeds the maximum number of rows: 1,000,000. Self-hosted projects can change this limit with the `admin.auditLogs.exportMaxRows` configuration variable in the Admin Panel API config file (`./config/admin.js`). #### Audit Logs format Every Audit Log has the following format: ```typescript type Event { action: string, // Name of the event date: Date, // When the event happens userId: number, // Id of the user that trigger the event payload?: Object, // Extra info of the event }; ``` To understand how we obtain this information, we need to know how we emit an event with the Event Hub. To emit an event, we use the following function: (To see more info about the EventHub, click [here](../../core/event-hub.md) ```typescript strapi.eventHub.emit(name: Pick, payload: Pick); ``` First, we check the event is coming from admin requests and it's on our [default events](https://github.com/strapi/strapi/blob/main/packages/core/admin/ee/server/services/audit-logs.js#L9) list, then when creating our Audit Log, we retrieve the action and payload from this emitted event, where the first argument is the action or event name, and the second one is the payload. We obtain the user from the requestContext. --- ## useLicenseLimits An abstraction around `react-query`'s `useQuery` hook to fetch license limits for a project. ### Usage ```js const { license, getFeature, isError, isLoading } = useLicenseLimits(options); ``` ### Options #### `enabled` Boolean flag which is passed onto react-query indicating whether the license check should be performed. This can be useful if e.g. other permissions need to be checked before. ### Result #### `license` An object that contains the raw admin API response. #### `getFeature(name: string)` Returns options for a given feature. If the feature was not found, it returns an empty object. This is mostly a convenience method, to avoid having to filter the features array on the license object every time. ##### Usage ``` const { getFeature } = useLicenseLimits(); const reviewWorkflowOptions = getFeature('review-workflows'); ``` ### Typescript ```ts import { UseQueryResult } from 'react-query'; // Note: the list of attributes might be incomplete interface License { enforcementUserCount: number; currentActiveUserCount: number; permittedSeats: number; shouldNotify: boolean; shouldStopCreate: boolean; licenseLimitStatus: 'OVER_LIMIT' | 'AT_LIMIT'; isHostedOnStrapiCloud: boolean; features: LicenseFeature[]; } interface LicenseFeature { name: string; options?: object; } type Options { enabled?: boolean; } type UseLicenseLimit = (options: Options) => Pick & { license: License, getFeature: (name: string) => object } ``` --- ## useAdminRoles An abstraction around `react-query`'s `useQuery` hook. It can be used to fetch one or more admin role. ### Usage The hooks can receive two optional parameters: 1. query params: an object containing the query params to be sent to the API. They are going to be stringified by `qs`. All params are equal except `id`, which is used to fetch a single users, if it is passed. 2. options: an object containing the options to be passed to `useQuery`. It returns an object containing some of the react-query attributes. ### Typescript ```ts import { UseQueryOptions } from 'react-query' type Role = object; useAdminRoles(queryParams: object, reactQueryOptions: UseQueryOptions): { roles: Role[], isLoading: boolean; error: object; isError: boolean; refetch: () => Promise; }; ``` #### Fetch all roles ```jsx import { useAdminRoles } from 'path/to/hooks'; const MyComponent = ({ onMoveItem }) => { const { roles, isLoading, refetch } = useAdminRoles(); return /* ... */; }; ``` #### Fetch one role ```jsx import { Box } from '@strapi/design-system'; import { useAdminRoles } from 'path/to/hooks'; const MyComponent = ({ onMoveItem }) => { const { users: [user], isLoading, refetch, } = useAdminRoles({ id: 1 }); return /* ... */; }; ``` --- ## useEnterprise A hook that returns either community or enterprise-edition data-structures based on the global `window.strapi.isEE` flag. ### Usage ``` import { CE_DATA } from './data'; function Component() { const data = useEnterprise(CE_DATA, async () => (await import('./ee/data')).default); } ``` It accepts an optional third argument to pass in options customizing the hook behavior: #### `combine()` THe `combine` callback can be used as a custom "merge" function for the ce and ee arguments: ``` const data = useEnterprise({ a: 1 }, () => { b: 1 }, { combine(ce, ee) { return { ...ce, ...ee } } }); console.log(data); // { a: 1, b: 1 } ``` #### `defaultValue` By default the hook returns `null` if `window.strapi.isEE` is true and the enterprise data structure is not yet loaded. Customizing this value can help implementing various loading scenarios: ``` // display a loading state while an EE component is loading const Component = useEnterprise(() =>

CE

, () =>

EE

, { defaultValue: () =>
loading ...
}) // display nothing while an EE component is loading, but don't block the overall rendering const Component = useEnterprise(() =>

CE

, () =>

EE

, { defaultValue: () => null }) // display nothing while an EE component is loading const Component = useEnterprise(() =>

CE

, () =>

EE

) if (!Component) { return; } ``` #### `enabled` Similar to react-query this boolean flag allows disabling the EE import, e.g. when more than one condition needs to be applied. If `enabled` is set to false, the first argument (CE_DATA) will be returned. --- ## NPS ### What does it do The NPS survey is shown to users to get their feedback about Strapi. It is based on a rating scale from 0 to 10, and we also invite users to provide additional comments. ### When do we show the survey? The NPS survey is only displayed to admin users who have selected the "Keep me updated" checkbox during registration. The survey is displayed after 5 minutes of activity. The survey is shown to eligible users based on the following rules: - If a user responds to the survey, the survey will be presented again within 90 days. - If a user does not respond to the survey the first time after their last response, the survey will be presented again after 7 days. - If a user does not respond to the survey for the second or subsequent time after their last response, the survey will be presented again after 90 days. ### Where data is submitted The data is sent to this endpoint: `https://analytics.strapi.io/submit-nps`. ### Hooks #### useNpsSurveySettings This hook uses the `usePersistentState` hook from the helper-plugin (more information available [here](../hooks/use-persistent-state.md)). It is exported so that it can be used during the registration process to determine whether users have selected the "Keep me updated" checkbox. --- ## Admin telemetry (frontend) The admin panel sends usage events **directly from the browser** to the Strapi analytics hub. This is separate from server-side telemetry (`strapi.telemetry.send`) documented in [Server-side telemetry](../../core/telemetry.md). For the public overview of collected data, see [Usage information](https://docs.strapi.io/developer-docs/latest/getting-started/usage-information.html). --- ### Architecture ``` ┌──────────────────────────────────────────────────────────────────┐ │ React components / hooks │ │ trackUsage('didSaveContentType') │ │ trackUsage('didCreateEntry', { documentId, status }) │ └────────────────────────────┬─────────────────────────────────────┘ │ ┌────────────────────────────▼─────────────────────────────────────┐ │ packages/core/admin/admin/src/features/Tracking.tsx │ │ TrackingProvider → context (uuid, telemetryProperties) │ │ useTracking() → trackUsage() │ └────────────┬───────────────────────────────┬───────────────────────┘ │ │ │ GET /admin/telemetry-properties│ POST /api/v2/track ▼ ▼ Strapi backend analytics.strapi.io (group metadata) (override: STRAPI_ANALYTICS_URL) ``` There is **no server proxy** for routine UI events — the admin SPA posts to the analytics endpoint with `axios`. Failures are swallowed; tracking never blocks UI flows. --- ### Core API #### `useTracking()` Primary hook for admin and plugins: ```tsx import { useTracking } from '@strapi/admin/strapi-admin'; const MyComponent = () => { const { trackUsage } = useTracking(); const handleSave = () => { trackUsage('didSaveContentType'); // or with properties: trackUsage('didCreateEntry', { documentId: 'abc', status: 'draft' }); }; }; ``` Implementation: `packages/core/admin/admin/src/features/Tracking.tsx`. Exported from `@strapi/admin` via `packages/core/admin/admin/src/index.ts`. #### `TrackingProvider` Mounted in `packages/core/admin/admin/src/components/Providers.tsx`, wrapping the authenticated app tree. Responsibilities: 1. Reads project `uuid` from `useInitQuery()` (`GET /admin/init`). 2. Fetches `telemetryProperties` via `useTelemetryPropertiesQuery()` when the user is logged in. 3. Fires **`didInitializeAdministration`** once when both `uuid` and telemetry properties are available (anonymous — empty `userId`). --- ### When events are sent (opt-in gates) `trackUsage` only fires when **all** of the following are true: | Gate | Source | | ------------------------------------------- | ------------------------------------------------------------------------ | | Project `uuid` is truthy | `GET /admin/init` — `false` when telemetry is disabled in `package.json` | | `window.strapi.telemetryDisabled === false` | Set at build time from `STRAPI_TELEMETRY_DISABLED` in `render.ts` | If either gate fails, `trackUsage` returns `null` without making a network request. Server-side `strapi.telemetry.isDisabled` also affects whether `/admin/telemetry-properties` returns data (204 when disabled), so group metadata is unavailable when telemetry is off. --- ### Request payload Each `trackUsage` call POSTs to: ``` ${STRAPI_ANALYTICS_URL || 'https://analytics.strapi.io'}/api/v2/track ``` Body shape (same endpoint as server-side telemetry): | Field | Frontend source | | --------------------------- | ---------------------------------------------------------------------------------------------- | | `event` | First argument to `trackUsage` | | `userId` | SHA-256 hash of admin email (`hashAdminUserEmail` in `AuthenticatedLayout`) | | `eventProperties` | Second argument to `trackUsage` (action-specific) | | `userProperties.deviceType` | `'desktop' \| 'tablet' \| 'mobile'` from `useDeviceType()` (user-agent heuristic) | | `groupProperties` | Spread of `telemetryProperties` + `projectId: uuid` + `projectType: window.strapi.projectType` | Header: `X-Strapi-Event: `. #### Group properties from the server `GET /admin/telemetry-properties` (`packages/core/admin/server/src/controllers/admin.ts`): - `useTypescriptOnServer`, `useTypescriptOnAdmin` - `isHostedOnStrapiCloud` - `numberOfAllContentTypes`, `numberOfComponents`, `numberOfDynamicZones` Requires authenticated admin. Returns 204 when `strapi.telemetry.isDisabled`. --- ### Lifecycle / session events These fire automatically — you usually do not add them in feature code: | Event | When | Notes | | -------------------------------------- | ------------------------------------------- | ------------------------------------------------------------ | | `didInitializeAdministration` | First load with uuid + telemetry properties | Anonymous (`userId: ''`); uses raw `fetch`, not `trackUsage` | | `didAccessAuthenticatedAdministration` | Once per authenticated load | `registeredWidgets`, `projectId`; after version is ready | `didAccessAuthenticatedAdministration` is sent from `packages/core/admin/admin/src/layouts/useAuthenticatedAccessTracking.ts` (called by `AuthenticatedLayout`) once the information request has returned `projectId` and the telemetry-properties request has succeeded or failed. If the information request fails, the event is not sent because `projectId` comes from that response. If the telemetry-properties request fails, the event is still sent without telemetry properties. --- ### Event naming conventions Same vocabulary as server-side telemetry, with more **`will*`** intent events (user started an action in UI): | Pattern | Meaning | Examples | | --------- | --------------------------------- | --------------------------------------------------------- | | `will*` | User initiated / about to perform | `willCreateEntry`, `willNavigate`, `willSaveContentType` | | `did*` | Completed action | `didCreateEntry`, `didSaveContentType`, `didPublishEntry` | | `didNot*` | Failed or cancelled | `didNotCreateEntry`, `didNotDeleteEntry` | Event names are defined as a **TypeScript union** in `Tracking.tsx` (`TrackingEvent`). Known events split into: - `EventWithoutProperties` — no second argument - `EventsWithProperties` — typed property shapes per event (content manager entries, media library, tokens, guided tour, etc.) Adding a new event requires updating the union in `Tracking.tsx` so callers get type checking and autocomplete. --- ### Plugin and package patterns Plugins import `useTracking` from `@strapi/admin/strapi-admin` and call `trackUsage` in components or hooks. Major consumers: | Package | Typical events | | ------------------------------ | --------------------------------------------------------- | | `@strapi/admin` | Settings, roles, tokens, navigation, guided tour, widgets | | `@strapi/content-manager` | Entry CRUD, bulk actions, list config, filters, history | | `@strapi/content-type-builder` | Schema editing, AI chat (via wrapper below) | | `@strapi/upload` | Media library actions (via wrapper below) | | `@strapi/content-releases` | `didPublishRelease` | #### Wrapper hooks (enrich properties, don't replace the API) **Content-type builder — `useCTBTracking`** `packages/core/content-type-builder/admin/src/components/CTBSession/useCTBTracking.ts` Wraps `useTracking` and merges `ctbSessionId` into every event's properties. Accepts arbitrary event name strings (relaxed typing for CTB-specific events). **Upload — `useTracking`** `packages/core/upload/admin/src/hooks/useTracking.ts` Wraps `useTracking` and adds `isAiMediaLibraryConfigured` when AI is available (mirrors server upload metrics pattern). When adding plugin telemetry, prefer wrapping the core hook if you need consistent extra properties — do not POST to analytics directly. --- ### Other analytics endpoints (not `trackUsage`) These bypass `Tracking.tsx` but hit the same analytics host: | Endpoint | Component | Purpose | | -------------------- | ----------------------------- | ---------------------------------------------- | | `POST /api/v2/track` | `Tracking.tsx`, `useTracking` | Standard events | | `POST /register` | `UseCasePage.tsx` | First-admin persona registration (email, role) | | `POST /submit-nps` | `NpsSurvey.tsx` | NPS survey responses — see [NPS](./nps.md) | --- ### Volume and rate limiting Unlike server-side telemetry, the **admin frontend has no rate limiter or batching**. Every `trackUsage` call that passes the opt-in gates results in one HTTP request. Conventions that keep volume reasonable in practice: - Pair **`will*`** with **`did*`** / **`didNot*`** only at meaningful completion points (not on every keystroke). - Prefer property **categories** over high-cardinality identifiers where possible (though some events do include `documentId`). - Navigation tracking (`willNavigate`) fires on menu clicks, not on every route render. Do not add per-render or high-frequency tracking without an explicit product need. --- ### `window.strapi` telemetry fields Built by `createBrowserStrapi` (`packages/core/admin/admin/src/utils/browserStrapi.ts`) with defaults, enriched from `/admin/project-type`, then assigned to `window.strapi` once by `render.ts`: ```typescript const browserStrapi: Window['strapi'] = { telemetryDisabled: process.env.STRAPI_TELEMETRY_DISABLED === 'true', projectType: 'Community', // …other fields }; // after fetching /admin/project-type → { isEE, isTrial, planPriceId, features, flags, ai } browserStrapi.projectType = getProjectType({ isEE, planPriceId }); // assigned exactly once, before the app renders window.strapi = browserStrapi; ``` `projectType` is never derived from `isEE` alone — always call `getProjectType`, which returns `Growth` when `planPriceId` contains `growth` and `Enterprise` for other licensed plans. Typed in `packages/core/admin/admin/custom.d.ts`. --- ### Adding a new frontend event — checklist 1. **Choose a name** — follow `will*` / `did*` / `didNot*` conventions. 2. **Update types** — add to `EventWithoutProperties` or create a new interface in `Tracking.tsx` and include it in `EventsWithProperties`. 3. **Call `trackUsage`** from the component or hook at the right lifecycle moment (button click, mutation success/error). 4. **Properties** — use `eventProperties` for action context; rely on automatic `groupProperties` / `userProperties` for project and device metadata. 5. **Plugin?** — import from `@strapi/admin/strapi-admin`; consider a wrapper hook if every event needs extra fields. 6. **Tests** — mock `axios.post` and/or `useTracking` (see `Tracking.test.tsx` and plugin `__mocks__/useTracking.ts`). Do **not** add frontend tracking for features that only exist server-side (e.g. MCP) — use `strapi.telemetry.send` instead. --- ### Testing `packages/core/admin/admin/src/features/tests/Tracking.test.tsx` covers: - Payload shape (`userId`, `eventProperties`, `groupProperties`, `userProperties.deviceType`) - No request when `telemetryDisabled` or missing `uuid` - Graceful failure when `axios.post` rejects Plugin tests often use `jest.mock('@strapi/admin/strapi-admin', () => ({ useTracking: … }))` or local `__mocks__/useTracking.ts`. --- ### Related docs - [Server-side telemetry](../../core/telemetry.md) — `strapi.telemetry`, rate limiting, cron aggregates - [NPS](./nps.md) — survey timing and `/submit-nps` - [Telemetry Service](/api/Telemetry) — public `telemetry.send()` reference (server) --- ## Guided Tour The Guided Tour provides an interactive onboarding experience that guides new users through key features of the admin panel. This documentation explains how the system works and how to create new guided tours. ### Architecture Overview The Guided Tour system is built with a modular architecture consisting of: - **Context Provider** (`Context.tsx`) - Global state management and persistence - **Tour Factory** (`Tours.tsx`) - Tour factory and GuidedTourTooltip - **Steps** (`Steps`) - Step factory, reusable step components, and tour specific step components - **Overview Component** (`Overview.tsx`) - Homepage tour overview and progress tracking ### Core Concepts #### 1. Tours and Steps Tours are collections of steps that guide users through specific workflows. Each step will display its content in a popover. ```typescript const tours = { contentTypeBuilder: createTour('contentTypeBuilder', [ { name: 'Introduction', content: (Step) => ( ), }, // ... more steps ]), }; ``` #### 2. State Management The tour state is managed through a React Context with a reducer pattern: ```typescript type State = { tours: Tour; // Tour progress for each tour enabled: boolean; // Whether tours are globally enabled completedActions: ExtendedCompletedActions; // User-completed actions }; ``` #### 3. Conditional Steps Steps can be conditionally displayed based on user actions: ```typescript { name: 'ConditionalStep', when: (completedActions) => completedActions.includes('didCreateContent'), content: (Step) => (/* step content */) } ``` #### 4. Excluding Steps from Step Count Some steps shouldn't be counted in the step count displayed to the user. Use `excludeFromStepCount` to exclude them: ```typescript { name: 'Welcome', excludeFromStepCount: true, content: (Step) => ( ) } ``` `createTour()` will add a `_meta` property to the tour object that provides information about the number of steps in the tour (total steps) and then the number of steps that will actually be displayed to the user: - `_meta.totalStepCount` - The total number of steps defined for the tour - `_meta.displayedStepCount` - The total number of steps - the number of steps with `excludeFromStepCount: true` ### Usage Guide #### Creating a New Tour 1. **Define the tour structure** in `Tours.tsx`: ```typescript const myNewTour = createTour('myNewTour', [ { name: 'Introduction', content: (Step) => ( ), }, { name: 'MainAction', content: (Step) => ( ), }, { name: 'Finish', content: (Step) => ( ), when: (completedActions) => completedActions.includes('didCompleteMainAction'), }, ]); // Add to the tours object const tours = { // ... existing tours myNewTour, } as const; ``` 2. **Add tour components to Strapi**: Wrap the element that should be the anchor for the step popover. ```tsx import { tours } from '@strapi/admin/strapi-admin'; const MyComponent = () => { return (

My Feature Title

); }; ``` 3. **Mark actions complete** to trigger conditional steps: ```tsx import { useGuidedTour } from './Context'; const MyComponent = () => { const dispatch = useGuidedTour('MyComponent', (s) => s.dispatch); const handleMainAction = () => { // Perform the action performMainAction(); // Track the completion dispatch({ type: 'set_completed_actions', payload: ['didCompleteSomeAction'], }); }; }; ``` #### Step Components API Each step provides three main components: ##### Step.Root The container for the popover with positioning options. Wraps and receives the same props as the [Radix popover](https://www.radix-ui.com/primitives/docs/components/popover). ```tsx ``` ##### Step.Title The step title with i18n support: ```tsx // Or with custom content: ``` ##### Step.Content The step content with i18n support: ```tsx // Or with custom content: ``` ##### Step.Actions Action buttons with built-in functionality: ```tsx // Or with custom actions: ``` ### State Management #### State The guided tour system initializes with a default state structure defined in the Context: ```typescript type State = { tours: Tour; enabled: boolean; completedActions: ExtendedCompletedActions; }; ``` Example: ```typescript const initialState = { tours: { contentManager: { currentStep: 0, isCompleted: false, }, }, enabled: true, completedActions: ['didCreateSchema'], }; ``` #### Actions The tour reducer handles these actions: - `next_step` - Advance to the next step in a tour - `previous_step` - Go back to the previous step in a tour - `go_to_step` - Go to a specific step - `skip_tour` - Mark a tour as completed (skipped) - `skip_all_tours` - Disable all tours globally - `reset_all_tours` - Reset all tours to initial state - `set_completed_actions` - Update the list of completed user actions #### Using the Hook ```tsx import { useGuidedTour } from './Context'; const MyComponent = () => { const state = useGuidedTour('MyComponent', (s) => s.state); const dispatch = useGuidedTour('MyComponent', (s) => s.dispatch); const currentTour = state.tours.myNewTour; const isEnabled = state.enabled; const completedActions = state.completedActions; // Dispatch actions const handleNext = () => { dispatch({ type: 'next_step', payload: 'myNewTour' }); }; }; ``` #### Persistence Tour state is automatically persisted to localStorage using the `usePersistentState` hook with the key `STRAPI_GUIDED_TOUR`. ### Backend Integration Tours integrate with the backend through: `useGetGuidedTourMetaQuery()` - Fetches relative metadata from the server such as `isFirstSuperAdminUser` ### E2E Testing Tours are tested in the e2e test suite `tests/e2e/tests/admin/guided-tour.spec.ts` ### API Reference #### Types ```typescript // Tour configuration type TourStep

= { name: P; content: Content; when?: (completedActions: ExtendedCompletedActions) => boolean; excludeFromStepCount?: boolean; // Exclude from "Step X of Y" counting }; // State management type State = { tours: Tour; enabled: boolean; completedActions: ExtendedCompletedActions; }; type Action = | { type: 'next_step'; payload: ValidTourName } | { type: 'skip_tour'; payload: ValidTourName } | { type: 'previous_step'; payload: ValidTourName } | { type: 'go_to_step'; payload: { tourName: ValidTourName; step: number } } | { type: 'set_completed_actions'; payload: ExtendedCompletedActions } | { type: 'skip_all_tours' } | { type: 'reset_all_tours' }; // Tour metadata type TourMeta = { totalStepCount: number; displayedStepCount: number; }; // Step components type Step = { Root: React.ForwardRefExoticComponent; Title: (props: StepProps) => React.ReactNode; Content: (props: StepProps) => React.ReactNode; Actions: (props: ActionsProps & { to?: string } & FlexProps) => React.ReactNode; }; // Tour object includes both steps and metadata type Tour = Components & { _meta: TourMeta }; ``` --- ## useContentTypes An abstraction around `react-query` to fetch content-types and components. It returns the raw API response for components. `collectionTypes` and `singleTypes` are filtered by `isDisplayed=true`. ### Usage ```jsx import { useContentTypes } from 'path/to/hooks'; const MyComponent = () => { const { isLoading, collectionTypes, singleTypes, components } = useContentTypes(); return (/* ... */); }; ``` --- ## usePersistentState Provides a easily usable hook to store data on the local storage. ### Usage ```js import { usePersistentState } from '@strapi/helper-plugin'; const MyComponent = () => { const [navbarOpened, setNavbarOpened] = usePersistentState('navbar-open', true); return

; }; ``` ### Typescript ```ts function usePersistentState(key: string, defaultValue: T): [T, Dispatch>]; ``` --- ## @strapi/admin ### Purpose `@strapi/admin` is the admin panel of Strapi. It has two parts. The admin part is a React application that runs in the browser and calls the server over HTTP. The server part runs inside the Strapi process. It owns admin users, roles, permissions, API tokens, transfer tokens, sessions and the admin authentication. All core plugins build their screens on it. They import its components, hooks and the `StrapiApp` API from `@strapi/admin/strapi-admin`. `@strapi/core` loads the server part with the internal `admin` provider. Application developers customize the panel with the file `src/admin/app.ts` (or `app.tsx`, `app.js`, `app.jsx`). ### Key concepts #### Entry points The package has one entry point per runtime. `./strapi-admin` is the React code, and `./strapi-admin/ee` holds the Enterprise exports. `./strapi-server` is the Node.js code. The plugin loader does not load it. The `admin` provider in `@strapi/core` requires it and loads its artifacts under the `admin::` namespace. Read [Server and admin split](../../../architecture/index.md#server-and-admin-split). #### `StrapiApp` `StrapiApp` in [`admin/src/StrapiApp.tsx`](https://github.com/strapi/strapi/blob/develop/packages/core/admin/admin/src/StrapiApp.tsx) is the admin application object. `renderAdmin` creates it with the plugins that the admin build collects. It runs each plugin `register` and then `bootstrap`, loads the translations, and renders. During `register`, plugins call methods such as `registerPlugin`, `addMenuLink`, `addSettingsLink`, `addReducers`, `addMiddlewares`, `createHook` and `registerHook`. The app also holds the `router`, the `widgets` and the `customFields` APIs. #### Shared UI and state `admin/src/index.ts` exports the components, hooks and features that plugins reuse: `Form`, `Table`, `Filters`, `useFetchClient`, `useRBAC`, `useAuth`, `useTracking`, `useNotification` and others. The store uses Redux Toolkit. `adminApi` is the shared RTK Query API, and plugins add their endpoints to it with `injectEndpoints`. Plugins add reducers with `addReducers`, and the store injects them at runtime. Read the hook pages [`useContentTypes`](./hooks/use-content-types.md) and [`usePersistentState`](./hooks/use-persistent-state.md). #### RBAC Admin permissions are role based. On the server, the `permission` service holds an action provider and a condition provider, and it builds an engine on top of [`@strapi/permissions`](../permissions/index.md). Plugins register their actions with the action provider, for example in `bootstrap`. On the client, `useRBAC` and `useAuth` check the permissions of the current user. Read [Permissions introduction](./permissions/00-intro.mdx), [How permissions work](./permissions/01-how-they-work.md) and [Using permissions](./permissions/02-frontend/using-permissions.md). #### Server part The server part exposes one `admin` router. It holds the routes for authentication, users, roles, permissions, webhooks, API tokens, admin tokens, transfer tokens and the homepage. Its `register` also adds the route that serves the built panel when `admin.serveAdminPanel` is on. It defines the `admin::` content types: `user`, `role`, `permission`, `api-token`, `api-token-permission`, `transfer-token`, `transfer-token-permission` and `session`. Its `register` sets up passport and adds three auth strategies: an admin strategy, an admin token strategy and a content API token strategy. The `data-transfer` strategy authorizes the transfer routes. Read [Authentication](../../../architecture/08-authentication.md). Code: [`server/src`](https://github.com/strapi/strapi/tree/develop/packages/core/admin/server/src). #### Enterprise code The `ee/admin` and `ee/server` folders hold the Enterprise code, with their own `LICENSE`. The server `index.ts` merges the Enterprise admin into the base admin when `strapi.EE` is true, and it concatenates the routes. On the client, `useEnterprise` picks the Enterprise or the Community component from `window.strapi.isEE`. Audit logs and license limits live in `ee`. Read [Enterprise Edition](../../../architecture/09-enterprise-edition.md), [Audit logs](./ee/audit-logs.md) and [`useEnterprise`](./features/hooks/use-enterprise.md). #### Built-in features The admin part ships features that plugins can use or extend. The [guided tour](./guided-tour.md) teaches new users the panel. The [NPS survey](./features/nps.md) collects feedback. The [admin telemetry](./features/telemetry.md) sends usage events from the browser with `useTracking`. The home page shows widgets, and plugins add their own with `app.widgets.register`. ### Related - [Architecture overview](../../../architecture/index.md): the server and admin split. - [Package map](../../../architecture/01-package-map.mdx): the plugins that depend on the admin. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): when the `admin` provider registers and bootstraps. - [HTTP request path](../../../architecture/06-http-request-path.md): how an admin request passes the auth strategies and policies. - [Glossary](../../../architecture/11-glossary.md): the `admin::` namespace and other terms. - [`@strapi/core`](../core/index.md): loads the server part. - [`@strapi/permissions`](../permissions/index.md): the engine behind admin RBAC. - [`@strapi/content-manager`](../content-manager/index.md): the main plugin that extends the admin panel. - [Server-side telemetry](../core/telemetry.md): the server counterpart of the admin telemetry. --- ## Introduction ## Permissions (RBAC) :::caution This is not about the `user-permissions` plugin which is API orientated, this is about roles based access control commonly known as RBAC. ::: This section is an overview of all the features related to the Permissions core plugin: import DocCardList from '@theme/DocCardList'; import { useCurrentSidebarCategory } from '@docusaurus/theme-common'; --- ### ⚠️ To document ⚠️ - RBAC in the settings page of the admin app - How RBAC works serverside --- ## How Permissions Work ### Overview The RBAC permission system relies under the hood on the [CASL](https://casl.js.org) library. A `permission` is the combination of an `action`, a `subject`, some `properties` and some `conditions`. The logic is that: a user can perform an `action` on a `subject` and some of its `properties` only if its role has the `permission` and if the `conditions` associated to the permission pass. The creation and edition of permissions is done through the [edit page](http://localhost:1337/admin/settings/roles/2) of a role by checking or unchecking checkboxes. Example 1: the action `update` can be applied on the subject `article` and property `title`, this result is the permission to update the title of an article. Example 2: the condition `isCreator` added to the previous permission will ensure that only the creator of the article can update the title. Example 3: the action `access the Marketplace page` has no subject to apply on. The permission only contains the action `admin::marketplace.read`. The permissions are attached to a role. Each role can have different permissions independently. There is no inheritance system. To check if an action can be performed by a user, frontend and backend simply retrieve the list of the permissions associated with the user's roles and check if it contains the permission associated with the action about to be performed. The backend also runs the associated conditions. :::tip If a user has several roles, the user will be allowed to perform an action if at least one of its roles is allowed to perform it. ::: ### Actions #### Action definition An action contains the following information: | key | description | type | required | Default value | example | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ------------- | -------------------------- | | **uid** | An id that has to be unique within the plugin. | string | required | - | `'audit-logs.read'` | | **pluginName** | Name of the plugin registering the action. | string | - | `'api'` | `'admin'` | | **section** | Name of the section among `contentTypes`, `plugins`, `settings` and `internal`. It will define in which permission tab the action will appear. `internal` is not displayed in any tab but used for internal purposes. | string | required | - | `'settings'` | | **category** | Name of the category. It will define in which category the action will appear. _Only for the plugins and settings section_. | string | - | - | `'audit logs'` | | **subCategory** | Name of the subcategory. It will define in which subcategory or the category the action will appear. _Only for the plugins and settings section_. | string | - | - | `'options'` | | **displayName** | Human name of the action. | string | required | - | `'Read'` | | **subjects** | List of subjects the action can be applied to. _Only for the contentTypes section_. | array | - | - | `['api::article.article']` | | **options** | Option object | object | - | `{}` | `{}` | | **options.applyToProperties** | List of properties the action can be applied to. _Only for the contentTypes section_. | - | - | `[]` | `['fields', 'locale']` | ![Screenshot of the RBAC edit page showing the where are located the section, category, subcategory and display name](/img/permissions/rbac-edit-page.png) _above: Screenshot of the RBAC edit page showing the where are located the section, category, subcategory and display name_ :::note `uid` and `pluginName` are used to create a global unique id. Examples: `admin::audit-logs.read`, `plugin::content-manager.explorer.create` ::: #### Register an action An action can be registered in the backend during the `bootstrap` phase of the app. Example: ```ts // bootstrap.ts module.exports = async () => { const actions = [ { uid: 'provider-login.read', displayName: 'Read', pluginName: 'admin', section: 'settings', category: 'single sign on', subCategory: 'options', }, ]; await strapi.service('admin::permission').actionProvider.registerMany(actions); }; ``` #### Parametrize actions Actions can be parametrized with the `actionParameters` property. This property is an object that can contain any key/value pair. An example of the feature: - The action `review-workflows.stage.transition` can be parametrized with the `from` and `to` parameters, which are the ids of the stages a user can transition from and to. At the moment only Review Workflows use this feature internally. ### Conditions #### Condition definition A condition contains the following information: | key | description | type | required | default value | example | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ------------- | -------------- | | **displayName** | The condition name as shown in the admin panel | string | required | - | `'Is creator'` | | **name** | A name that has to be unique within the plugin | string | required | - | `'is-creator'` | | **plugin** | Name of the plugin that register the condition | string | - | `'api'` | `'admin'` | | **category** | Name of the category. Conditions can be grouped into categories available in the admin panel | string | - | `'Default'` | `'admin'` | | **handler** | A function used to verify the condition (see [docs.strapi.io](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/rbac.html#using-the-condition-handler)) | string | required | - | `'admin'` | :::note Currently the user can choose to apply a condition on any action. There is no way to define a list of actions on which the condition can be applied to. This is why it is possible to set the condition `isCreator` on a the action `plugin::content-manager.explorer.create` even if it doesn't make sense. ::: :::note `name` and `plugin` are used to create a global unique id. Examples: `admin::is-creator` ::: #### Registering a condition A condition can be registered in the backend during the `bootstrap` phase of the app. Example: ```ts // bootstrap.ts module.exports = async () => { const conditions = [ { displayName: 'Is creator', name: 'is-creator', plugin: 'admin', handler: (user) => ({ 'createdBy.id': user.id }), }, ]; await strapi.service('admin::permission').conditionProvider.registerMany(conditions); }; ``` More information on how the handler works [here](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/rbac.html#using-the-condition-handler). ### Permissions #### Permission definition The permissions are modified in the admin panel (on the [edit page](http://localhost:1337/admin/settings/roles/2) of a role) and stored in the database with the following information: | key | description | type | required | example | | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ------------------------------------------------------------- | | **action** | Id of the action that will be permitted. | string | required | `'plugin::content-manager.explorer.update'` | | **actionParameters** | Object to parametrize actions. | string | - | `{}` | | **subject** | Id of the subject on which the action will be permitted. | string | - | `'api::article.article'` | | **properties** | List of the properties of the subject on which the action will be permitted | object | - | `{ fields: ['title', 'description'], locales: ['en', 'fr'] }` | | **conditions** | List of the conditions that will be ran against an entry to determine whether the action on this entry is permitted or not | array | - | `['admin::is-creator']` | A permission contains all needed information for the backend and the frontend to prevent users to perform non-permitted action. --- ## Fetching Permissions This document lays out the code-flow of fetching the permissions in the CMS and how the data is then injected in to the app to be used by components. ### Where do we fetch them? At the very root of the entire admin panel we handle 4 routes: - Signing into the app - Not Found - Collecting information from a first time user - The authenticated application (where you use the CMS) Permissions, along with other vital application information is fetched in the `AuthenticatedApp` component, located in the `packages/core/admin/admin/src/AuthenticatedApp` folder. The cache key (used with `react-query`'s `useQueries` hook) for the particular call is `admin-users-permission`. ### What do we fetch? Because permissions are based on users in this case the individual logged in, we fetch their permissions from the route: `/admin/users/me/permissions`. An example can be seen below: ```json { "data": [ { "id": 426, "action": "admin::api-tokens.access", "subject": null, "properties": {}, "conditions": [] }, { "id": 427, "action": "admin::api-tokens.regenerate", "subject": null, "properties": {}, "conditions": [] }, { "id": 763, "action": "plugin::content-manager.explorer.create", "subject": "api::homepage.homepage", "properties": { "fields": ["title", "slug", "single", "multiple"], "locales": ["en", "fr"] }, "conditions": [] }, { "id": 765, "action": "plugin::content-manager.explorer.read", "subject": "api::homepage.homepage", "properties": { "fields": ["title", "slug", "single", "multiple"], "locales": ["en", "fr"] }, "conditions": [] }, { "id": 767, "action": "plugin::content-manager.explorer.update", "subject": "api::homepage.homepage", "properties": { "fields": ["title", "slug", "single", "multiple"], "locales": ["en", "fr"] }, "conditions": [] } ] } ``` An explanation on how these work can be found [here](../01-how-they-work.md). But as a quick overview we can see that permissions typically associate to actions and in the case of the content manager they have subjects to be more granular and then fields & locales to be even more granular. These actions are defined by domains of the application registered via the application as they also have effect when calling APIs. ### What do we do with the data? The data is then passed to the `RBACProvider` component which passes it to a public facing [React Context](https://beta.reactjs.org/learn/passing-data-deeply-with-context) as well as injecting the information into the global `redux` store – see diagram below: Inside the `RBACProviderContext` we also pass the ability to refetch the permissions. These two inconjunction with one another are accessed via the `useRBACProvider` hook. You can see the API for this hook [here](./using-permissions.md#typescript) --- ## Using Permissions Currently, there are a few different ways to use permissions in the application. This document will explain the different ways, how to use them and suggestions on when is best to use them. It's advised that you've read [how they work](../01-how-they-work.md) and [fetching permissions](./fetching-permissions.md) before reading this document so you have the full context. ### Components There are two different wrapper components available to use. They are very similar but have different use cases. They are both available in the `@strapi/strapi/admin`. #### CheckPagePermissions Used to apply RBAC to a view/page of the application. If the user does not have the permissions to access this page they will be redirect to the homepage. ##### Usage ```tsx import { Page } from '@strapi/strapi/admin'; const permissions = [{ action: 'plugins::my-plugin.access', subject: null }]; const MyPage = () => { return (

aha you found me

); }; ``` ##### Typescript ```ts interface Permission { id: number; action: string; subject: string | null; // This can be custom defined to the needs of the plugin/application properties: Record; conditions: Array; } type CheckPagePermissionsProps = { permissions: Permission[]; children: React.ReactNode; }; type CheckPagePermissions = (props: CheckPagePermissionsProps) => JSX.Element; ``` ### useRBAC Is a wrapper around the [`hasPermissions`](#haspermissions) function which calls the `/admin/permissions/check` endpoint with a provided list of permissions to ascertain if a user can do a specific action. This hook is typically used with plugin permissions alongside the global permissions object generated from the [`useRBACProvider`](#userbacprovider) hook which can either be passed or will be accessed internally. #### `hasPermissions` Function that checks if a user has the required permissions. #### `useRBACProvider` Hook that provides the global permissions object. Because it's fetching, we also provide a `isLoading` state which can be used to show a loading state while the permissions are being fetched. If the hook is unmouted before the fetch request completes, the request will be cancelled via an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). #### Usage In the below example, we're checking if a user can "create" a post from the content-manager list view. If they can't, the button is `disabled`. ```tsx import { useRBAC } from '@strapi/strapi/admin'; const MyComponent = () => { const { isLoading, allowedActions } = useRBAC({ create: [ { action: 'plugin::content-manager.explorer.create', subject: 'api::post.post', }, ], }); if (isLoading) { return

Loading...

; } return ; }; ``` #### Typescript ```ts interface Permission { id: number; action: string; subject: string | null; // This can be custom defined to the needs of the plugin/application properties: Record; conditions: Array; } interface PluginPermissions { create: Permission[]; read: Permission[]; update: Permission[]; delete: Permission[]; publish: Permission[]; } type UseRBAC = ( pluginPermissions: PluginPermissions, permissions?: Permission[] ) => { isLoading: boolean; setIsLoading: () => void; allowedActions: { canCreate: boolean; canRead: boolean; canUpdate: boolean; canDelete: boolean; canPublish: boolean; }; }; ``` --- ## Documents At the very core of the CM is the concept of a document. Although the logic for creating a document lives primarily within the `@strapi/core` package, we still need to understand what they are to interact with them. Introduced for the _new_ draft & publish feature in V5 (Q1 2024), documents are essentially a matrix of entries, this matrix grows in complexity based on the amount of dimensions your document has. For example, having Draft & Publish and Internationalisation (with 2 locales) enabled would give you 4 entries within the document – a draft for each locale and a published entry for each locale. :::note Draft & Publish is optional on content-types ::: ### Getting Documents We get documents via the [`useDocument`](./hooks/use-document.md) hook, which requires us to understand which "model" we want – the model is basically which content-type e.g. `api::article.article`, the "kind" of content-type e.g. `single-type` or `collection-type` and the specific `id` of said document. Using these three parameters we can fetch any document, without passing specific parameters e.g. `locale` we get a default version defined by the user's application. However, all dimensions should be queryable to narrow down the point on the document matrix to a specific entry e.g. `published & 'en-GB'`. ### Interacting with Documents There are universal actions that can be performed on documents, these are: - `create` – creates a new document - `update` – updates an existing document - `delete` – deletes an existing document Documents that are `collection-types` additionally can be cloned. If a document has draft & published enabled then we can additionally publish & unpublish the document. All of this functionality is exposed via the [`useDocumentActions`](./hooks/use-document-actions.md) hook. Additional actions can be added by plugins via APIs. --- ## Layouts Layouts are fundamental to how the CM renders its list & edit views. The edit view's layout can be manipulated via plugins using the `'Admin/CM/pages/EditView/mutate-edit-view-layout'` hook. Meanwhile the list view can currently only have table columns injected. ### What is a layout? A layout is simply a data-structure we iterate over to understand how to render the views. For both List & Edit view their structures have explicitly been designed to be similar: ```ts interface ListLayout { layout: ListFieldLayout[]; components?: never; metadatas: object; settings: object; } interface EditLayout { layout: Array>; components: Record>; metadatas: object; settings: object; } ``` The above types have been simplified for the purpose of explanation, but you can see from the `components` property of the `EditLayout` that it's essentially the same as its parent, just a nested and organised per component. This consistency allows us to easily iterate over the layouts and render the views using the same components for the general layout & component layouts. ### How are they made? The layout is a combination of a content-types's schema & its configuration file, while their data-structures are similar they differ at the very root interface. #### EditView The very base level interface for the EditView layout is an `EditFieldLayout` which is primarily derived from the `InputProps` of our Form Components: ```ts interface InputProps { disabled?: boolean; hint?: string; label: string; name: string; placeholder?: string; required?: boolean; type: Exclude< Attribute.Kind, 'media' | 'blocks' | 'richtext' | 'uid' | 'dynamiczone' | 'component' | 'relation' >; } interface EditFieldSharedProps extends Omit { mainField?: string; size: number; unique?: boolean; visible?: boolean; } /** * Map over all the types in Attribute Types and use that to create a union of new types where the attribute type * is under the property attribute and the type is under the property type. */ type EditFieldLayout = { [K in Attribute.Kind]: EditFieldSharedProps & { attribute: Extract; type: K; }; }[Attribute.Kind]; ``` The excluded `type` values are unique to the content-manager and as such aren't expected to be rendered as universal form inputs. This data-structure is passed to the `InputRenderer` component to render any form input (previously known as `GenericInputs` from the helper-plugin). Notice how these inputs don't receive their `value`, `onChange` or `error` prop. These are extracted from the `Form` using `useField(name)`: ```ts export const StringInput = forwardRef( ({ disabled, label, hint, name, placeholder, required }, ref) => { const field = useField(name); return ( ); } ); ``` To handle the CM specific inputs, the EditView domain has its own `InputRenderer` component which follows the same principle but this will additionally handle attributes like custom-fields etc. The EditView them "simply" renders over its layout, passing props to the `InputRenderer`, if there's a dynamic-zone or component we recursively render the layout by utilising the `components` property of the `EditLayout` data-structure, as previously said, because this follows the same structure as its parent, we can iterate over it the exact same way rendering _any fields_ required, incl. deeply nested components and dynamic-zones. #### ListView Because the list view is essentially a giant table, its data-structure is considerably simpler than the edit view: ```ts interface ListFieldLayout { /** * The attribute data from the content-type's schema for the field */ attribute: Attribute.Any | { type: 'custom' }; // custom attributes are expected to use `cellFormatter`. /** * Typically used by plugins to render a custom cell */ cellFormatter?: ( data: { [key: string]: unknown }, header: Omit ) => React.ReactNode; label: string | MessageDescriptor; /** * the name of the attribute we use to display the actual name e.g. relations * are just ids, so we use the mainField to display something meaninginful by * looking at the target's schema */ mainField?: string; name: string; searchable?: boolean; sortable?: boolean; } ``` The `cellFormatter` property is paramount to plugin developers being able to inject their own columns, because we, most likely, will not have the components necessary to render the data the plugin provides. This is why they receive the data for the entire row & the header that they injected via the `'Admin/CM/pages/ListView/inject-column-in-table'` hook. --- ## RBAC :::note This is not a detailed breakdown of permissions within strapi, if you're looking for this you should look at [Permissions Intro](../admin/permissions/00-intro.mdx). ::: Each document's permission's object will contain `properties.fields` which is an array of strings, we can use these to understand which properties can be created / updated etc. Via the subject of the permission. ```json // An example permission object { "id": 666, "action": "plugin::content-manager.explorer.create", "actionParameters": {}, "subject": "api::article.article", "properties": { "fields": ["short_text", "blocks", "single_compo.name", "single_compo.test", "dynamiczone"] }, "conditions": [] } ``` The above permissions relate to what fields the user can create on the article content-type. The list of fields are their names in the schema, not their labels (which can be overridden in the EditViewSettings), components are dot separated paths where the component name will be the first part of said path, repeatable components **will not** have indexes in the path and finally, in dynamic zones all fields are always allowed. ### DocumentRBAC Component The `DocumentRBAC` component wraps the ListView & EditView pages providing - if a user can `create/read/update/delete/publish` a document at all - the list of fields for each action - a utility function to check if a user `canAction` ```ts interface DocumentRBACContextValue { canCreate?: boolean; canCreateFields: string[]; canDelete?: boolean; canPublish?: boolean; canRead?: boolean; canReadFields: string[]; canUpdate?: boolean; canUpdateFields: string[]; canUserAction: ( fieldName: string, fieldsUserCanAction: string[], fieldType: Attribute.Kind ) => boolean; isLoading: boolean; } ``` :::note Because the `useRBAC` hook fetches data from the API to check against `conditions` of a permission, we optionally have the `isLoading` returned in case a component needs to await this. ::: Using all this information, we can disabled & hide fields in the application based on the user's permissions. --- ## Content Releases ### Summary Content releases is an enterprise feature so you need a valid license. Additionally, your user role must be granted specific Permissions, such as `plugin::content-releases.read` and `plugin::content-releases.create-action`. You can modify these permissions in the Settings section. The capability to add entries to a release within the content manager is available through the content-manager edit view. #### Edit view If the feature is enabled with the correct license, and the user has the necessary permissions enabled, a 'Releases' section will appear on the right side of the Edit View. The 'Releases' section has an 'Add to release' button. Clicking this button opens a modal allowing you to assign the entry to a specific release and specify the desired action (Publish/Unpublish). In the event that no releases are present, users need to create a release first, which can be done on the [Content Releases](../content-releases/index.md) page. If the user's permissions also includes the `plugin::content-releases.delete-action`, the user can access additional options by clicking the 'More' button (represented by three dots) in the releases sidebar. This will reveal the 'Remove from release' button, providing the ability to detach the entry from the selected release. --- ## Relations ### Summary Relations are a term used to describe how two or more entities are connected. Previously in the sidebar of an entity, in Nov2020 we released a refactor that moved these fields into the main editing flow for a better editor experience and to improve performance of the CMS application when many relations were used. _above: An example of the relations input in the CMS edit view_ ### Data management in frontend _above: A high-level diagram of how relations state management works_ #### Preparing relation fields in the store When you first open an existing entity, we call the admin API and put the data into the store to pre-populate fields with existing values. However, its important to know when you have fields with `type === 'relation'` in your schema that the data you receive will not be an array, but rather an object with the count of how many relations in that field exist. For example, a section of the response may look like this: ```json { "my_relations": { "count": 6 } } ``` So without intervention, your inputs would try to append new relations to the `my_relations` object, which would not work. Instead of this, before calling the redux action `INIT_FORM` we recursively find the paths fields based on the following conditions: - The field is a relation - The field is a component - The field is a repeatable component - The field is a dynamic zone These paths _do not_ take into account index values. So if you have a repeatable component field where the schema looks like: ```json { "repeatable_single_component_relation": { "type": "component", "repeatable": true, "component": "basic.relation" } } ``` and the components looks like: ```json { "basic.relation": { "attributes": { "id": { "type": "integer" }, "categories": { "type": "relation", "relation": "oneToMany", "target": "api::category.category", "targetModel": "api::category.category", "relationType": "oneToMany" }, "my_name": { "type": "string" } } } } ``` Then the path to the relation field would be `repeatable_single_component_relation.categories`. Even though when relations are added the path to the field in the redux store would be `repeatable_single_component_relation.0.categories`. Inside the reducer we reduce the array of `relationalFieldPaths` to an object with the `initialValues` clone as the base. If there is `modifiedData` in the browser i.e. you've made changes to the entity and saved those changes, we just replace the first level of the field with the `modifiedData` so the data structure is preserved and we're not losing the relations we had already loaded in the component. If the first part of the path is highlighted as the `relationalField` then we simply replace that initial object with an empty array. However, if the first part of the path is either a repeatable component, a dynamic zone or a regular component then we recursively find the relation fields and replace the object with an array. This is handled by the `findLeafByPathAndReplace` utility function. This function in short, takes an end path (in this case the relational field) and a primitive to replace when it finds the endpath (an empty array in this case). It then recursively reduces the paths to the relational field mapping through arrays if necessary (in the instance of repeatable components for example) replacing the endpath with the primitive. When this is done, we have successfully prepared our initial data for usage with relations. #### Handling updates to relation fields Because we've prepared the fields prior to the component loading, adding & removing relations, it's relatively easy to do so. When a relation is added, we simply push the new relation to the array of relations. When a relation is removed, we simply filter out the relation from the array of relations. This is handled inside the reducer actions `CONNECT_RELATION` & `DISCONNECT_RELATION` respectively. :::note Connecting relations adds the item to the end of the list, whilst loading more relations prepends to the beginning of the list. This is the expected behaviour, to keep the order of the list in the UI in sync with the API response. ::: The `RelationInput` component takes the field in `modifiedData` as its source of truth. You could therefore consider this to be the `browserState` and `initialData` to be the `serverState`. When relations are loaded they're added to both the `initialData` and `modifiedData` objects, but when you connect/disconnect only the `modifiedData` is updated. This is useful when we're preparing data for the API. #### Cleaning data to be posted to the API The API to update the entity expects relations to be categorised into two groups, a `connect` array and `disconnect` array. You could do this as the user interacts with the input but we found this to be confusing and then involved us managing three different arrays which makes the code more complex. Instead, because the browser doesn't really care about what's new and removed and we have a copy of the slice of data we're mutating from the server we can run a small diff algorithm to determine which relations have been connected and which have been disconnected. Returning an object like so: ```json { "my_relations": { "connect": [{ "id": 1 }, { "id": 2 }], "disconnect": [] } } ``` ### Frontend component architecture The input field for relation fields consist of two components: #### `RelationInputDataManager` This container component handles data fetching and data normalization for the `RelationInput` component. This has been extracted from the `RelationInput` so that Strapi is able to move the underlying component into the design-system if the community would need it (most other input components can be consumed from there). #### `RelationInput` This component is the presentational counterpart to the `RelationInputDataManager` component. It renders an input field based on the data passed from the data manager. Under the hood it is using `react-window` to render a list of relations in a virtualized view. Some fields need to render thousands of relations, which would otherwise have a negative impact on the overall performance of the content-manager. ### useRelation() hook This hook takes care of data-fetching and normalizes results relations as well as search-results. ```ts const { relations: RelationResults, search: RelationResults, searchFor } = useRelation(reactQueryCacheKey: Array, options: Options); ``` #### `Options` `option`s is a mandatory configuration and should implement the following shape: ```ts type Options = { name: string; // name of the relation field relation: RelationConfiguration; search: SearchConfiguration; } type RelationConfiguration = { endpoint: string; // URL from where existing relations should be fetched enabled: boolean; // defines whether relations should be fetched once the hook is called pageParams: object; // additional query params which will be appended to `endpoint` onLoad: (results: RelationResult[]) => void; // callback that will be fired after relations have been fetched (paginated) normalizeArguments = { mainFieldName: string; // name of the target model main field, determining which field to display (fallback: id) shouldAddLink: boolean; // if the user is allowed to read the target model, the returned relations should include a link to the target targetModel: object; // target content-type model }; pageGoal: number; // the current page-count of the already loaded relations used to keep the redux store and query cache in sync. } type SearchConfiguration = { endpoint: string; // URL from where new relations should be fetched pageParams: object; // additional query params which will be appended to `endpoint` } ``` #### Return values `relations` and `search` both return a consistent relation format: ```ts type RelationResults = RelationResult[]; type RelationResult = { id: number; href?: string; // based on `shouldAddLink` and the `targetModel` publicationState: 'draft' | 'published'; mainField: string; // will fallback to "id" if not set }; ``` ##### `relations` `relations` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated relational data as well as methods to check if there are more pages or fetch more paginated results. Relations for a given field are fetched as soon as the hook is called. ##### `search` `search` refers to a [inifinite-query return type](https://tanstack.com/query/v4/docs/react/guides/infinite-queries) from react-query. It exposes paginated search results for a relational field. Search results are only fetched after `searchFor()` has been called. ##### `searchFor(string)` `searchFor` is a method which can be used to search for entities which haven't been connected with the source entity yet. The method accepts a search-term: `searchFor("term")`. --- ## Blocks editor The blocks editor is a modern text editor based on the [Slate.js library](https://docs.slatejs.org/). It is designed as the successor to the classic markdown editor, and can only be used within the content-manager. ### Data format #### Why JSON While the markdown editor stores content as a string, Blocks stores it as a JSON object. Since Strapi is headless, we want a format that makes it easy to map content across different platforms and offer a good experience for non-web use cases too. In the case of React frontends, JSON also means we don't need to rely on `dangerouslySetInnerHTML` to render the formatted content. #### Slate-based schema The blocks editor schema is based on Slate.js, which allows us to design our own custom schema structure. We chose Slate in part because its JSON format remains human-readable. This was a key element to ensure the data could be rendered in any frontend where official or third-party libraries may not be available. Our Blocks schema is made of the following elements: - Block nodes: they're at the root of the document JSON: Paragraphs, images, headings, lists, code blocks... - Inline nodes: they're children of block nodes, and can have children. Inline nodes include text nodes, and links. - Text leaves: they're children of inline nodes, and contain the actual text content. One specificity of our design compared to most Slate implementations is that they must have a `"type": "text"` entry in addition to the standard `text` property. - Modifiers: they're variations of text nodes that can be applied to text content. They include bold, italic, underline, and strikethrough. They can be combined. With these elements combined, here's what the content for a sample Blocks attribute looks like: ```json [ { "type": "heading", "children": [ { "type": "text", "text": "I am a heading" } ], "level": 1 }, { "type": "paragraph", "children": [ { "type": "text", "text": "Nice content here. Isn't it? Some of it can be " }, { "type": "text", "text": "bold", "bold": true }, { "type": "text", "text": ", " }, { "type": "text", "text": "italic", "italic": true }, { "type": "text", "text": ", or " }, { "type": "text", "text": "both", "bold": true, "italic": true }, { "type": "text", "text": "." } ] }, { "type": "list", "format": "unordered", "children": [ { "type": "list-item", "children": [ { "type": "text", "text": "first list item" } ] }, { "type": "list-item", "children": [ { "type": "text", "text": "sub list" } ] }, { "type": "list", "format": "unordered", "indentLevel": 1, "children": [ { "type": "list-item", "children": [ { "type": "text", "text": "sub 1" } ] }, { "type": "list-item", "children": [ { "type": "text", "text": "sub 2" } ] } ] } ] } ] ``` ### Components structure As shown in the diagram, the Blocks input is split into several components: - `BlocksInput` is the input ready to be used in the content manager edit view. Besides the editor, it is responsible for rendering the label, label actions (like the i18n icon), as well as error and hint messages. - `BlocksEditor` is the root of the Slate editor. It provides the context for all the stateful parts of the editor. - `BlocksToolbar` is the toolbar that appears above the editor. It provides buttons for formatting the text, as well as a button to insert a new block. - `BlocksContent` is the WYSIWYG editor that allows users to write and view their formatted content. ### Blocks and modifiers management A key goal of the blocks implementation is that it should be driven by a declarative list of blocks and modifiers. Each block and each modifier should manage its own rendering, logic and behavior. The editor itself (whether it's the toolbar or the content) should not contain any logic targeting specific blocks or modifiers. It should remain agnostic. This has several upsides. The logic for each block or modifier is self-contained within its own file, making it easier to grasp and edit. It keeps the editor's code lean and avoids spaghetti implementations. It lets us manage blocks from several entry points: the toolbar's dropdown, a Notion-style `/` to open a blocks popover... And importantly, it opens the door for a [custom](https://github.com/strapi/strapi/pull/24427) [blocks](https://feedback.strapi.io/customization/p/add-ability-to-extend-strapis-rich-text-editor-with-custom-slate-elements) and custom modifiers API, letting users extend the editor with their own building blocks. #### Block registration A block is registered in the editor as an object of always the same shape. For a single "type", there can be several variations, all of which will have their own object. For example, Heading 1 and Heading 2 both have `heading` type, but are displayed separately in the editor, and therefore are registered separately. | Property | Description | | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **renderElement**`(props: RenderElementProps) => JSX.Element` | Displays the block inside `BlocksContent`. Make sure to spread `attributes` on the root (that's how Slate makes it editable), and to render `children` so that inline nodes and text leaves can be rendered. | | **icon**`React.ComponentType` | A visual cue to identify the block. Use icons from the design system. | | **label**`MessageDescriptor` | A translation object to display what the block is called. | | **isInBlocksSelector**`boolean` | Whether the block should appear in the blocks selector dropdown. In almost all cases, this is `true`. The main exception is list items, as they are hidden under their list parent. | | **dragHandleTopMargin**`CSSProperties['marginTop']` | Adjusts the vertical alignment of the grip icon used to reorder nodes. | | **matchNode**`(node: BlocksNode) => boolean` | Returns a boolean that indicates whether a node is of the given block type. | | **handleConvert**`(editor: Editor) => void` | Customizes the logic run when transforming the currently selected node into the given node type. It generally involves setting the `type` property and clearing unwanted properties. The `baseHandleConvert` util manages just that. | | **handleEnterKey**`(editor: Editor) => void` | Customizes the logic ran when the user presses enter. By default, it creates a paragraph under the current node and selects it. For blocks that may have multi-line content, such as code blocks, a common pattern is to replicate this behavior when the user presses enter twice. | | **handleBackspaceKey**`(editor: Editor, event: KeyboardEvent) => void` | Customizes the logic ran when the user presses backspace. Useful for blocks that need special behavior when deleting content at the start of a block. | | **handleTab**`(editor: Editor) => void` | Customizes the logic ran when the user presses tab. Useful for blocks like lists that support indentation. | | **snippets**`string[]` | Strings that can trigger a transformation into the given block after pressing space. | ```tsx const headingBlocks = { 'heading-one': { renderElement: (props) =>

{props.children}

, icon: HeadingOne, label: { id: 'components.Blocks.blocks.heading1', defaultMessage: 'Heading 1', }, matchNode: (node) => node.type === 'heading' && node.level === 1, isInBlocksSelector: true, handleConvert: (editor) => baseHandleConvert(editor, { type: 'heading', level: 1 }), snippets: ['#'], dragHandleTopMargin: '14px', }, }; ``` #### Modifier registration A modifier is registered in the editor as an object with a consistent shape. | Property | Description | | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **renderLeaf**`(children: JSX.Element \| string) => JSX.Element` | Displays the modified text inside `BlocksContent`. This wraps the children in the appropriate styled component (e.g., `` for bold). | | **icon**`React.ComponentType` | A visual cue to identify the modifier. Use icons from the design system. | | **label**`MessageDescriptor` | A translation object to display what the modifier is called. | | **isValidEventKey**`(event: KeyboardEvent) => boolean` | Returns a boolean that indicates whether a keyboard event should trigger this modifier. Used for keyboard shortcuts (e.g., `Ctrl+B` for bold). | | **checkIsActive**`(editor: Editor) => boolean` | Returns a boolean that indicates whether the modifier is currently active on the selection. | | **handleToggle**`(editor: Editor) => void` | Customizes the logic ran when toggling the modifier on or off. | ```tsx const modifiers = { bold: { renderLeaf: (children) => {children}, icon: Bold, label: { id: 'components.Blocks.modifiers.bold', defaultMessage: 'Bold', }, isValidEventKey: (event) => event.key === 'b', checkIsActive: (editor) => Boolean(Editor.marks(editor)?.bold), handleToggle: (editor) => { if (Editor.marks(editor)?.bold) { Editor.removeMark(editor, 'bold'); } else { Editor.addMark(editor, 'bold', true); } }, }, }; ``` --- ## Live preview The live preview feature lets users see their content rendered on their frontend while editing. It includes visual editing that identifies and highlights editable fields. ### Why not an SDK Visual editing requires running some of our code on the user's frontend to detect fields and draw highlights. The obvious approach would be an SDK package users install in their project. We intentionally avoided this. An SDK would require ongoing maintenance and create version mismatch risks between the SDK and Strapi. It would also tie us to specific frameworks or require multiple framework-specific packages. Instead, the preview script is defined inside Strapi and sent to the frontend via `postMessage`. The frontend just needs a small snippet to receive and execute it. This keeps the script always in sync with the CMS version, works with any framework, and requires no package installation. ### How the script works #### Self-contained constraint The preview script (`packages/core/content-manager/server/src/preview/controllers/previewScript.js`) is served verbatim from a server endpoint (`GET /content-manager/preview/script`). The admin fetches it, wraps it with its runtime config, and posts the result to the iframe: ```ts const previewScriptSource = await fetch( `${window.strapi.backendURL}/content-manager/preview/script` ).then((res) => res.text()); const config = { colors: previewHighlightColors, events: INTERNAL_EVENTS, parentOrigin: window.location.origin, }; const script = `(${previewScriptSource})(${JSON.stringify(config)})`; ``` Because of this, it **cannot import dependencies or reference external variables**. All logic must be self-contained. The only external code (`@vercel/stega` for decoding) is loaded dynamically from a CDN at runtime. It lives in a standalone `.js` file served as-is rather than being bundled with the admin: a bundler wraps the module in helper code that breaks once the function is stringified and injected into the iframe. The file is still type-checked via `@ts-check` and JSDoc. This is why the file has an unusual structure with many functions defined inline. #### Field identification with stega We use [stega encoding](https://github.com/vercel/stega) to identify which Strapi field each piece of text comes from. Stega embeds invisible metadata into text content using Unicode zero-width characters that are imperceptible to users but can be decoded programmatically. 1. The Document Service encodes field metadata into text values (invisible to users) 2. The frontend renders the content normally 3. The preview script decodes the metadata and attaches `data-strapi-source` attributes to DOM elements 4. Highlights are drawn over elements with source attributes The metadata uses URL search params format, because it makes it easy to encode and decode multiple pieces of information into a single string: `path=title&type=string&documentId=abc123&locale=en&model=api::page.page` #### Stega limitations Stega can only encode strings. This means: - **Numbers and booleans aren't encoded** — we can't modify their type in the response. - **Fields inside components and dynamic zones work** — we encode individual string fields within them, not the parent object. The path includes indices (e.g., `components.2.title`) to identify the exact field. - **Media fields work partially** — the string properties inside media objects (like `url`, `name`, `alternativeText`) get encoded when traversed. Blocks fields hold a JSON AST rather than a plain string, so they can't use the standard stega encoding. They use a dedicated path described in [Blocks visual editing](#blocks-visual-editing) below. #### Communication protocol The admin panel and preview iframe communicate via `postMessage`. ```mermaid sequenceDiagram participant Admin participant Iframe as Preview Iframe participant Host as Host App (window) Note over Admin,Iframe: Initialization (public events) Iframe->>Admin: previewReady Admin->>Iframe: strapiScript Note over Admin,Iframe: User edits in admin panel (internal events) Admin->>Iframe: strapiFieldFocus Admin->>Iframe: strapiFieldBlur Note over Admin,Host: Blocks field change (public event — admin→iframe postMessage, then script→host CustomEvent) Admin->>Iframe: strapiFieldChange (postMessage) Iframe->>Host: strapiFieldChange (CustomEvent on window) Note over Admin,Iframe: User clicks in preview (internal events) Iframe->>Admin: strapiFieldSingleClickHint Iframe->>Admin: strapiFieldFocusIntent (double-click, includes blockIndex for blocks fields) Note over Admin,Iframe: After editing closes (internal event) Admin->>Iframe: strapiRescanHighlights Note over Admin,Iframe: Content saved (public event) Admin->>Iframe: strapiUpdate ``` Public events (`previewReady`, `strapiScript`, `strapiUpdate`, `strapiFieldChange`) are documented to users—changing them is a breaking change. Internal events (for field focus/blur synchronization) are defined in `packages/core/content-manager/admin/src/preview/utils/constants.ts` and can be changed freely since we control both ends. #### Frontend configuration Users can configure the preview behavior from their frontend via `window` globals, without modifying Strapi: - `window.STRAPI_DISABLE_STEGA_DECODING` - disable field detection entirely. When true, users need to write the `data-strapi-source` attribute manually for fields to be editable - `window.STRAPI_HIGHLIGHT_HOVER_COLOR` - customize hover highlight color - `window.STRAPI_HIGHLIGHT_ACTIVE_COLOR` - customize active highlight color ### Blocks visual editing Blocks fields hold a JSON AST, so their content can't use the stega string encoding that other field types rely on. They use a dedicated flow: the server encodes lightweight stega markers into the text leaves of the AST, and double-clicking the rendered field opens the regular `InputPopover` with the cursor pre-positioned at the clicked block. #### Stega encoding for blocks The server-side content-source-maps service (`packages/core/core/src/services/content-source-maps.ts`) has a dedicated `encodeBlocks` pass over the blocks AST. Instead of encoding every text node, it injects **one stega marker per visual block**: - `paragraph`, `heading`, and `quote` nodes: the first `{ type: 'text' }` leaf in the subtree. - `list` nodes: the first text leaf of each list item (recursively for nested lists). - `image` nodes: the `alternativeText` string field, when non-empty (encoded into the `alt` attribute — the preview script decodes this to detect image blocks). The `url` field is intentionally **not** encoded because stega-encoding it corrupts the `src` attribute and can cause the preview highlight to expand across the full page. Image blocks with no `alternativeText` receive no stega marker; the blocks field is still highlightable via its text blocks. - `code` blocks are **skipped** — encoding their content would corrupt the syntax. All markers within one blocks field share both the same `fieldPath` key (e.g., `fieldPath=content`) and the same `path` key — `blocksFieldMetadata` sets `path = fieldPath` for every marker. The presence of `fieldPath` in the source attribute is what distinguishes blocks markers from regular string-field markers, and the preview script uses it as the group key to cluster all markers into one highlight. #### Highlight grouping The `deriveGroupKey` function in the preview script detects the presence of `fieldPath` in a `data-strapi-source` attribute and strips `path` from the group key, so all encoded spans within one blocks field share a single `HighlightGroup`. The bounding box of that group is computed in `computeGroupRect`, via `getBlocksContainer`. `getBlocksContainer` trusts an already-established, still-attached container for the group without re-checking it — it only runs the guarded discovery below when there's no cached container yet. This matters because live edits can legitimately dwarf a field's currently-marked content (e.g. typing an image into a field whose only marked content is a short paragraph); re-validating on every redraw would otherwise reject the still-correct container and collapse the highlight. The cache is populated by `syncContainerObservation` (see [Live container resize](#live-container-resize)) and shared with `findBlockIndex`. Fresh discovery (`findBlocksContainer`), run only when there's no cached container: 1. Walk up the DOM from the first span in the group looking for an ancestor whose direct children include a block-level tag (`P`, `H1`–`H6`, `UL`, `OL`, `BLOCKQUOTE`, `PRE`), skipping `LI`, `UL`, and `OL` elements themselves — a list item can have a nested `
    `/`
      ` as a direct child (which would satisfy the check but is not the container), and `@strapi/blocks-react-renderer` places nested lists directly inside `
        `/`
          ` rather than wrapped in an `
        1. `, so list containers also need to be skipped as candidates. 2. Reject the candidate if its area is more than `MAX_CONTAINER_AREA_RATIO` (6x) larger than the union of the group's own marked elements — this stops an image-only field (no block-level tags of its own) from escaping into an unrelated, page-level ancestor and swallowing the whole page. 3. Use the container's `getBoundingClientRect()` as the highlight rect. This includes container padding and empty trailing blocks that have no stega spans. 4. Fallback (if no container is found): union of all span rects plus an 80 px bottom buffer so that empty trailing blocks remain clickable. #### Live container resize Live edits never carry a stega tag until the field is saved, so growing or shrinking content inside an already-open blocks field (typing a new paragraph, adding an image) doesn't touch any marked span — nothing signals the highlight manager that a redraw is needed. `syncContainerObservation` covers this gap: whenever a group's container is (re)established, a shared `containerResizeObserver` (`ResizeObserver`) watches it, and any resize triggers `updateAllHighlights()`. `containerResizeObserver` is disconnected in `__strapi_previewCleanup` alongside the other observers. Since the preview script re-runs on every iframe navigation and `strapiScript` re-injection, leaving it connected would leak one live observer — still pinning its watched DOM node — per re-run. #### Double-click flow Double-clicking a blocks field highlight opens the editor at the clicked block. 1. The preview script picks the element at the click point via `pickElementAtPoint`, which reports whether it was an **exact** hit (inside a _marked_ element's own rect) or a fallback to the nearest marked element (only good for "which field", not "which block"). - **Exact hit**: `findBlockIndex(anchor, groupKey, group)` resolves the block index — it gets the field's container via `getBlocksContainer`, walks up from the clicked span until its parent is the container, and returns the DOM child index of that element. The full `children` list (not filtered by tag name) is used so that empty paragraphs (`
          `) and other non-standard elements don't cause index drift. - **No exact hit**: the click landed on something unmarked — a code block or alt-less image (never marked, by design), or content rendered from a live, unsaved edit (not marked yet, since nothing is stega-encoded until the field is saved). Either way, `findBlockIndex` doesn't actually need a marker — it only needs a genuine DOM descendant of the field container to walk up from. The script finds the real clicked element with `document.elementsFromPoint` (skipping past the highlight overlay itself, which is what actually received the click) and resolves its block index the same way as an exact hit. This is different from falling back to the nearest _marked_ sibling (which is only reliable for "which field", not "which block", and is what the fallback anchor above is still used for — path/popover position, not block index). 2. For the popover trigger position the script uses the clicked element's rect — not the full group rect — so the popover opens adjacent to the clicked line. If the stega span is zero-width (invisible chars only), it walks up to the nearest visible ancestor. 3. `strapiFieldFocusIntent` is sent with the `path` set to the `fieldPath` value (the blocks field path) and `blockIndex` in the payload — a number, or `null` when it couldn't be resolved (see above). 4. `InputPopover` receives the message and calls `setPopoverField` with both the field metadata and the `blockIndex`. 5. `InputPopoverProvider` makes `blockIndex` available via `usePreviewPopoverBlockIndex()` to all inputs rendered inside the popover. 6. `InputRenderer` picks up `blockIndex` and passes it — along with `livePreviewSync={true}` — to `BlocksInput` → `BlocksEditor`. 7. `BlocksEditor`'s `blockIndex` effect runs on mount: it calls `Transforms.select(editor, Editor.start(editor, [targetIndex]))` to position the Slate cursor (clamped to the last block if the index is out of range) and focuses the editor. `Editor.start` — rather than a literal `[targetIndex, 0]` path — resolves the actual first valid text point inside that block, which matters for blocks whose first child isn't a plain text leaf (list items, headings with nested inline formatting). A `requestAnimationFrame` callback then scrolls that block into view inside the editor's scroll container. #### Live sync When a blocks field is rendered inside an `InputPopover`, `InputRenderer` passes `livePreviewSync={true}` to `BlocksEditor`. In this mode `onChange` fires synchronously on every Slate operation instead of being debounced by 300 ms. This keeps the form state current so that an immediate Save after typing doesn't lose the last few keystrokes. `autoFocus` is set to `true` only when `blockIndex` is `null` (i.e., the field was opened by clicking on an empty area with no block identified). When `blockIndex` is a number, the `blockIndex` effect handles both cursor positioning and focus. #### Rescan after editing Live-preview sync updates the iframe DOM while the popover is open (the host app re-renders its `BlocksRenderer` on each `strapiFieldChange`). This can change the rendered height of the blocks field, making the group highlight stale. When `popoverField` transitions from a non-null value back to `null` (the popover closed), `Preview.tsx` posts `strapiRescanHighlights` to the iframe. The preview script handles this message by scheduling `highlightManager.rescan()` via `requestAnimationFrame`, which: 1. Prunes any `data-strapi-source` elements that were removed from the DOM during editing. 2. Registers any new elements that appeared (e.g., newly created blocks). 3. Redraws all highlight positions and sizes. The `requestAnimationFrame` delay lets any in-flight React renders in the iframe commit before the rescan runs. #### Live update integration for host apps While the popover is open, the admin fires `strapiFieldChange` on every keystroke. `strapiFieldChange` is a **public event** — its name is stable and host apps can depend on it without risk of a breaking change. The preview script re-dispatches it as a CustomEvent on the iframe `window` so host apps don't need to deal with the `postMessage` origin checks themselves: ```js // Preferred: CustomEvent dispatched by the preview script on the iframe window window.addEventListener('strapiFieldChange', (e) => { const { field, value } = e.detail; // update your local state with the new blocks AST }); // Alternative: raw postMessage received in the iframe from the admin parent window.addEventListener('message', (e) => { if (e.data?.type === 'strapiFieldChange') { const { field, value } = e.data.payload; // update your local state with the new blocks AST } }); ``` --- ## useDocumentActions :::warning This hook is considered unstable and may change in a future release. ::: Contains all the operations that can be performed on a single document. Designed to be able to be used anywhere within a Strapi app. The hooks will handle notifications should the operation fail, however the response is always returned in case the user needs to handle side-effects. ### Usage ```tsx import { Form } from '@strapi/admin/admin'; const { id, model, collectionType } = useParams<{ id: string; model: string; collectionType: string; }>(); const { update } = useDocumentActions(); const handleSubmit = async (data) => { await update({ collectionType, model, documentId: id }, data); }; return
          ; ``` ### Typescript ```ts type OperationResponse = | Pick | { error: BaseQueryError | SerializedError }; type UseDocumentActions = () => { clone: ( args: { model: string; documentId: string; params: object; }, document: Omit, trackerProperty?: Extract< TrackingEvent, { name: 'willCreateEntry' | 'didCreateEntry' | 'didNotCreateEntry' } >['properties'] ) => Promise>; create: ( args: { model: string; params?: object; }, document: Omit, trackerProperty?: Extract< TrackingEvent, { name: 'willCreateEntry' | 'didCreateEntry' | 'didNotCreateEntry' } >['properties'] ) => Promise>; delete: ( args: { collectionType: string; model: string; documentId: string; }, trackerProperty?: Extract< TrackingEvent, { name: 'willDeleteEntry' | 'didDeleteEntry' | 'didNotDeleteEntry' } >['properties'] ) => Promise>; getDocument: (args: { collectionType: string; model: string; documentId?: string; params?: object; }) => Promise; publish: (args: { collectionType: string; model: string; documentId: string; }) => Promise>; update: ( args: { collectionType: string; model: string; documentId: string; params: object; }, document: Document, trackerProperty?: Extract< TrackingEvent, { name: 'willEditEntry' | 'didEditEntry' | 'didNotEditEntry' } >['properties'] ) => Promise>; unpublish: (args: { collectionType: string; model: string; documentId: string; }) => Promise>; }; ``` --- ## useDocument :::warning This hook is considered unstable and may change in a future release. ::: Returns a document based on the model, collection type & id passed as arguments. Also extracts its schema from the redux cache to be used for creating a validation schema. ### Usage ```tsx const { id, model, collectionType } = useParams<{ id: string; model: string; collectionType: string; }>(); if (!model || !collectionType) return null; const { document, isLoading, validate } = useDocument({ documentId: id, model, collectionType, params: { locale: 'en-GB' }, }); const { update } = useDocumentActions(); const onSubmit = async (entity: Entity) => { const errors = validate(entity); if (errors) { // handle errors } await update({ collectionType, model, documentId: id }, entity); }; ``` #### Content Manager If you're using this in the Strapi Content-Manager package, it is preferable to import `useDoc` instead. This is because whilst it's a light wrapper around `useDocument` it automatically extracts the params from react-router hooks to pre-provide the information for you e.g. `model` and `collectionType`. Saving us from getting the params each time and validating that they are not `undefined`. ### Typescript ```ts import type { Attribute } from '@strapi/strapi'; interface Document { documentId: string; [key: string]: Attribute.GetValue; } interface UseDocumentArgs { collectionType: string; model: string; documentId?: string; params?: object; } type UseDocument = (args: UseDocumentArgs) => { document?: Document; isLoading: boolean; validate: (entity: Entity) => null | Record; }; ``` --- ## useDragAndDrop An abstraction around `react-dnd`'s `useDrag` and `useDrop` hooks. It provides a simple API to handle drag and drop events maintaining the same behaviour across the application e.g. 1. When we consider the item to be above a new drop zone and move it immediately. 2. Item is above the drop zone, we move it when user drops it. This hook also wraps an internal hook `useKeyboardDragAndDrop` which implements keyboard accessible drag and drop by returning an onKeyDown handler to be passed to the component's drag icon button. ### Usage :::note The following examples assume that you have already set up the `DndProvider` with `HTML5Backend` in your application and that you are somewhat familiar with `@strapi/design-system` components. ::: #### Basic usage Move items on hovering over drop zone Below is a basic example usage where we move items immediately and we're not interested in rendering custom previews in the DragLayer. However, we do replace the current item with a placeholder. ```jsx import { Box, Flex, IconButton } from '@strapi/design-system'; import { Drag } from '@strapi/icons'; import { useDragAndDrop } from 'path/to/hooks'; import { composeRefs } from 'path/to/utils'; import { Placeholder } from './Placeholder'; const MyComponent = ({ onMoveItem }) => { const [{ handlerId, isDragging, handleKeyDown }, myRef, dropRef, dragRef] = useDragAndDrop(true, { type: 'my-type', index, onMoveItem, }); const composedRefs = composeRefs(myRef, dragRef); return ( {isDragging ? ( ) : ( {'My item'} )} ); }; ``` #### Basic usage - Move items on dropping over drop zone Below is a basic example usage where we move items immediately without custom previews in the DragLayer. We do not replace the current item until its dropped and show a drop placeholder instead next to drop target using drag direction. ```jsx import { Box, Flex, IconButton } from '@strapi/design-system'; import { Drag } from '@strapi/icons'; import { useDragAndDrop } from 'path/to/hooks'; import { composeRefs } from 'path/to/utils'; import { Placeholder } from './Placeholder'; const MyComponent = ({ onDropItem }) => { const [{ handlerId, handleKeyDown, isOverDropTarget, direction }, myRef, dropRef, dragRef] = useDragAndDrop(true, { type: 'my-type', index, onDropItem, }); const composedRefs = composeRefs(myRef, dragRef); return ( {isOverDropTarget && } {'My item'} ); }; ``` #### Using custom previews The only really difference between the previous example and this one is that we're using the `getEmptyImage` function from `react-dnd-html5-backend` to render custom previews in the DragLayer. ```jsx import { getEmptyImage } from 'react-dnd-html5-backend'; import { Box, Flex, IconButton } from '@strapi/design-system'; import { Drag } from '@strapi/icons'; import { useDragAndDrop } from 'path/to/hooks'; import { composeRefs } from 'path/to/utils'; import { Placeholder } from './Placeholder'; const MyComponent = ({ onMoveItem }) => { const [{ handlerId, isDragging, handleKeyDown }, myRef, dropRef, dragRef, dragPreviewRef] = useDragAndDrop(true, { type: 'my-type', index, onMoveItem, }); // highlight-start useEffect(() => { dragPreviewRef(getEmptyImage()); }, [dragPreviewRef]); // highlight-end const composedRefs = composeRefs(myRef, dragRef); return ( {isDragging ? ( ) : ( {'My item'} )} ); }; ``` ### Typescript ```ts import { Identifier } from 'dnd-core'; import { ConnectDropTarget, ConnectDragSource, ConnectDragPreview } from 'react-dnd'; interface UseDragAndDropOptions { index: number | Array; onMoveItem?: (newIndex: number | Array, currentIndex: number | Array) => void; /** * @default "regular" * Defines whether the change in index should be immediately over another * dropzone or half way over it (regular). */ dropSensitivity?: 'immediate' | 'regular'; item?: object; /** * @default 'STRAPI_DND' */ type?: string; onCancel?: (index: number) => void; onDropItem?: (currentIndex: number | Array, newIndex: number | Array) => void; onEnd?: () => void; onGrabItem?: (index: number) => void; onStart?: () => void; } type UseDragAndDropReturn = [ props: { handlerId: Identifier; isDragging: boolean; handleKeyDown: (event: KeyboardEvent) => void; isOverDropTarget: boolean; direction: string; }, objectRef: React.RefObject, dropRef: ConnectDropTarget, dragRef: ConnectDragSource, dragPreviewRef: ConnectDragPreview, ]; type UseDragAndDrop = (active: boolean, options: UseDragAndDropOptions) => UseDragAndDropReturn; ``` ### Accessibility Its advised to implement a [live text region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) in the parent component holding your individual dnd children. This should be done to inform the user of the current state of the drag and drop. To implement this, you need to pass the `onDropItem`, `onGrabItem` and `onCancel` callbacks to the `useDragAndDrop` hook which are fired only with the purpose of updating the live region, hence why they're optional. You would also update the live region as part of your `onMoveItem` callback. There are generic messages that can be used in the `intl` provider, an example of using this may look like: ```js setLiveText( formatMessage( { id: getTrad('dnd.drop-item'), defaultMessage: `{item}, dropped. Final position in list: {position}.`, }, { item: 'my item', position: 1, } ) ); ``` ### Further Reading - [react-dnd docs](https://react-dnd.github.io/react-dnd/docs/overview) - [useDrag API](https://react-dnd.github.io/react-dnd/docs/api/use-drag) - [useDrop API](https://react-dnd.github.io/react-dnd/docs/api/use-drop) - [useDragLayer API](https://react-dnd.github.io/react-dnd/docs/api/use-drag-layer) ### Troubleshooting #### Firefox quirks You might notice in the [basic usage](#basic-usage) section this piece of code: ```jsx ``` In `firefox` the drag handler will not work if you click and drag when the element is a `button`, this is known [bug in the browser](https://bugzilla.mozilla.org/show_bug.cgi?id=568313). Therefore the workaround is to use the `forwardedAs` prop to render a `div` instead of a `button` and add the `role` and `tabIndex` props to make this accessible. The actual `IconButton` component adds an accessible label from the `aria-label` prop. So we don't have to concern ourselves with that. --- ## @strapi/content-manager ### Purpose The content-manager (CM) is a core plugin that allows users to write, update and delete their content. It runs on the server and in the admin panel. In its most basic form, the CM is a table and a few forms. The admin part exports plugin APIs to change these forms and tables, and some universal hooks that other plugins can use inside and outside of the CM. The server part serves the admin API routes behind the views and checks permissions. Other core plugins, such as content-releases, review-workflows and i18n, extend it. ### Key concepts #### Documents At the core of the CM is the concept of a document. The logic that creates a document lives in `@strapi/core`, but the CM needs to know what a document is to work with it. The CM gets documents with the [`useDocument`](./hooks/use-document.md) hook and changes them with the [`useDocumentActions`](./hooks/use-document-actions.md) hook. Read [Documents](./01-documents.md). #### Layouts A layout is a data structure that tells the CM how to render the list view and the edit view. Plugins can change the edit layout with the `Admin/CM/pages/EditView/mutate-edit-view-layout` hook. They can add columns to the list view. The list and edit settings of each content type and component live in the core store, and the `content-types` and `components` services reconcile them with the schemas at `bootstrap`. Read [Layouts](./02-layouts.md). #### Plugin APIs The admin part registers a plugin object (`ContentManagerPlugin`) with these methods: `addEditViewSidePanel`, `addDocumentAction`, `addDocumentHeaderAction`, `addBulkAction` and `addRichTextBlocks`. Other plugins reach them with `app.getPlugin('content-manager').apis`. The content-releases plugin uses this route. See [Content releases in the Content Manager](./03-content-releases.md). The public exports are in [`admin/src/exports.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/content-manager/admin/src/exports.ts). #### Permissions The server checks permissions with the `permission-checker` service, on the actions `plugin::content-manager.explorer.*`. The admin part wraps the list, edit, history and preview views in the `DocumentRBAC` provider, so components can read the permissions of the current document. Read [RBAC](./03-RBAC.md) and [Permission checker](./services/00-permission-checker.md). #### Server services and routes The routes are admin routes, mounted under `/content-manager`. A `routing` middleware can send a request to another controller action if the config `layout..actions.` sets one. The `document-manager` service wraps `strapi.documents` with the deep populate that the views need. It works next to the `populate-builder`, `document-metadata` and `content-types` services. At `bootstrap`, the plugin also registers MCP tools for the displayed content types when the MCP server is enabled. Code: [`server/src`](https://github.com/strapi/strapi/tree/develop/packages/core/content-manager/server/src). #### Relations and blocks The edit view response holds only a count for each relation field, and the relations input loads and tracks the related documents itself, with its own `relations` controller on the server. Read [Relations](./04-relations.md). The blocks editor is a JSON-based rich text field built on Slate, and it works only inside the CM. Read [Blocks editor](./05-blocks.md). #### History and preview `server/src/history` and `admin/src/history` hold the content history feature. It is an Enterprise feature, gated by `cms-content-history`. When the gate is off, the plugin still registers the `historyVersion` model so that no data is lost. The `preview` folders hold the live preview. Read [Live preview](./06-preview.md). Both folders carry their own `LICENSE` file. ### Related - [Document write path](../../../architecture/05-document-write-path.md): the Document Service calls behind the CM actions. - [Enterprise Edition](../../../architecture/09-enterprise-edition.md): how the history gate works. - [Extension points](../../../architecture/04-extension-points.md): server-side ways to extend the CM routes and documents. - [HTTP request path](../../../architecture/06-http-request-path.md): how an admin route reaches a CM controller. - [`@strapi/admin`](../admin/index.md): provides the app shell, the plugin registry and the permission model. - [`@strapi/content-releases`](../content-releases/index.md): adds a side panel, an action and a column. - [`@strapi/content-type-builder`](../content-type-builder/index.md): defines the content types that the CM edits. - [`@strapi/core`](../core/index.md): provides the Document Service and the core store. --- ## Permission Checker ### Overview The **Permission Checker** is a service in Strapi that helps enforce access control policies by verifying user permissions for various actions on content entities. It provides methods to check, sanitize, and validate user actions based on their permissions. ### Features - **Permission Checking**: Verify whether a user has the necessary permissions for a specific action. - **Sanitization**: Remove unauthorized fields from input and output data. - **Validation**: Ensure that queries and input data comply with permission rules. - **Query Enforcement**: Modify queries to include permission constraints automatically. ### Actions Supported The service defines the following actions: ```ts const ACTIONS = { read: 'plugin::content-manager.explorer.read', create: 'plugin::content-manager.explorer.create', update: 'plugin::content-manager.explorer.update', delete: 'plugin::content-manager.explorer.delete', publish: 'plugin::content-manager.explorer.publish', unpublish: 'plugin::content-manager.explorer.publish', discard: 'plugin::content-manager.explorer.update', } as const; ``` ### Usage #### Instantiating the Permission Checker To use the permission checker, retrieve it from the **content-manager** plugin: ```ts const permissionChecker = strapi .plugin('content-manager') .service('permission-checker') .create({ userAbility, model }); ``` - `userAbility`: The user's ability object containing their permissions. Accessed on the request context object: `ctx.state.userAbility` - `model`: The content type model for which permissions are checked. #### Checking Permissions ##### `can.()` Determines if the user **has** permission to perform a specific action. ```ts if (permissionChecker.can.create()) { console.log('User can create content'); } ``` ##### `cannot.()` Determines if the user **does not** have permission to perform a specific action. ```ts if (permissionChecker.cannot.delete()) { throw new errors.ForbiddenError('User is not allowed to delete content'); } ``` #### Sanitization Methods ##### `sanitizeOutput(data, { action })` Cleans output data based on permissions. ```ts const sanitizedData = permissionChecker.sanitizeOutput(entity); ``` ##### `sanitizeQuery(query, { action })` Cleans a query before execution. ```ts const safeQuery = permissionChecker.sanitizeQuery({ page: '1', pageSize: '10' }); ``` ##### `sanitizeInput(action, data, entity)` Cleans input data before saving. ```ts const sanitizedInput = permissionChecker.sanitizeInput('create', inputData); ``` ##### `sanitizeCreateInput(data)` Shortcut for sanitizing create input. ```ts const sanitizedCreateData = permissionChecker.sanitizeCreateInput(inputData); ``` ##### `sanitizeUpdateInput(entity) => (data)` Shortcut for sanitizing update input. ```ts const sanitizedUpdateData = permissionChecker.sanitizeUpdateInput(existingEntity)(inputData); ``` #### Validation Methods ##### `validateQuery(query, { action })` Ensures a query is valid based on permissions. ```ts const validatedQuery = permissionChecker.validateQuery({ page: '1', pageSize: '10' }); ``` ##### `validateInput(action, data, entity)` Ensures input data is valid before saving. ```ts permissionChecker.validateInput('update', inputData, existingEntity); ``` #### Query Enforcement ##### `sanitizedQuery.(query)` Modifies queries to enforce permissions automatically. ```ts const securedQuery = permissionChecker.sanitizedQuery.read({ sort: 'createdAt:desc' }); ``` ### Exported Service ```ts export default ({ strapi }: { strapi: Core.Strapi }) => ({ create: createPermissionChecker(strapi), }); ``` ### Example Usage in Strapi ```ts const canCreate = strapi.plugin('content-manager').service('permission-checker').can.create(); if (!canCreate) { throw new errors.ForbiddenError('User does not have permission to create content'); } ``` --- ## Backend Design All backend code can be found in: ``` packages/core/content-releases/server ``` ### Content-types The content-releases plugin creates two hidden content-types. #### Release The `Release` content type stores all the information about a release and its associated Release Actions. It is saved in the database as `strapi_releases`. The schema can be found in: ``` packages/core/content-releases/server/src/content-types/release/schema.ts ``` #### Release Action Th `Release Action` content type is associated with any entry from any content-type that has draft and publish enabled. It is responsible for storing the action to perform for an associated entry. It is saved in the database as `strapi_release_actions`. In v4, we used built-in polymorphic relations, but for v5, we stored `contentType`, `locale`, and `entryDocumentId` in the Release Action schema to create a "manual" relationship between actions and entries. This approach allows us to link a release action to a document ID instead of a specific entry ID, as the entry ID may change over time and is not reliable. The schema can be found in: ``` packages/core/content-releases/server/src/content-types/release-action/schema.ts ``` ### Routes Release and Release Action routes are only accessible on the Admin API. #### Release Release routes can be found in: ``` packages/core/content-releases/server/src/routes/release.ts ``` **Get all releases**: - method: `GET` - endpoint: `/content-releases/` - params: ```ts { page: number; pageSize: number; } ``` **Get all releases with/without an entry**: - method: `GET` - endpoint: `/content-releases/getByDocumentAttached` - params: ```ts { contentTypeUid: string; locale?: string; documentId?: string; hasEntryAttached?: boolean; } ``` **Get a single release** - method: `GET` - endpoint: `/content-releases/:id` **Create a release**: - method: `POST` - endpoint: `/content-releases/` - body: ```ts { name: string; } ``` **Update a release**: - method: `PUT` - endpoint: `/content-releases/:id` - body: ```ts { name: string; } ``` **Delete a release**: - method: `DELETE` - endpoint: `/content-releases/:id` **Publish a release**: - method: `POST` - endpoint: `/content-releases/:id/publish` #### Release Action **Create a release action** - method: `POST` - endpoint: `/content-releases/:releaseId/actions` - body: ```ts { type: 'publish' | 'unpublish', contentType: string; locale?: string; entryDocumentId?: string; } ``` **Get release actions from a release** - method: `GET` - endpoint: `/content-releases/:releaseId/actions` - body: ```ts { page: number; pageSize: number; } ``` **Update a release action** - method: `PUT` - endpoint: `/content-releases/:releaseId/actions/:actionId` - body: ```ts { type: 'publish' | 'unpublish'; } ``` **Delete a release action** - method: `DELETE` - endpoint: `/content-releases/:releaseId/actions/:actionId` ### Controllers #### Release Handles requests to interact with the Release content type ``` packages/core/content-releases/server/src/controllers/release.ts ``` #### Release Action Handles requests to interact with the Release Action content type ### Services #### Release Interacts with the database for Release CRUD operations ``` packages/core/content-releases/server/src/services/release.ts ``` #### Release Actions Interacts with the database for Release Actions CRUD operations ``` packages/core/content-releases/server/src/services/release-action.ts ``` #### Release Validation Exposes validation functions to run before performing operations on a Release ``` packages/core/content-releases/server/src/services/validation.ts ``` #### Scheduling :::caution Scheduling is still under development, but you can try it **at your own risk** with future flags. The future flag to enable scheduling is `contentReleasesScheduling`. ::: Exposes methods to schedule release date for releases. ``` packages/core/content-releases/server/src/services/scheduling.ts ``` #### Release status update triggers: Considering that retrieving the status of all entries in a release is a heavy operation, we don't fetch it every time a user wants to access a release. Instead, we store the status in a field within the Release Content Type, and we only update it when an action that changes the status is triggered. These actions include: ##### Creating a release: When creating a release, its status is automatically set to "Empty" as there are no entries initially. ##### Adding an entry to a release: Upon adding an entry to a release, its status is recalculated to either "Ready" or "Blocked" based on the validity of the added entry. ##### Removing an entry from a release: After removing an entry from a release, the status is recalculated to determine if the release is now "Ready", "Blocked", or "Empty". ##### Updating a release: Whenever a release is updated, its status is recalculated based on the validity of the actions performed during the update. ##### Publishing a release: During the publishing process, if successful, the status changes to "Done"; otherwise, it changes to "Failed". ##### Listening to events on entries: When an entry is updated or deleted, the status of all releases containing that entry is recalculated to reflect any changes in validity. ### Migrations We have two migrations that we run every time we sync the content types. #### `deleteActionsOnDisableDraftAndPublish` When a user disables Draft and Publish in one Content Type we make sure to remove all the release actions related to entries of that content type to avoid errors. #### `deleteActionsOnDeleteContentType` When a Content Type is deleted, delete all actions containing entries from that Content Type. ### Subscribing to Lifecycles Events When an entry is deleted delete all actions containing that entry. --- ## Introduction(02-frontend) ### Summary There are two pages, ReleasesPage and ReleaseDetailsPage. To access these pages a user will need a valid Strapi license with the feature enabled and at least `plugin::content-releases.read` permissions. Redux toolkit is used to manage content releases data (data retrieval, release creation and editing, and fetching release actions). `Formik` is used to create/edit a release and all input components are controlled components. #### License limits Most licenses have feature-based usage limits configured through Chargebee. These limits are exposed to the frontend through [`useLicenseLimits`](../../admin/ee/hooks/use-license-limits.md). If the license doesn't specify the number of maximum pending releases a hard-coded default is used: max. 3 pending releases. #### Endpoints For a list of all available endpoints please refer to the [detailed backend design documentation](../01-backend.md). --- ## Releases page #### Overview The releases page provides a comprehensive display of all available content releases. Content is organized into two tabs: 'pending' (releases not yet published) and 'done' (releases already published). ##### New Release Creation: If a user has the `plugin::content-releases.update` permissions, a 'New release' button will be visible in the header. Clicking the button opens a form requiring a name. The name of a pending release must be unique. ##### License limits If the user reaches the limit of maximum pending releases defined on their license then the 'New release' button is disabled and a banner is displayed to inform the user. --- ## Release details page #### Overview: The release details page allows users with appropriate permissions to do the following: ##### Group entries: Entries in a release can be grouped by the following options: - Content-Types: Group all entries with the same content type together - Locales: Group all entries with the same locale together. - Actions: Group all entries with the same action (publish/unpublish) together. ##### Edit a release: If the user has the `plugin::content-releases.update` permissions, a "three dot" button will appear in the header giving access to a menu with an "Edit" option. The Edit button opens a modal, enabling users to modify the release name. To save the changes, the new name must be unique (only for pending releases), non-empty, and distinctly modified. ##### Delete a release: If the user has the `plugin::content-releases.delete` permissions, a "three dot" button will appear in the header giving access to a menu with a "Delete" option. Selecting this option triggers a confirmation modal. ##### View Entry Status Each entry within a release is assigned a status, indicating its readiness for actions such as Publish/Unpublish or any validation errors present. If the entry has validation errors, a user can use the entry's "three dots" menu to access the "Edit entry" link which navigates directly to the entry in the content manager. Within the CM edit view, users can resolve any identified errors. Upon completion, clicking the "Refresh" button on the release details page updates the status of each entry to reflect any changes made. ##### Publish a release: To execute the release of a content release, users can simply click on the "Publish" button (shown if you have the "plugin::content-releases.publish" permission). If any of the entries exhibit validation errors, the "Publish" action triggers a notification to alert users of the errors. --- ## Content Releases Scheduling :::caution Content Releases Scheduling is not yet a stable feature. Therefore, all the elements documented on this page are not currently visible. If you wish to try Releases Scheduling, you can enable it **at your own** risk using the `contentReleasesScheduling` feature flag. ::: Scheduling provides users with the ability to set a scheduled date for the release, automating its publication or unpublishing. When this happens, a webhook is triggered, providing the result of the attempt to publish the release. ### How it works Every time you create or update a release and add a scheduled date, the server responsible for handling this request will generate a new cronjob (via `strapi.cron` / croner) for the selected date to publish the release. ### Timezones When selecting a scheduled date, you have the option to choose a specific timezone; by default, your system timezone is selected. This measure is taken to prevent any potential confusion when selecting the publication time for a release. Consequently, if a user sets a schedule for 16:00 using the "Europe/Paris" timezone (UTC+01:00), another user accessing the same release will see the same time (16:00 (UTC+01:00)), regardless of their system's timezone. ### Scheduling in a architecture with multiple Strapi instances It's possible that your Strapi project runs on multiple instances. In such cases, what happens with the cronjobs? Do they all run simultaneously, attempting to publish the release multiple times? To understand how we address this scenario, it's important to differentiate between two cases when scheduling a release: #### Release scheduled on runtime If you have 3 Strapi instances running concurrently, and you distribute traffic among them using any method, there is not a big problem. This is because the server responsible for handling one request to create/update a release and add a schedule will be the only server with the associated cronjob. Then, there's no duplication, and potential race condition problems are avoided. #### Starting a strapi instance The problem is starting a new Strapi instance, because we retrieve all scheduled releases and ensure that cronjobs are created for each one. Consequently, multiple Strapi instances might end up with the same cronjob for a release publish. To address this, we implement the following logic: We set up a transaction that locks the release being published using SQL forUpdate. This means that any other processes attempting to access the release row will be put on hold until the first one finishes executing. If the validation of the release entries is successful, the publish action proceeds smoothly. In this scenario, we update the releasedAt column of the release with the current date and release the row lock. Subsequently, any incoming processes attempting to access the release would simply encounter an error because the release has already been published. On the other hand, if the publish process fails, we update the release's status to "failed". When the status is marked as failed, any subsequent attempts to publish will fail silently. The "failed" status only changes when a user makes alterations to the release. --- ## @strapi/content-releases ### Purpose A release contains various content entries, each capable of being assigned a specific action such as publish or unpublish. Within a release, entries may be in different locales or come from different content types. With a simple click of a button, a release can execute the designated action for each entry. Content Releases is an enterprise edition feature. The plugin runs on the server and in the admin panel. The admin part plugs into the Content Manager edit view and list view. The server part depends on the Document Service to publish documents. ### Key concepts #### Release and release action The plugin defines two hidden content types. `Release` (`strapi_releases`) stores the name, the status and the schedule. `Release Action` (`strapi_release_actions`) stores one action for one entry. An action links to its entry with `contentType`, `locale` and `entryDocumentId`, not with a relation, because the entry ID can change and the document ID does not. Read [Backend design](./01-backend.md). #### Release's status Releases are assigned one of five statuses: - **Ready**: Indicates that the release is fully prepared for publishing, with no invalid entries present. - **Blocked**: Release has at least one invalid entry preventing publishing. - **Empty**: Release contains no entries and cannot be published. - **Failed**: Indicates that the publishing attempt for the release has encountered an error with no changes since then. - **Done**: Confirms that the release has been successfully published without encountering any errors. These statuses are dynamically updated based on actions such as creation, addition/removal of entries, updates, and publishing attempts. They provide a concise overview of release readiness and validity, ensuring smooth operations and data integrity. #### Publishing a release The `release` service locks the release row with `forUpdate` in a transaction. It rejects a release that is already published or failed. It groups the actions by content type, publishes in an order that keeps relations, and calls `strapi.documents(uid).publish` and `unpublish`. On success it sets the status to `done`. On failure it sets `failed`. Code: [`services/release.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/content-releases/server/src/services/release.ts). #### Scheduling The `scheduling` service adds one cron job per scheduled release with `strapi.cron`. At bootstrap, `syncFromDatabase` recreates the jobs for the releases that are not yet released. The row lock stops several instances from publishing the same release twice. Read [Scheduling](./03-scheduling.md). #### Keeping actions in sync with documents The plugin registers two Document Service middlewares, `deleteActionsOnDelete` and `updateActionsOnUpdate`. It also subscribes to database lifecycles for `deleteMany`. They delete actions or revalidate entries when a document changes, and they update the status of the releases that hold them. Handlers on the content type sync hooks delete actions when draft and publish is disabled or a content type is deleted, and they revalidate the changed content types. See [Extension points](../../../architecture/04-extension-points.md). #### Built as a plugin Most EE features live in the [EE folders](../../../architecture/09-enterprise-edition.md) of their package. Releases is built as its own plugin instead. The server entry checks `strapi.ee.features.isEnabled('cms-content-releases')`. When the feature is off, the plugin exports only `register` and `contentTypes`, so the release data stays in the database. #### Admin part The admin part adds a menu link, a settings page, a dashboard widget and the Releases and Release details pages. It injects an edit view side panel, a document action, a bulk action and a list view column into the Content Manager through its plugin APIs and hooks. Read the [frontend introduction](./02-frontend/00-intro.md) and [Content Manager and releases](../content-manager/03-content-releases.md). ### Related - [Enterprise Edition](../../../architecture/09-enterprise-edition.md): how the license enables features such as `cms-content-releases`. - [Document write path](../../../architecture/05-document-write-path.md): the publish calls that a release makes. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): where the plugin `register` and `bootstrap` run. - [`@strapi/content-manager`](../content-manager/index.md): hosts the edit view, list view and bulk action extension points. - [`@strapi/admin`](../admin/index.md): permissions, settings and the widget API. - [`@strapi/core`](../core/index.md): Document Service, cron and webhook store. - [`@strapi/data-transfer`](../data-transfer/index.md): skips the release content types during a transfer. --- ## Content structure file `src/content-structure/groups.json` stores the folder organization shown by the Content Type Builder. It is an application configuration file, not content data. ### Source, build, and deployment Author and commit `src/content-structure/groups.json`. The Content Type Builder writes to that source path. During the application build, the compiler produces the runtime artifact at `dist/src/content-structure/groups.json`; Strapi reads that `dist` artifact at runtime and caches the cleaned structure in the current process. A successful Content Type Builder write invalidates that local cache, and the normal compile/reload lifecycle refreshes the `dist` artifact before the runtime reads it again. Do not edit the `dist` file directly: it is build output and will be replaced by the next build. Treat the source file as part of the application source control and deploy it with the same application revision as the content-type schemas it references. Build the deployed application from that revision so its `dist` artifact contains the same file. A deployment that reuses an old or missing build artifact will use the old folder organization, or no folders, until it is rebuilt and restarted. Data Transfer (DTS) does not inspect, transfer, restore, or delete this file. Core-store does not persist it. Moving database data or running a DTS import therefore does not move folder organization; use source control and the normal application build/deployment flow instead. ### Version 1 format The file is a JSON object with all of these required properties: - `version`: the number `1`. - `sections.collectionTypes.groups`: an array of collection-type groups. - `sections.singleTypes.groups`: an array of single-type groups. Each group has these required properties: - `id`: an opaque, non-empty string. It has no required prefix or character pattern. The Content Type Builder currently generates IDs with a `grp_` prefix, but hand-authored IDs such as `products` or `my-folder` are valid. - `name`: a user-visible string from 1 through 255 characters. It must not be blank or consist only of whitespace, must not start or end with whitespace, and must not contain control characters or ANSI escape sequences. - `parent`: `null` for a root group, or the ID of its parent group. - `children`: an array of child entries. A content-type child is `{ "type": "contentType", "uid": "" }`; a group child is `{ "type": "group", "id": "" }`. This is a minimal valid file with one empty root group: ```json { "version": 1, "sections": { "collectionTypes": { "groups": [ { "id": "products", "name": "Products", "parent": null, "children": [] } ] }, "singleTypes": { "groups": [] } } } ``` ### Validation rules Content Type Builder writes validate the complete structure strictly. A rejected write is not silently normalized into a different source file. Group IDs are unique across both sections, not only within one section. Parent and group-child references must use the exact ID and stay in the same section. The two directions of the group tree must agree: every non-root group is listed exactly once as a group child of its parent, and a root group is not a group child. A group tree cannot contain a cycle. Its root has depth 1 and no group may be deeper than depth 3. Sibling group names must be unique within the same parent, case-insensitively. For example, `Marketing` and `marketing` cannot both be root groups in `collectionTypes`, but the same name can be used under different parents or in the other section. Content-type UIDs must use a supported fully qualified form: `strapi::`, `admin::`, `api::.`, or `plugin::.`, where each name contains word characters or hyphens. On a Content Type Builder write, each referenced UID must exist in the effective content-type registry and have the kind for its section: collection types in `collectionTypes` and single types in `singleTypes`. A UID can appear in at most one group in each section. ### Runtime cleaning Runtime loading is intentionally more tolerant than a Content Type Builder write. After confirming that the JSON parses, has `version: 1`, and has a `sections` object, Strapi cleans each section in memory and logs warnings. It can drop malformed groups and children, duplicate group IDs within a section, duplicate content-type references, unknown content types, wrong-kind content types, and inconsistent group children. It reparents groups with missing parents, cycles, or excessive depth to the root, and it adds a missing reciprocal group child when the parent reference is otherwise valid. If the file cannot be parsed, has no `sections` object, or has an unsupported version, Strapi continues without content-type folders. Cleaning does not rewrite the source or `dist` file. Fix the source file, rebuild, and restart rather than relying on runtime cleaning as a migration tool. Manual source edits do not pass through the strict Content Type Builder request validator, so they must follow the validation rules above before they are built and deployed. ### Troubleshooting - If a folder is missing, check the startup warnings first. A malformed entry, an unavailable content type, a type in the wrong section, or an invalid graph edge may have been omitted while the remaining structure continued to load. - If a Content Type Builder save is rejected, check both sides of every parent/child edge, duplicate IDs across both sections, the depth limit, and sibling names after case folding. - If a source edit does not appear in production, verify that the deployed build was generated from the revision containing `src/content-structure/groups.json`; runtime does not read the source path directly. --- ## @strapi/content-type-builder ### Purpose `@strapi/content-type-builder` (CTB) is a core plugin that lets a developer model data in the admin panel. The server part reads the registered content types and components. It writes the changes as schema files in the application source, and then reloads the server. The admin part is the visual editor for these schemas. The write routes work only in development, with `autoReload` on. Other packages rely on it in two ways. The Content Manager shows the content types that CTB creates. Plugins such as `i18n` extend the CTB forms through the `forms` API of the admin plugin. Application developers commit the files that CTB writes. ### Key concepts #### Schema files are the source of truth CTB does not store schemas in the database. A content type of an API lives in `src/api//content-types//schema.json`. A component lives in `src/components//.json`. A content type from a plugin lives under `src/extensions//content-types/`. At the next boot, `@strapi/core` loads these files and `@strapi/database` syncs the tables. Read [Schema sync](../../../architecture/07-schema-sync.md). #### Schema builder `createBuilder()` in [`services/schema-builder`](https://github.com/strapi/strapi/tree/develop/packages/core/content-type-builder/server/src/services/schema-builder) copies the current `strapi.contentTypes` and `strapi.components` into an in-memory model. It offers create, edit and delete methods for both. `writeFiles()` flushes every changed schema to disk. `rollback()` restores the previous files. If a write fails, the builder rolls back and the service throws `Invalid schema edition`. #### Batch update The admin panel does not save one attribute at a time. It sends the whole change set to `POST /content-type-builder/update-schema`. Each content type, component and attribute in the payload has an `action` (`create`, `update` or `delete`). The `schema` service applies the payload to the builder, writes the files, generates or clears API folders, and commits the folder groups last. On error, `rollbackSchemaMutation` restores the touched files. Code: [`services/schema.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/content-type-builder/server/src/services/schema.ts). #### Reload after a write After a successful update, the `schema` controller sets `strapi.reload.isWatching` to `false` and calls `strapi.reload()` in a `setImmediate`. The reloader sends a `reload` message to the parent process of `strapi develop`, which restarts the server. The route `GET /update-schema-status` returns `isUpdating`. The admin polls it with `useServerRestartWatcher` until the server is back. #### Development-only writes The `isDevelopmentMode` middleware guards every write route. It throws a `PolicyError` unless `autoReload` is `true`. This blocks schema changes in production, where a restart is not safe. The read routes do not use it. See [`middlewares/is-development-mode.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/content-type-builder/server/src/middlewares/is-development-mode.ts). #### API generation When CTB creates a content type, `content-types.generateAPI` calls `@strapi/generators` to create the API folder with a controller, a service and a router. The `api-handler` service backs up and removes these folders when a content type is deleted. CTB manages only `api::` content types for deletion. See the [`@strapi/generators` package page](../../generators/generators/index.md). #### Content structure (folders) The `content-structure` service writes the folder groups that the admin navigation shows. The file is `src/content-structure/groups.json`. It is application source, not content data. Read [Content structure file](./01-content-structure.md). #### Admin editor The admin part registers a menu link and the plugin API `forms`, which other plugins use to add fields and validation to the CTB modals. `DataManagerProvider` keeps the working copy of the schema in a Redux slice with undo and redo (`undoRedo.ts`). Modals in `components/FormModal` edit attributes and types. The read permission is `plugin::content-type-builder.read`, which `bootstrap` registers. ### Related - [Schema sync](../../../architecture/07-schema-sync.md): what happens to the tables after CTB writes a file. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): how the reload replays register and bootstrap. - [Extension points](../../../architecture/04-extension-points.md): the `src/extensions` folder and plugin content types. - [`@strapi/content-manager`](../content-manager/index.md): edits the documents of the types that CTB defines. - [`@strapi/core`](../core/index.md): loads schema files and provides `strapi.reload`. - [`@strapi/i18n`](../../plugins/i18n/index.md): extends the CTB forms. - [`@strapi/database`](../database/index.md): creates the tables for the schemas. - [`@strapi/generators`](../../generators/generators/index.md): creates the API folders. --- ## Introduction(Configuration) ## Configuration This section is an overview of how configuration files are loaded in Strapi and how they should be used. ### Config loading Loading of configuration is the very first thing Strapi does in bootstrap (after setting signal handlers), which is necessary because most of the Strapi bootstrap process depends on values from the configuration. This means that configuration loading happens even before Strapi creates a logger. Therefore any errors or warnings from the configuration unfortunately must output to `console` NOTE: For now. in theory, we could define some configuration files that do not depend on being passed a `strapi` object and could provide logger settings in there, but at this time we do not have that capability. Currently, Strapi loads every .js and .json file within a project's `./config` directory directly into the strapi config object. That is, if there is a file called `./config/myconfig.js` it will be loaded automatically and accessible from `strapi.config.get('myconfig')` (NOT YET IMPLEMENTED) Starting in Strapi v5, will load every defined value from the `STRAPI_` environment namespace into the configuration, using underscore (\_) as the object delimiter. For example, if you define `STRAPI_ADMIN_APP_KEYS=abcd,efg` it will be available from `strapi.config.get('admin.app.keys')` as `['abcd', 'efg']` Along with that feature, all configuration values will become case-insensitive. That is, accessing `strapi.config.get('admin.App.Keys')` will return the same value as `strapi.config.get('admin.app.keys')`. ### Base Strapi Configurations The following are considered 'base' Strapi configurations and a description of what each file contains is given, so that new settings can be placed in the appropriate place. A Strapi configuration file must export either an object, or a function that returns an object. WARNING: There are some discrepancies where certain configuration files such as middlewares may return an array and some files may not be able to export a function. Please confirm before use, and update this documentation when exceptions are found. (NOT YET IMPLEMENTED) Starting in Strapi v5, the contents of the base configurations will be strictly defined and no values that are not created by Strapi will be allowed in them; that is, an error will be thrown on unrecognized values. Any user additions must go in separate configuration namespaces. #### Base Config Names ##### admin Defines settings related to the Strapi admin panel, such as the 'autoopen' option for `yarn develop` ##### server Defines settings related to the Strapi backend server, such as 'host' and 'port' ##### database Defines the database configuration options. NOTE: Most of this is defined by Knex, be careful when adding options that they are compatible or placed in the appropriate location ##### api Content API configuration, for example the 'rest.maxLimit' option to set the maximum `limit` available in a query. ##### features Feature flags for enabling and configuring future features that would be breaking changes and cannot be enabled by default, or experimental features that are not ready for public stable release. ##### typescript Controls TypeScript tooling during development (for example `autogenerate` to emit types for JavaScript projects). ##### plugins Contains plugin configurations, with each root-level value the id of a plugin, for example, 'users-permissions' IMPORTANT: Plugins are not loaded like other Strapi configurations into the `plugins.` namespace, because they are loaded during the module loading. That means that in an example plugins.js file like below, its values are accessible at, for example, `strapi.config.get('plugin::graphql.endpoint')` ``` module.exports = () => ({ graphql: { enabled: true, config: { endpoint: '/graphql', defaultLimit: 25, maxLimit: 100, depthLimit: 10, apolloServer: { tracing: true, }, }, }, }); ``` NOTE: Because this configuration contains user and plugin keys and Strapi does not yet provide a method to extend the definitions, this configuration is not strictly typed and will not be loaded from the environment variables ##### middlewares The configuration of middlewares. NOTE: This is the only configuration that is currently an array rather than an object. It also very loosely typed. Future improvements are planned to strongly type all the built-in Strapi middlewares. #### Typings Most of the base configurations have been typed, although they are not currently publicly available. They can be found in: `packages/core/types/src/core/config` Any time a new configuration option is added to Strapi, it must also be added to the types for the appropriate config file (and once the env var loading feature is available, a parser for it such as string, integer, stringArray, etc must be defined) In Strapi v5, they will be used for: - user-facing config factories to assist in typing project configuration - as part of the environment variable configuration loading to validate the structure of the base config file parsers to parse and strongly type values from the environment #### Configuration filename restrictions In Strapi v5, environment variables with the prefix `STRAPI_` will be loaded automatically into Strapi configuration. Because of that, some naming restrictions have been added to prevent conflicts. - filenames without extension must be case-insensitive unique (ie, only one file among ADMIN.js and admin.js or admin.js and admin.json) - All base Strapi configurations and a set of restricted names currently found in `packages/core/core/src/configuration/config-loader.ts` may not be used **even as prefixes** IMPORTANT: Any new base Strapi configurations should also be limited to a-z0-9, no special characters! This is necessary to allow environment variable loading. The same restriction does not apply to user config files, but if not followed that file will not support autoloading env. #### Base attribute name restrictions Configuration options defined for Strapi configurations must include only a-zA-Z0-9! If a key is used with a special character, underscore, or dash, it will not be configurable from the environment variables, and people will get angry that it was poorly designed. Please use camelCase like all existing Strapi configuration keys. There are not currently any special characters defined in Strapi configurations, with the sole exception of plugins.js 'users-permissions', please keep it that way. --- ## Recommended security defaults New Strapi applications scaffolded with `create-strapi-app` ship with the following security-oriented defaults. Existing projects can adopt the same values in `config/api`, `config/plugins`, and `config/server`. ### Strict API parameters Reject unrecognized query and document-service parameters so typos and injection-style keys fail fast instead of being silently ignored. `config/api.js` / `config/api.ts`: ```js module.exports = { rest: { strictParams: true, }, documents: { strictParams: true, strictRelations: true, }, }; ``` See also: [strictParams and custom Content API parameters](https://docs.strapi.io/cms/backend-customization/routes#custom-content-api-parameters). ### Strict required relations Enforce `required` on media and relation fields at the document-service level. Without this flag a `required` media or relation can be left empty on published content, so entries that are missing mandatory associations still get created or published. With `documents.strictRelations: true`, a write that leaves a required media/relation empty is rejected. `config/api.js` / `config/api.ts` (shown in the snippet above): ```js module.exports = { documents: { strictRelations: true, }, }; ``` - Enforced only on **published** (non-draft) writes — drafts stay lenient so you can save work in progress with associations still missing. - Applies to required media and relations, including those nested inside components and dynamic zones. - On by default in newly scaffolded apps only; existing projects keep the legacy (lenient) behaviour until they opt in, so enabling it will start rejecting previously-accepted writes that omit a required association. ### Content API authentication (refresh tokens) Use short-lived access tokens with refresh rotation instead of long-lived legacy JWTs. Store refresh tokens in httpOnly cookies when clients support cookie-based auth. `config/plugins.js` / `config/plugins.ts`: ```js module.exports = () => ({ 'users-permissions': { config: { jwtManagement: 'refresh', sessions: { httpOnly: true, }, }, }, }); ``` Scaffolded `.env` files include a randomly generated `JWT_SECRET` alongside other secrets. See [JWT management modes](https://docs.strapi.io/cms/features/users-permissions#jwt-management-modes) for client integration details. ### Optional GraphQL plugin operation limits GraphQL is optional and is not configured in applications scaffolded by `create-strapi-app`. If your application exposes GraphQL, configure list and nesting limits in `config/plugins.js` or `config/plugins.ts`: ```js module.exports = () => ({ graphql: { config: { defaultLimit: 25, maxLimit: 100, depthLimit: 10, }, }, }); ``` Review these values against your client queries: `maxLimit` can cap list results and `depthLimit` can reject deeply nested operations. Use deployment-level rate limiting and timeouts as additional defense in depth; these limits are not a complete protection against expensive operations. The current GraphQL plugin defaults are `maxLimit: -1` (unbounded) and no `depthLimit`. When pagination is omitted, the effective list limit is 10. At startup, Strapi warns when its built-in `maxLimit` or `depthLimit` is unbounded or invalid. The warning is informational: it does not change existing query behavior, and custom Apollo validation rules may independently enforce limits. ### Upload MIME restrictions New projects scaffolded with `create-strapi-app` use an allowlist for common media and document types, plus a denylist for SVG, executables, and shell scripts. When both are set, denied types take precedence over the allowlist. `config/plugins.js` / `config/plugins.ts`: ```js module.exports = () => ({ upload: { config: { security: { allowedTypes: [ 'image/*', 'video/*', 'audio/*', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.*', 'text/plain', 'text/csv', ], deniedTypes: [ 'image/svg+xml', 'application/vnd.microsoft.portable-executable', 'application/x-msdownload', 'application/x-msdos-program', 'application/x-executable', 'application/x-dosexec', 'application/x-sh', 'text/x-shellscript', 'application/x-mach-binary', ], }, }, }, }); ``` See [MIME type validation](../../upload/01-backend/01-mime-validation.md) for how allow/deny lists are evaluated. #### SVG uploads and existing projects Although `image/*` matches SVG files, newly scaffolded projects explicitly deny `image/svg+xml`. Existing projects can adopt this protection by adding `image/svg+xml` to `plugin::upload.security.deniedTypes`; entries in `deniedTypes` take precedence over the allowlist. This does not change existing projects, projects without an explicit upload policy, or objects that were already uploaded. Projects that intentionally accept SVG should sanitize it before serving it. SVG can embed scripts and pose an XSS risk when served from the same origin as your app. Serving uploads from a separate domain or CDN can reduce that risk, but remote providers and CDNs control the final delivery headers, so this configuration does not provide a universal delivery guarantee. ### Webhook payloads Limit relation data in webhook payloads to reduce accidental data exposure: `config/server.js` / `config/server.ts`: ```js module.exports = ({ env }) => ({ webhooks: { populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false), }, }); ``` ### CORS in production The `strapi::cors` middleware defaults to `origin: '*'` with `credentials: true`, which suits local development but is too permissive for production. Restrict `origin` to your frontend URL(s) in `config/middlewares`: ```js module.exports = [ // ... { name: 'strapi::cors', config: { origin: ['https://your-frontend.example'], credentials: true, }, }, // ... ]; ``` ### Other production hardening - Enable **database SSL** for managed PostgreSQL or MySQL (`DATABASE_SSL=true`). - Set **`server.proxy: true`** when running behind a reverse proxy so secure cookies and client IPs work correctly. - Use **HTTPS** for remote data transfer (`strapi transfer --from` / `--to`). --- ## Event Hub ### Summary The event hub is a central system that processes a variety of events in a Strapi application. These events can be emitted from a variety of sources to trigger associated subscriber functions. _above: A diagram showing how the event hub processes events from different sources with multiple subscribers_ Events are mainly used in Strapi to power the webhooks and audit logs features. However, plugin developers can also access the event-hub using the plugin API. This means plugins can listen to events emitted by Strapi and emit new events to the event hub. ### Detailed design The event hub is a store of subscriber functions. When an event is emitted to the hub, each subscriber function in the store will be called with the event's name and a variable number of arguments. This design was inspired by the way Strapi handles [lifecycle hooks](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#lifecycle-hooks). It was chosen over the [Node.js event emitter](https://nodejs.org/api/events.html#class-eventemitter) because it provides the ability to have a single subscriber function per feature, and does not cause [memory leak concerns](https://stackoverflow.com/questions/9768444/possible-eventemitter-memory-leak-detected). #### Emitting events ##### `emit` Dispatches a new event into the hub. It returns a promise that resolves when all the subscribers have run. ```ts // Types type Emit = (name: string, ...args: any[]) => Promise; // Usage strapi.eventHub.emit('some.event', { meta: 'data' }); ``` #### Managing subscribers ##### `subscribe` Adds a subscriber function that will be called for each event emitted to the hub. It returns a function that can be called to remove the subscriber. ```ts // Types type Subscriber = (name: string, ...args: Object) => void | Promise; type UnsubscribeCallback = () => void; type Subscribe = (subscriber: Subscriber) => UnsubscribeCallback; // Add a subscriber const unsubscribe = strapi.eventHub.subscribe((name: string, ...args: any[]) => { // Write your subscriber logic here }); // Remove the subscriber using the returned function unsubscribe(); ``` ##### `unsubscribe` Removes a subscriber function. You need to give it the reference of the subscriber as an argument. ```ts // Types type Subscriber = (name: string, ...args: any[]) => void | Promise; type Unsubscribe = (subscriber: Subscriber) => void; // After a subscriber has been added const subscriber: Subscriber = (name, ...args) => {}; strapi.eventHub.subscribe(subscriber); // Use its reference to remove it strapi.eventHub.unsubscribe(subscriber); ``` #### Listening to a single event If you only need to run a function for one specific event, then creating a subscriber function may be overkill. For this reason, the event hub provides the `on`,`off` and `once` methods, inspired by the [Node.js event emitter](https://nodejs.org/api/events.html#class-eventemitter). ##### `on` Registers a listener function that is called every time a _specific_ event is emitted. It returns a function that can be called to remove the listener. ```ts // Types type Listener = (args: any[]) => void | Promise; type RemoveListenerCallback = () => void; type On = (eventName: string, listener: Listener) => RemoveListenerCallback; // Add a listener const removeListener = strapi.eventHub.on('some.event', () => { // Write your listener logic here }); // Remove the listener using the returned function removeListener(); ``` ##### `off` Removes a listener function. You need to give it the name of the event it's listening to, as well as the reference of the listener as an argument. ```ts // Types type Listener = (args: any[]) => void | Promise; type Off = (listener: Listener) => void; // After a listener has been added const listener: Listener = (...args) => {}; strapi.eventHub.on('some.event', listener); // Use its reference to remove it strapi.eventHub.off('some.event', listener); ``` ##### `once` Registers a listener function that will only be called the first time an event is emitted. Once the event has been emitted, the listener will be removed automatically. It also returns a function that can be used to remove the listener before it was called. ```ts // Types type Listener = (args: any[]) => void | Promise; type RemoveListenerCallback = () => void; type Once = (eventName: string, listener: Listener) => RemoveListenerCallback; // Add a single-use listener const removeListener = strapi.eventHub.once('some.event', () => { // Write your listener logic here }); // Remove the single-use listener using the returned function removeListener(); ``` ### Tradeoffs - Potential breaking changes: a change to an event's name or payload may affect other features or plugins listening to the same event. Backwards compatibility is a concern when managing these events. - Performance: Strapi emits a lot of events, so you need to make sure your subscriber functions aren't too expensive to run. ### Alternatives You may not need the event hub: - If you want to listen to database events on a specific content type, use [lifecycle hooks](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#lifecycle-hooks) - If you want to listen to database events on all content types, use a [generic database lifecycle hook](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#declarative-and-programmatic-usage) - If you want to emit an event, but don't want it to be exposed to other features or plugins, create a [service](https://docs.strapi.io/developer-docs/latest/development/backend-customization/services.html#services) and call it directly instead --- ## @strapi/core ### Purpose `@strapi/core` is the server runtime. It defines the `Strapi` class and starts, runs and stops a Strapi application. It loads the configuration, the plugins, the APIs, the components and the middlewares. It hosts the core services, for example the Document Service, the HTTP server, the event hub, webhooks and the MCP server. It runs on the server only. Applications do not import it directly. They import `@strapi/strapi`, which re-exports `@strapi/core`. Plugins get the `strapi` instance as a parameter. CLI commands call `createStrapi()` and `compileStrapi()`. ### Key concepts #### The `Strapi` class and the container `Strapi` extends `Container`, a small service locator with `add`, `get` and `has`. The constructor loads the configuration and adds the core services to the container. `createStrapi()` in [`src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/index.ts) builds the instance. Read [Container and registries](../../../architecture/03-container-and-registries.md). Code: [`src/Strapi.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/Strapi.ts) and [`src/container.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/container.ts). #### Providers A provider is an object with optional `init`, `register`, `bootstrap` and `destroy` functions, created with `defineProvider`. The `providers` array in `src/providers/index.ts` sets the call order. Providers add the registries, the admin, the core store, the session manager, webhooks, telemetry, cron and the MCP server to the container. They are not the same as the packages in `packages/providers/`. Read [Server lifecycle](../../../architecture/02-server-lifecycle.md). #### Registries Registries store the artifacts that plugins, APIs and the application define: content types, components, services, controllers, policies, middlewares, hooks and others. Each artifact has a UID with a namespace such as `api::` or `plugin::`. The `registries` provider adds them to the container. Code: [`src/registries`](https://github.com/strapi/strapi/tree/develop/packages/core/core/src/registries). #### Loaders and modules Loaders read plugins, APIs, components, middlewares, policies, sanitizers and validators from disk and from `node_modules` into the registries. A plugin or an API becomes a module (`createModule`), which runs its own `register`, `bootstrap` and `destroy`. Code: [`src/loaders`](https://github.com/strapi/strapi/tree/develop/packages/core/core/src/loaders) and [`src/domain/module`](https://github.com/strapi/strapi/tree/develop/packages/core/core/src/domain/module). See [Extension points](../../../architecture/04-extension-points.md). #### Core services Core services live in [`src/services`](https://github.com/strapi/strapi/tree/develop/packages/core/core/src/services). They are not in the `services` registry. Examples are the Document Service (`strapi.documents`), the HTTP `server`, `eventHub`, `webhookRunner`, `entityValidator` and `content-api`. The `db` entry creates the `Database` from `@strapi/database`. The `config` service holds the configuration that the loader reads from the `config/` folder of the application. Read [Document write path](../../../architecture/05-document-write-path.md), [Event hub](./event-hub.md), [MCP server](./mcp-server.md), [Localization provider](./localization.md), [Server-side telemetry](./telemetry.md), [Configuration](./configuration/00-intro.md) and [Recommended security defaults](./configuration/01-security-defaults.md). #### HTTP server The `server` service wraps a Koa app. It builds the route managers for the admin API and the Content API, and it registers the middlewares and the routes at `bootstrap`. Code: [`src/services/server`](https://github.com/strapi/strapi/tree/develop/packages/core/core/src/services/server). Read [HTTP request path](../../../architecture/06-http-request-path.md). #### Core API factories `createCoreController`, `createCoreService`, `createCoreRouter` and `createCoreValidator` in `src/factories.ts` build the default controller, service, router and validator of a content type. A user extends the default with a config object or callback. The exports are available as `factories` from `@strapi/strapi`. Code: [`src/core-api`](https://github.com/strapi/strapi/tree/develop/packages/core/core/src/core-api). #### Enterprise Edition gate `strapi.EE` reads `utils.ee.isEE`. The license check runs in `bootstrap()`, after the schema sync. Code: [`src/ee`](https://github.com/strapi/strapi/tree/develop/packages/core/core/src/ee). Read [Enterprise Edition](../../../architecture/09-enterprise-edition.md). ### Related - [Architecture overview](../../../architecture/index.md): where the runtime fits in the monorepo. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): boot order of providers, loaders and modules. - [Container and registries](../../../architecture/03-container-and-registries.md): container keys and registry rules. - [Glossary](../../../architecture/11-glossary.md): naming traps for provider, service, module and hook. - [Future flags](../../../architecture/10-future-flags.md): opt-in features that the runtime reads. - [`@strapi/strapi`](../strapi/index.md): entry package that re-exports this one. - [`@strapi/database`](../database/index.md): `strapi.db`. - [`@strapi/types`](../types/index.md): the `Core.Strapi` type and the `Modules.*` contracts. - [`@strapi/admin`](../admin/index.md): loaded by the `admin` provider. --- ## Localization provider Core depends on `Modules.Localization.Provider`, exposed through `strapi.localization`, for localization behavior. The i18n plugin registers its implementation during `register`, before database migrations and content-type synchronization. Plugins that register before i18n, and API modules, see the inert defaults during their own `register`. The adapter resolves i18n services when called, so services replaced or extended through the services registry after i18n's `register` are observed; `src/extensions/` overrides are applied at load time and need no such care. Core does not import i18n's contracts or look up the plugin by name. Only one provider can be registered per application: a second registration throws. i18n's own surface (the `locale` and `localizations` attributes it adds to content types, its routes, GraphQL extension and sanitizers) reads the schema flag through its own services and does not consult `strapi.localization`. A provider's `isLocalizedContentType` must therefore agree with that flag. Without a provider, `isEnabled()` is `false`, content types are treated as nonlocalized, the default locale is `null`, the locale list, populate paths and nonlocalized attributes are empty, and copying nonlocalized fields leaves the entry unchanged. Use `isEnabled()` when the absence of a localization plugin must be told apart from a plugin with zero locales. These defaults serve test fixtures, embeddings and providers from other plugins; the bundled i18n plugin cannot be disabled through configuration, so a stock application always has a provider. Each Strapi instance has its own provider. --- ## MCP Server Strapi ships an opt-in [Model Context Protocol](https://modelcontextprotocol.io/) server that exposes content and platform capabilities (tools, prompts, resources) to AI clients over HTTP. It is disabled by default and lives entirely in `@strapi/core`, with `@strapi/content-manager` as its main built-in consumer. Source: `packages/core/core/src/services/mcp/`, `packages/core/core/src/providers/mcp.ts`, `packages/core/core/src/mcp.ts`. Content-manager integration: `packages/core/content-manager/server/src/mcp/`. Shared types: `packages/core/types/src/modules/mcp.ts`. ### Summary | Concern | Answer | | ------------- | -------------------------------------------------------------------------------------------------------------- | | Enable | `server.mcp.enabled` (default `false`) | | Endpoint | `POST /mcp` (fixed path, not configurable) | | Transport | MCP SDK `NodeStreamableHTTPServerTransport`, stateless (one server instance per request) | | Auth | Admin API token (`Authorization: Bearer `) — same store as Settings → API Tokens (admin) | | Authorization | Two layers: coarse session capability gate (CASL ability vs `auth.policies`) + fine-grained handler checks | | Extensibility | Any plugin or app can register tools/prompts/resources via `strapi.ai.mcp.register*` during the register phase | | Built-in | One dev-only `log` tool (core) + one CRUD tool set per displayed content type (content-manager) | | SDK | `@modelcontextprotocol/server@2.0.0` + `@modelcontextprotocol/node@2.0.0` | ### Architecture ``` init providers/mcp.ts → strapi.add('ai.mcp', createMcpService) register plugins / app → strapi.ai.mcp.registerTool|Prompt|Resource(...) bootstrap plugins (content-manager) → registerContentManagerMcpTools(...) providers/mcp.ts → strapi.ai.mcp.start() → mounts /mcp routes request POST /mcp → authenticate → per-request McpServer → sync capabilities → handle → close ``` Two pieces cooperate: | Piece | File | Responsibility | | -------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Provider | `packages/core/core/src/providers/mcp.ts` | Lifecycle glue only: registers the `ai.mcp` DI key on `init`, calls `start()`/`stop()` on `bootstrap`/`destroy` if enabled. | | Service | `packages/core/core/src/services/mcp/index.ts` | The real implementation: capability registries, HTTP route mounting, the built-in `log` tool, status tracking (`idle` → `starting` → `running`). | The provider is registered last in `packages/core/core/src/providers/index.ts`, after `ai` (which creates the `strapi.ai` namespace that `mcp` attaches to). #### Bootstrap ordering matters Strapi's `bootstrap()` phase runs plugin lifecycles **before** provider lifecycles (`packages/core/core/src/Strapi.ts` — `runPluginsLifecycles(BOOTSTRAP)` runs, then the providers loop runs). Practically: 1. Plugin `bootstrap()` hooks run — this is where content-manager derives and registers its per-content-type tools (`packages/core/content-manager/server/src/bootstrap.ts`). 2. Provider `bootstrap()` hooks run — the `mcp` provider calls `strapi.ai.mcp.start()`, which mounts the `/mcp` routes. 3. The app's own `bootstrap()` (`src/index.ts`) runs **after** providers — too late to register capabilities. :::caution Register tools, prompts, and resources from a plugin's `register()` phase (preferred) or `bootstrap()` phase (works, but only for plugins — app-level `bootstrap()` is too late). `strapi.ai.mcp.registerTool/Prompt/Resource` throw once the server has left the `idle` status. ::: ### Configuration | Key | Default | Notes | | ----------------------------- | ------- | ------------------------------------------------------ | | `server.mcp.enabled` | `false` | Must be `true` to start the server | | `server.mcp.connectTimeoutMs` | `5000` | Timeout for `mcpServer.connect(transport)` per request | | `server.mcp.requestTimeoutMs` | `60000` | Timeout for `transport.handleRequest(...)` per request | Implementation: `packages/core/core/src/services/mcp/internal/McpConfiguration.ts`. Type: `McpConfig` in `packages/core/types/src/core/config/server.ts`. Dev mode (`autoReload` config) additionally unlocks `devModeOnly` capabilities like the built-in `log` tool — see [Authorization](#authorization). ### Transport & routes Routes (`packages/core/core/src/services/mcp/routes.ts`): | Method | Path | Behavior | | ---------------------------------- | ------ | ------------------------------------------------------------------------------------------------- | | `POST` | `/mcp` | Real handler — authenticate, then hand off to the MCP SDK transport | | `GET` / `DELETE` / `PUT` / `PATCH` | `/mcp` | JSON-RPC `METHOD_NOT_ALLOWED` (so MCP clients get a parseable error instead of a default 404/405) | All five routes set `config: { auth: false }` — Strapi's own admin/content-API authentication is bypassed at the router level because MCP performs its **own** authentication inside the handler (see below). The transport is the MCP SDK's `NodeStreamableHTTPServerTransport` with `sessionIdGenerator: undefined`, i.e. **stateless**: a fresh `McpServer` instance is created and connected for every POST request, then closed in a `finally` block (`packages/core/core/src/services/mcp/handlers/handlePost.ts`). An OAuth discovery fallback middleware (`middleware/oauthDiscoveryFallback.ts`) intercepts `/.well-known/oauth-*` and `/register` with a plain JSON 404, since some MCP clients probe these paths and would otherwise hit Strapi's default HTML 404 page. ### Authentication `packages/core/core/src/services/mcp/authentication.ts`: 1. Extract `Authorization: Bearer ` — missing/malformed header → `missing_token`. 2. Validate through `strapi.admin.services['api-token-admin'].authenticateAdminToken(token)` — the **same admin API token store** used by Settings → API Tokens. Invalid/expired token → `invalid_token`. 3. On success, the handler receives the token's CASL `ability` and owning `user`. There is no separate "MCP token" type and no admin-panel screen dedicated to MCP — an admin creates a regular admin API token and uses it as the bearer credential for `/mcp`. Both failure reasons are reported through telemetry (`didNotAuthenticateMcpRequest`) but returned to the client as a single generic JSON-RPC `AUTHENTICATION_REQUIRED` error — the distinction is for observability, not client-facing signaling. ### Authorization Authorization happens in two layers. #### 1. Session capability sync (coarse allowlist) On every request, after authentication, `syncMcpSessionCapabilities` (`packages/core/core/src/services/mcp/internal/syncMcpSessionCapabilities.ts`) walks every registered tool/prompt/resource definition and enables or disables it **for that session's `McpServer` instance** based on: - `devModeOnly: true` → enabled only if `config.isDevMode()` (i.e. `autoReload` is on); never exposed in production. - `auth: { policies: [...] }` → enabled if the ability satisfies **any** policy (`ability.can(action, subject)`), OR'd across the array. This is deliberately coarse — it decides what a client sees in `tools/list` etc. It does **not** replace per-request checks; the source comment is explicit: _"Field/entity conditions must be enforced by capability handlers with request-specific context."_ #### 2. Handler-level checks (fine-grained) Content-manager's derived tools re-run the same `permission-checker` service used by REST controllers inside each handler: entity-level checks (can this user act on _this_ document?), field-level filtering (`getPermittedFields`), and locale filtering (`resolvePermittedLocaleSchema`) — all computed **per request**, from the caller's ability, and folded directly into the tool's Zod input/output schema via `resolveInputSchema`/`resolveOutputSchema`. A user with `read` on an article but not on its `body` field never sees `body` in the tool's schema or output. Custom tools should follow the same pattern: use `auth.policies` for the coarse gate, then call into the same policy/permission services your REST controllers use for the fine-grained checks. ### Advertised schema wire contract The `inputSchema` and optional `outputSchema` returned by `tools/list` are JSON Schema 2020-12 documents. A schema can declare this dialect with `https://json-schema.org/draft/2020-12/schema` or omit `$schema`; the MCP specification defines an absent declaration as 2020-12. Capability authors and clients must interpret the document structurally as 2020-12. For example, positional tuples use `prefixItems`, and generated recursive schemas use `$defs` rather than the older `definitions` container. Strapi passes each capability's authored Zod schema to the MCP SDK. Zod serializes it and the SDK advertises that result. Strapi does not rewrite or re-serialize advertised schemas. This boundary is covered by tests that enumerate through a real MCP client and through an authenticated `/mcp` request, then compile every advertised input and output schema as 2020-12. #### Current content-manager schema observations The known SDK conversion hazards are tracked upstream in [#2464](https://github.com/modelcontextprotocol/typescript-sdk/issues/2464) and [#2636](https://github.com/modelcontextprotocol/typescript-sdk/issues/2636). Authenticated enumeration of real content-manager tools currently shows: - Date, datetime, time, and timestamp write fields are advertised as strings. Content-manager does not author these fields with `z.date()`, so the date conversion failure in #2464 is not present. - Content-manager output schemas contain no defaulted fields, so the defaulted-output mismatch in #2464 is not present. - Extra-property declarations follow the authored object behavior: strict write-data objects advertise `additionalProperties: false`, loose output objects advertise an open `additionalProperties` schema, and ordinary input objects can omit the keyword. The omission described in #2636 is present, but it remains valid JSON Schema 2020-12 and does not drop any content-manager tool in strict schema validation. ### Defining and registering capabilities Public builders are re-exported from `@strapi/core` / `@strapi/strapi` as `ai.mcp.defineTool`, `ai.mcp.definePrompt`, `ai.mcp.defineResource` (`packages/core/core/src/mcp.ts` → `services/mcp/{tool,prompt,resource}-registry.ts`). They are identity functions at runtime — their only job is inferring/narrowing TypeScript types (input/output schema shape, `devModeOnly` vs `auth` access variant). ```ts import { ai } from '@strapi/strapi'; import { z } from '@strapi/utils'; const greet = ai.mcp.defineTool({ name: 'greet', title: 'Greet', description: 'Greets a user by name', auth: { policies: [{ action: 'plugin::my-plugin.greet' }] }, resolveInputSchema: () => z.object({ name: z.string() }), resolveOutputSchema: () => z.object({ message: z.string() }), createHandler: (strapi) => async ({ args }) => { const message = `Hello, ${args.name}!`; return { content: [{ type: 'text', text: message }], structuredContent: { message } }; }, }); // in a plugin's register() (preferred) or bootstrap(): strapi.ai.mcp.registerTool(greet); ``` `registerPrompt` and `registerResource` follow the same shape (`argsSchema`/`createHandler` returning an `McpPromptResult`, and `uri`/`metadata`/`createHandler` returning an `McpResourceReadResult`, respectively). All three throw if called once `strapi.ai.mcp` has left the `idle` status. #### Capability results Strapi owns every value a capability author constructs, so no result requires naming an MCP SDK type. All of them live in `@strapi/types` (`Modules.MCP`): | Type | What it describes | | ---------------------------- | ------------------------------------------------------------------------ | | `McpToolResult` | Tool content plus either successful structured data or an `isError` flag | | `McpPromptResult` | A prompt's description and messages | | `McpResourceReadResult` | What a resource read returns | | `McpResourceListingMetadata` | What a resource advertises at registration, before anyone reads it | | `McpContentBlock` | One block of tool or prompt content | `McpToolHandlerReturn` remains an alias of `McpToolResult`, so existing handler annotations keep compiling unchanged. `McpContentBlock` is a five-variant union: `text`, `image`, `audio`, `resource_link`, and `resource` (an embedded resource). Resource contents come in a `text` or a base64 `blob` form. Protocol extension metadata (`_meta`) and arbitrary extension fields on prompt and resource read results reach the client unchanged. `services/mcp/utils/toSdkMcpCapabilityResult.ts` is the outbound counterpart to the handler-context translator: it maps each Strapi-owned variant onto the protocol shape the SDK transports. A variant the protocol gains later becomes available to capability authors only once Strapi adds it to the owned contract, so an SDK change cannot alter Strapi's public API on its own. The SDK remains the final protocol validator, and structured-output validation is unchanged: a successful tool result must still satisfy the advertised output schema. #### Handler context Capability handlers retain Strapi's positional or `{ args, extra }` API. Strapi owns the `extra` handler-context contract and translates the underlying transport's request facts into it. Capability authors do not depend on an MCP SDK context type. Every member is optional because availability varies by request and transport: | Member | Meaning | | ------------- | -------------------------------------------------------- | | `signal` | Cancellation signal for the invocation | | `requestId` | JSON-RPC request identifier | | `sessionId` | Transport session identifier, when session state exists | | `authInfo` | Information about a token authenticated by the transport | | `_meta` | Complete request metadata | | `requestInfo` | Originating HTTP request information | Strapi preserves the established meaning of these names. `_meta` includes ordinary request metadata and reserved MCP protocol keys, even when the SDK handles those groups separately. `requestInfo` uses `{ headers, url? }`; `headers` is a plain mapping, so existing access such as `extra.requestInfo?.headers.authorization` continues to work. Server-initiated client messaging, SSE stream-closing controls, and experimental task facilities are not part of Strapi's handler context. There is no escape hatch to the SDK context. Code that reads one of these excluded fields fails the TypeScript check instead of receiving `undefined` at runtime. #### Fault isolation `createSafeCapabilityRegistration` (`services/mcp/utils/createSafeCapabilityRegistration.ts`) wraps every registration in three layers of error containment so one broken plugin tool cannot take down the MCP server or other capabilities: 1. **Factory errors** — if `createHandler(strapi)` throws while building the handler, a fallback handler is substituted that always returns an error result to the client. 2. **Runtime errors** — if the handler throws while executing, `wrapSafeHandler` catches it and returns a capability-shaped error result (`isError: true` for tools) instead of crashing the request. 3. **SDK registration errors** — if the MCP SDK itself rejects the registration call, the capability is replaced with a permanently-disabled no-op instead of aborting the whole registration loop. ### Built-in tools (core) | Tool | Access | Purpose | | ----- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `log` | `devModeOnly` | Writes a sanitized message to `strapi.log` at a given level; used for local debugging of MCP clients. Input is length-capped and stripped of control/ANSI characters and newlines before logging. | Source: `packages/core/core/src/services/mcp/tools/log.ts`. ### Content Manager tools Content-manager derives one MCP tool set per **displayed** content type at bootstrap time (`packages/core/content-manager/server/src/mcp/register-content-manager-mcp-tools.ts` → `deriveDisplayedContentTypeMcpToolDefinitions` in `derive-content-type-mcp-tools.ts`). Nothing is derived if MCP is disabled — the (relatively expensive) derivation is skipped entirely. #### Tool naming `slugifyUidForMcpToolName` (`packages/core/content-manager/server/src/mcp/utils.ts`) turns a content-type UID into a tool-name segment: - `api::article.article` → `article` - `api::writer.editor` → `writer_editor` (api name and content-type name differ, so both are kept) - `plugin::i18n.locale` → `plugin-i18n_locale` #### Collection types | Tool | Action | Notes | | ---------------------- | --------- | ------------------------------------------------------ | | `list_{slug}` | read | Paginated list with filters/sort/locale/status | | `get_{slug}` | read | Single document by `documentId` | | `create_{slug}` | create | Sets creator fields from the token's user | | `update_{slug}` | update | Updates the draft, or creates a locale version | | `delete_{slug}` | delete | Deletes a document or a locale version | | `publish_{slug}` | publish | Only if `draftAndPublish` is enabled | | `unpublish_{slug}` | unpublish | Only if `draftAndPublish`; optional `discardDraft` | | `discard_{slug}_draft` | discard | Only if `draftAndPublish`; restores published as draft | #### Single types | Tool | Action | Notes | | -------------------------------------------------------------- | ------------- | -------------------------------------------------------- | | `get_{slug}` | read | Reads the single document | | `write_{slug}` | create/update | Upserts the draft | | `delete_{slug}` | delete | Deletes / locale-deletes | | `publish_{slug}` / `unpublish_{slug}` / `discard_{slug}_draft` | — | Same semantics as collection types, if `draftAndPublish` | There are no content-manager MCP _prompts_ or _resources_, and no separate "describe schema" tool — schema introspection is implicit in each tool's Zod input/output schema, which an MCP client reads from `tools/list`. #### Output shaping Two extra steps run before a document is returned to an MCP client (`packages/core/content-manager/server/src/mcp/utils.ts` → `sanitizeFormatShape`; `sanitizers/shape-relations.ts`): 1. Standard permission-checker sanitization (same as REST). 2. Relations are reduced to **identity-only** references (`shapeRelationsForMcp`) rather than the fuller shapes REST responses use — relations are not meant to be traversed as full objects by an MCP client. ### Errors | Layer | Behavior | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Route-level (auth failure, method not allowed, uncaught error) | `sendJsonRpcError` (`utils/jsonRpcErrors.ts`) — a real JSON-RPC error body (`AUTHENTICATION_REQUIRED` -32000, `METHOD_NOT_ALLOWED` -32601, `INTERNAL_ERROR` -32603), never a plain-text/HTML error, so MCP clients can parse it. | | Capability handler throws | `wrapSafeHandler` catches it, logs, and returns a capability-shaped error result (`isError: true` for tools) instead of propagating. | | Registration-time errors | See [Fault isolation](#fault-isolation) above. | | Slow connect/handle | `withTimeout` (`utils/withTimeout.ts`) enforces `connectTimeoutMs` / `requestTimeoutMs`. | ### Telemetry MCP emits its own server-side telemetry events (`didStartMcpServer`, `didUseMcpServer`, `didNotAuthenticateMcpRequest`, `didNotHandleMcpRequest`, `didExecuteMcpCapability`, `didNotExecuteMcpCapability`) — see the [MCP section](./telemetry.md#mcp-core) of the server-side telemetry doc for event shapes and rate-limiting details. Implementation: `packages/core/core/src/services/mcp/metrics/`. ### Tests | Path | Covers | | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | `packages/core/core/src/services/mcp/__tests__/` | Route registration, disabled skip, service integration | | `packages/core/core/src/services/mcp/__tests__/authentication.test.ts` | Bearer parsing, missing/invalid/valid token | | `packages/core/core/src/services/mcp/handlers/__tests__/handlePost.test.ts` | Auth failure, timeouts, transport wiring | | `packages/core/core/src/services/mcp/internal/__tests__/` | Configuration, server factory, capability sync, registries | | `packages/core/content-manager/server/src/mcp/__tests__/derive-content-type-mcp-tools.test.ts` | Tool derivation per content type | | `packages/core/content-manager/server/src/mcp/sanitizers/__tests__/shape-relations.test.ts` | Relation shaping | | `tests/api/core/mcp/mcp-auth.test.api.ts` | End-to-end admin-token auth + `tools/list` | | `tests/api/core/mcp/mcp-content-manager-rbac.test.api.ts` | RBAC on derived tools | | `tests/api/core/mcp/mcp-content-manager-shaping.test.api.ts` | Relation shaping, end to end | ### Alternatives - If you need to expose data to a human through the admin panel, this is not the right layer — build a normal admin UI / API route instead. - If you need server-to-server automation without an AI client in the loop, use the [Document Service](../content-manager/index.md) or REST/GraphQL APIs directly rather than going through MCP. - If you only need request-scoped structured logging for debugging, prefer `strapi.log` directly; the `log` MCP tool exists specifically for MCP client-side debugging, not general application logging. --- ## Server-side telemetry Strapi collects anonymous usage data to understand feature adoption and improve the product. This document describes how **server-side** telemetry works in the monorepo — where events are sent, how they are named, and the patterns used to avoid flooding the analytics backend. For the public-facing overview of what is collected, see [Usage information](https://docs.strapi.io/developer-docs/latest/getting-started/usage-information.html). Admin panel (browser) events are a separate channel — see [Admin telemetry (frontend)](../admin/features/telemetry.md). MCP and other server-only features should use the **server-side** patterns below. --- ### Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Feature code (controller, service, bootstrap, CLI, cron) │ │ strapi.telemetry.send('didSomething', { ...payload }) │ └────────────────────────────┬────────────────────────────────────┘ │ ┌────────────────────────────▼────────────────────────────────────┐ │ packages/core/core/src/services/metrics/ │ │ index.ts → createTelemetryInstance, LIMITED_EVENTS │ │ sender.ts → POST body assembly + strapi.fetch() │ │ rate-limiter.ts → once-per-24h dedup for selected events │ │ middleware.ts → didReceiveRequest (REST, capped at 1000/d) │ └────────────────────────────┬────────────────────────────────────┘ │ ▼ https://analytics.strapi.io/api/v2/track (override: STRAPI_ANALYTICS_URL) ``` #### Registration Telemetry is wired through the `telemetry` provider: - `packages/core/core/src/providers/telemetry.ts` — registers `strapi.telemetry` on the Strapi container. - `packages/core/core/src/services/metrics/index.ts` — factory that exposes `send()`, `register()`, `isDisabled`. On `register()` (when telemetry is enabled): 1. A daily cron job sends a `ping` event (`0 0 12 * * *`). 2. REST request middleware is mounted to emit `didReceiveRequest` (see [Volume controls](#volume-controls)). #### Sending an event ```typescript strapi.telemetry.send('didCreateContentType', { eventProperties: { kind: 'collectionType' }, userProperties: { /* optional, per-user */ }, groupProperties: { /* optional, project-level */ }, }); ``` Implementation: `packages/core/core/src/services/metrics/sender.ts`. Each request is a single HTTP `POST` with JSON body: | Field | Purpose | | ----------------- | -------------------------------------------------------------- | | `event` | Event name (also sent as `X-Strapi-Event` header) | | `userId` | Hashed admin user when available (`generateAdminUserHash`) | | `installId` | Stable install identifier from project uuid + package.json | | `eventProperties` | Context specific to this occurrence | | `userProperties` | User/environment traits (OS, Node version, etc.) | | `groupProperties` | Project traits (Strapi version, DB client, EE plan, counts, …) | Default metadata is merged automatically (Docker/CI, TypeScript usage, `projectId`, `projectType`, EE subscription fields when present). Errors are swallowed — telemetry must never break application behaviour. Call sites often append `.catch(() => {})` or fire-and-forget without awaiting. #### When telemetry is disabled Telemetry is **off** when any of the following is true (`packages/core/core/src/services/metrics/index.ts`): - No project `uuid` in config (typical for fresh clones before first run). - `STRAPI_TELEMETRY_DISABLED` env is truthy (`1`, `true`, etc.). - `package.json` → `strapi.telemetryDisabled: true`. `strapi.telemetry.isDisabled` reflects this. Admin routes that expose telemetry data use the `admin::isTelemetryEnabled` policy. --- ### Event naming conventions Server-side events follow a consistent vocabulary: | Pattern | Meaning | Examples | | ------------------------------ | ---------------------------------------------------- | ---------------------------------------------------- | | `did*` | Completed action | `didCreateContentType`, `didInviteUser` | | `didNot*` | Failed or aborted action | `didNotCreateContentType`, `didNotOpenTab` | | `didCreateFirst*` | First occurrence in project lifetime | `didCreateFirstAdmin`, `didCreateFirstContentType` | | `didInitialize*` | Plugin/feature bootstrap snapshot | `didInitializeI18n`, `didInitializePluginUpload` | | `did*ProcessStart/Finish/Fail` | Long-running process lifecycle | `didDEITSProcessStart` (data export/import/transfer) | | `didSend*OnceAWeek` | Weekly aggregated snapshot | `didSendUploadPropertiesOnceAWeek` | | `ping` | Heartbeat | Daily cron | | `will*` | Intent (mostly **admin frontend**, rare server-side) | — | Casing is camelCase with no separators. Event names are flat strings — there is no namespacing prefix (e.g. no `mcp.didCallTool`). --- ### Payload conventions #### `eventProperties` Action-specific, per-event context. Examples in the codebase: - `kind` — content type kind on create - `model` — content type UID on first entry create - `url`, `success`, `statusCode` — REST middleware - `source`, `destination` — data transfer providers - `error` — failure messages Prefer **categories and counts** over identifiers when volume is a concern. Some existing events do include UIDs (`model`, `workflowId`); new features should avoid repeating user-specific or content-specific names when a category suffices. #### `userProperties` Traits of the acting user or anonymous environment: - Auto-attached on server: `environment`, `os`, `nodeVersion`, … - Explicit: `languagesInUse` (`didChangeInterfaceLanguage`) Some events are explicitly anonymous (empty `userId`), e.g. `didStartServer`, `didChangeInterfaceLanguage`. #### `groupProperties` Project/install-level state, often counts or configuration: - `numberOfAllContentTypes`, `numberOfComponents`, `database`, `plugins` — startup - `numberOfLocales` — i18n - `uploadProvider`, `privateProvider` — upload init - `numberOfActiveAdminUsers` — admin cron - Aggregated weekly metrics objects — upload / review workflows --- ### Where server events are emitted #### Core / startup | Event | Location | Notes | | ------------------------------ | ------------------------------------------------------------- | ---------------------------------------------------- | | `didStartServer` | `packages/core/core/src/Strapi.ts` → `sendStartupTelemetry()` | Fired once per process start; rich `groupProperties` | | `didOpenTab` / `didNotOpenTab` | `Strapi.ts` → `openAdmin()` | Dev auto-open browser only | | `ping` | metrics `register()` cron | Daily noon | | `didReceiveRequest` | `packages/core/core/src/services/metrics/middleware.ts` | REST API only; not MCP/GraphQL/admin | #### Admin (`packages/core/admin/server`) | Event | Trigger | | ----------------------------- | ------------------------------- | | `didCreateFirstAdmin` | First admin registration | | `didInviteUser` | User invitation | | `didUpdateRolePermissions` | Role permissions save | | `didChangeInterfaceLanguage` | Language preference change | | `didUpdateProjectInformation` | Bootstrap + daily midnight cron | | `didUpdateSSOSettings` | EE SSO settings (EE) | | `didWatchAnAuditLog` | EE audit log view (EE) | Metrics service: `packages/core/admin/server/src/services/metrics.ts`. #### Content-type builder | Event | Trigger | | ---------------------------------------------------- | ------------------------------------------------------ | | `didCreateFirstContentType` / `didCreateContentType` | Content type create (`_.isEmpty(strapi.apis)` → first) | | `didNotCreateContentType` | Create failure | | `didCreateFirstComponent` / `didCreateComponent` | Component create (registry size check) | #### Content manager | Event | Trigger | | -------------------------------- | ---------------------------------------------------------- | | `didCreateFirstContentTypeEntry` | First document in a collection type (`totalEntries === 0`) | | `didConfigureListView` | List view configuration save | Metrics service: `packages/core/content-manager/server/src/services/metrics.ts`. #### Content releases `didCreateContentRelease`, `didUpdateContentRelease`, `didDeleteContentRelease`, `didPublishContentRelease` — `packages/core/content-releases/server/src/services/release.ts`. #### Review workflows (EE) Per-action: `didCreateStage`, `didEditStage`, `didDeleteStage`, `didChangeEntryStage`, `didCreateWorkflow`, `didEditWorkflow`, `didEditAssignee`. Weekly aggregate: `didSendReviewWorkflowPropertiesOnceAWeek` via cron in `packages/core/review-workflows/server/src/services/metrics/weekly-metrics.ts`. #### Upload | Event | Trigger | | ----------------------------------------------------------------------- | ------------------------------------- | | `didInitializePluginUpload` | Plugin bootstrap (rate-limited daily) | | `didSaveMediaWithCaption` / `didSaveMediaWithAlternativeText` | Media save (rate-limited daily) | | `didEnableResponsiveDimensions` / `didDisableResponsiveDimensions` | Settings (rate-limited daily) | | `didUploadImage` | Image upload | | `didBulkDeleteMediaLibraryElements` / `didBulkMoveMediaLibraryElements` | Bulk operations | | `didSendUploadPropertiesOnceAWeek` | Weekly cron aggregate | Metrics wrapper: `packages/core/upload/server/src/services/metrics.ts` → `trackUsage()`. #### i18n | Event | Trigger | | ---------------------- | ---------------- | | `didInitializeI18n` | Plugin bootstrap | | `didUpdateI18nLocales` | Locale CRUD | #### CLI (in-process Strapi or standalone) | Event | Context | | ---------------------------------- | -------------------------------------------- | | `didDEITSProcessStart/Finish/Fail` | Export, import, transfer | | `didOptOutTelemetry` | `strapi telemetry:disable` | | `will/did*` project creation | `packages/cli/create-strapi-app` | | Cloud CLI events | Proxied via Cloud API (`packages/cli/cloud`) | --- #### MCP (core) | Event | Trigger | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `didStartMcpServer` | MCP server started with `server.mcp.enabled: true` (`eventProperties.path`; `groupProperties.numberOfTools` / `numberOfPrompts` / `numberOfResources`; rate-limited daily) | | `didUseMcpServer` | Authenticated MCP HTTP request (`handlePost.ts`; rate-limited daily) | | `didNotAuthenticateMcpRequest` | MCP request rejected before handling (`eventProperties.errorClass`: `missing_token` \| `invalid_token`; rate-limited daily) | | `didNotHandleMcpRequest` | Authenticated MCP request failed during connect/handle (`eventProperties.errorClass`: `timeout` \| `error`; rate-limited daily) | | `didExecuteMcpCapability` | Successful capability execution (`eventProperties.type`: `tool` \| `prompt` \| `resource`; `action`; `source`) | | `didNotExecuteMcpCapability` | Capability returned `isError: true` (same shape + `errorClass`: `execution_error`) | Implementation: `packages/core/core/src/services/mcp/metrics/`. Capability names are normalized to coarse `action` values — never content-type slugs. Request-level events use the core rate limiter; capability execute/not-execute events deduplicate **per type + action + source** in `metrics.ts`. Compare `didStartMcpServer` vs `didUseMcpServer` to see installs with MCP enabled but unused. Prompt/resource execution telemetry activates when normalization rules exist. --- ### Package-level metrics services Several packages wrap `strapi.telemetry.send` in a dedicated `metrics` service rather than calling telemetry directly from controllers: ``` packages/core/admin/server/src/services/metrics.ts packages/core/content-manager/server/src/services/metrics.ts packages/core/upload/server/src/services/metrics.ts packages/core/core/src/services/mcp/metrics/metrics.ts packages/plugins/i18n/server/src/services/metrics.ts packages/core/review-workflows/server/src/services/metrics/index.ts packages/core/admin/ee/server/src/services/metrics.ts ``` Pattern: thin service with `sendDid…()` helpers; controllers/services call the metrics service. Tests mock `strapi.telemetry.send` or the metrics service. --- ### Volume controls There is **no batching API** — each event is one HTTP request. Strapi uses several complementary strategies to limit volume: #### 1. Daily rate limiter (per event name) `packages/core/core/src/services/metrics/rate-limiter.ts` wraps the sender for events listed in `LIMITED_EVENTS`. For listed events: **at most one send per event name per 24 hours** (in-memory cache, resets on rolling 24h window). Important: deduplication is by **event name only**, not by `eventProperties`. If one event name covers multiple categories (e.g. `didExecuteMcpCapability` with different `type` / `action` values), apply per-category dedup **before** calling `strapi.telemetry.send` (see MCP `metrics.ts`) rather than adding many names to `LIMITED_EVENTS`. To rate-limit a new high-frequency event, either: - Add its exact event name to `LIMITED_EVENTS`, or - Use separate event names per category (each added to `LIMITED_EVENTS`), or - Dedupe locally by category before send (MCP tool actions), or - Use weekly aggregation (below). #### 2. REST middleware cap `didReceiveRequest` is capped at **1000 events per 24 hours** per process (`middleware.ts`). Counter resets on rolling 24h window. Only matches URLs under the REST API prefix; static assets and non-GET/PUT/POST/DELETE methods are skipped. MCP requests (`/mcp`) do **not** pass through this middleware. #### 3. Weekly aggregated cron jobs For noisy domains, compute metrics locally and send **one event per week**: - Upload: `didSendUploadPropertiesOnceAWeek` — folder depth stats, asset counts (`weekly-metrics.ts` + `strapi.store` for schedule jitter). - Review workflows: `didSendReviewWorkflowPropertiesOnceAWeek` — workflow/stage counts. Schedule is randomized per install (stored in `strapi.store`) to spread load. #### 4. First vs subsequent “First use” pattern without persistent store: - `didCreateFirstContentType` when `_.isEmpty(strapi.apis)` before create. - `didCreateFirstComponent` when component registry is empty. - `didCreateFirstAdmin` on registration flow. - `didCreateFirstContentTypeEntry` when `totalEntries === 0`. Subsequent actions use the non-`First` event (`didCreateContentType`, `didCreateComponent`, …). #### 5. Fire-and-forget / non-blocking Startup telemetry and many call sites do not `await` the send. Failures are ignored. Timeout on fetch defaults to 1s. --- ### Admin frontend telemetry (separate channel) See [Admin telemetry (frontend)](../admin/features/telemetry.md) for `useTracking()`, event types, plugin wrappers, and lifecycle events. New **server-side** features (like MCP) should use `strapi.telemetry.send`, not the React tracking hook. --- ### Adding a new server-side event — checklist 1. **Name** — `did*` past tense; use `didCreateFirst*` or `didInitialize*` when appropriate. 2. **Payload** — put action context in `eventProperties`; project-level counts in `groupProperties`. 3. **Privacy** — avoid PII, secrets, and high-cardinality identifiers (content type UIDs, document IDs) unless strictly necessary. 4. **Volume** — if the action can happen frequently, apply rate limiting or weekly aggregation from day one. 5. **Location** — prefer a `metrics` service in the owning package; call from controller/service after success. 6. **Disable guard** — `strapi.telemetry.send` no-ops when disabled; no extra check needed unless you perform expensive metric computation first. 7. **Tests** — mock `strapi.telemetry.send` in unit tests (see existing `__tests__/metrics.test.ts` files). --- ### API reference Public API: [Telemetry Service](/api/Telemetry). Key types: `packages/core/core/src/services/metrics/sender.ts` → `Payload`, `Sender`. --- ## Stream Lifecycle ## Stream Lifecycle in Data Transfer ### Overview The transfer process consists of five sequential stages. Each stage follows the same stream lifecycle pattern but handles different types of data: ```mermaid flowchart LR subgraph Process["Transfer Process"] subgraph S1["1.Schemas Stage"] direction TB DB["Database Structure"] CT["Content Type Configs"] CD["Component Definitions"] end subgraph S2["2.Entities Stage"] direction TB AC["Actual Content"] CoD["Component Data"] DZ["Dynamic Zone Data"] MM["Media Metadata"] end subgraph S3["3.Assets Stage"] direction TB UF["Upload Files"] FM["File Metadata"] FV["File Variants/Formats"] end subgraph S4["4.Links Stage"] direction TB CTR["Content Type Relations"] CR["Component Relations"] MR["Media Relations"] end subgraph S5["5.Configuration Stage"] direction TB SC["Strapi Configurations"] PS["Project Settings"] AC2["API Configurations"] end S1 ==> S2 S2 ==> S3 S3 ==> S4 S4 ==> S5 end %% Styling classDef stageClass fill:#f0f0ff,stroke:#d0d0ff,color:black classDef itemClass fill:white,stroke:#d0d0ff,color:black classDef processClass fill:#fafafa,stroke:#e0e0e0 class S1,S2,S3,S4,S5 stageClass class DB,CT,CD,AC,CoD,DZ,MM,UF,FM,FV,CTR,CR,MR,SC,PS,AC2 itemClass class Process processClass %% Link styling linkStyle 0,1,2,3 stroke:#d0d0ff,stroke-width:2px ``` 1. **Schemas Stage**: Content type definitions - Transfers database structure - Content type configurations - Component definitions 2. **Entities Stage**: Content without relations - Actual content data - Component data - Dynamic zone data - Media metadata (not files) 3. **Assets Stage**: Media files - Files from /uploads folder - File metadata - File variants/formats 4. **Links Stage**: Relations between entities - Content type relations - Component relations - Media relations 5. **Configuration Stage**: Project settings - Strapi configurations - Project-specific settings - API configurations ### Stream Lifecycle Phases #### 1. Stream Creation Phase During this phase, the Transfer Engine initializes and connects all necessary streams for the data transfer. ```mermaid sequenceDiagram participant TE as Transfer Engine participant SP as Source Provider participant TR as Transform Stream participant PT as Progress Tracker participant DP as Destination Provider rect rgb(200, 255, 200) Note over TE,DP: Stream Creation Phase TE->>SP: createReadStream() activate SP SP-->>TE: Source Stream TE->>TR: createTransformStream() activate TR TE->>PT: createProgressTracker() activate PT TE->>DP: createWriteStream() activate DP end ``` **Process:** 1. The Transfer Engine requests a read stream from the Source Provider 2. A transform stream is created to handle data modifications 3. A progress tracker stream is initialized for monitoring 4. A write stream is requested from the Destination Provider **Example Implementation:** ```typescript async transferEntities(): Promise { const stage: TransferStage = 'entities'; // 1. Create read stream from source const source = await this.sourceProvider.createEntitiesReadStream?.(); // 2. Create transform stream for data modifications const transform = this.#createStageTransformStream(stage); // 3. Create progress tracker const tracker = this.#progressTracker(stage); // 4. Create write stream from destination const destination = await this.destinationProvider.createEntitiesWriteStream?.(); // 5. Start the transfer await this.#transferStage({ stage, source, destination, transform, tracker }); } ``` #### 2. Stream Pipeline Phase This phase represents the active data transfer process where data flows through the connected streams. ```mermaid sequenceDiagram participant SP as Source Provider participant TR as Transform Stream participant PT as Progress Tracker participant DP as Destination Provider rect rgb(255, 240, 240) Note over SP,DP: Data Flow Through Pipeline SP->>TR: Read Data Note over SP,TR: 1. Read entities from database TR->>PT: Transform Data Note over TR,PT: 2. Filter specific content types PT->>DP: Track Note over PT,DP: 3. Track progress PT->>DP: Write Note over PT,DP: 4. write to destination end ``` **Process:** 1. Source Provider reads data (e.g., from database, file system) 2. Data passes through transform stream for any modifications 3. Progress tracker monitors and reports transfer progress 4. Destination Provider writes processed data **Key Aspects:** - Data flows in chunks through the pipeline - Each stream can process data asynchronously - Progress events are emitted for monitoring - Errors are caught and handled at each step **Example Flow:** ```typescript async #transferStage(options: { stage: TransferStage; source?: Readable; destination?: Writable; transform?: PassThrough; tracker?: PassThrough; }) { const { stage, source, destination, transform, tracker } = options; // Create pipeline let stream: Readable = source; // Add transform if provided if (transform) { stream = stream.pipe(transform); } // Add progress tracker if (tracker) { stream = stream.pipe(tracker); } // Connect to destination await new Promise((resolve, reject) => { this.#currentStream = stream .pipe(destination) .on('error', (error) => { this.#emitStageUpdate('error', stage); reject(error); }) .on('close', () => { this.#emitStageUpdate('finish', stage); resolve(); }); }); } ``` #### 3. Stream Closing Phase The closing phase handles the cleanup of resources and ensures proper completion of the transfer. ```mermaid sequenceDiagram participant TE as Transfer Engine participant SP as Source Provider participant TR as Transform Stream participant PT as Progress Tracker participant DP as Destination Provider rect rgb(240, 240, 255) Note over TE,DP: Cleanup Process SP--xTE: Close Source Note over SP,TE: 1. Close database connections TR--xTE: Close Transform Note over TR,TE: 2. Clear transform buffers PT--xTE: Close Tracker Note over PT,TE: 3. Final progress update DP--xTE: Close Destination Note over DP,TE: 4. Commit transactions end ``` **Process:** 1. Source stream indicates end of data 2. Transform stream processes final data chunks 3. Progress tracker records final metrics 4. Destination stream completes writing 5. Resources are cleaned up **Example Implementation:** ```typescript async close(): Promise { try { // Clean up providers const results = await Promise.allSettled([ this.sourceProvider.close?.(), this.destinationProvider.close?.() ]); // Handle any cleanup errors results.forEach((result) => { if (result.status === 'rejected') { this.reportError(result.reason, 'error'); } }); // Final cleanup this.#currentStream = undefined; this.progress.data = {}; } catch (error) { this.panic(error); } } ``` ### Progress Tracking The transfer engine maintains progress information for each stage: ```mermaid flowchart LR Start[stage::start] --> Progress[stage::progress] --> Finish[stage::finish] Progress -.-> Error[stage::error] Start -.-> Skip[stage::skip] style Start fill:#c8ffc8 style Progress fill:#f0f0ff style Finish fill:#c8ffc8 style Error fill:#ffebee style Skip fill:#e3f2fd ``` Progress events are emitted throughout the transfer: - `stage::start`: When a stage begins - `stage::progress`: During data transfer - `stage::finish`: When a stage completes - `stage::error`: If an error occurs - `stage::skip`: If a stage is skipped ```typescript interface TransferProgress { [stage: TransferStage]: { count: number; // Number of items processed bytes: number; // Amount of data transferred startTime: number; // When stage started endTime?: number; // When stage completed aggregates?: { // Optional grouped metrics [key: string]: { count: number; bytes: number; }; }; }; } ``` --- ## Transfer Engine The transfer engine manages the data transfer process by facilitating communication between a source provider and a destination provider. :::caution **Destructive operation**: Importing or transferring data **into a Strapi instance** (via the [local](../02-providers/04-local-strapi/02-destination.md) or [remote](../02-providers/05-remote-strapi/03-destination.md) Strapi destination providers) uses the **restore** strategy by default. After bootstrap, init, and the integrity check pass, the engine runs `beforeTransfer()` on the destination, which clears existing destination records and media matching the transfer scope before new data is written. Exporting to a file does not modify a Strapi instance. ::: ### Architecture Overview ```mermaid flowchart LR subgraph Ops["Stream Operations"] direction LR subgraph ReadOps["Read Operations"] direction TB SR[Schema Read] SR --> ER[Entity Read] ER --> AR[Asset Read] AR --> LR[Link Read] LR --> CR[Config Read] end subgraph Sources["Source Providers"] direction TB RSP[Remote Source Provider] RSP --> LSP[Local Source Provider] LSP --> FSP[File Source Provider] style Sources fill:#f0f0ff,stroke:#d0d0ff end subgraph Engine["Transfer Engine"] direction TB TE[Transfer Engine] TE --> TF[Transform Stream] TF --> PT[Progress Tracker] style Engine fill:#f0f0ff,stroke:#d0d0ff end subgraph Dests["Destination Providers"] direction TB RDP[Remote Destination Provider] RDP --> LDP[Local Destination Provider] LDP --> FDP[File Destination Provider] style Dests fill:#f0f0ff,stroke:#d0d0ff end subgraph WriteOps["Write Operations"] direction TB SW[Schema Write] SW --> EW[Entity Write] EW --> AW[Asset Write] AW --> LW[Link Write] LW --> CW[Config Write] end ReadOps --> Sources Sources --> Engine Engine --> Dests Dests --> WriteOps end %% Styling classDef provider fill:#fff0f0,stroke:#d0d0ff,color:black classDef engine fill:#fff0f0,stroke:#d0d0ff,color:black classDef stream fill:#c8ffc8,stroke:#a0ffa0,color:black classDef ops fill:#f8f8f8,stroke:#e8e8e8,color:black class RSP,LSP,FSP,RDP,LDP,FDP provider class TE,TF,PT engine class SR,ER,AR,LR,CR,SW,EW,AW,LW,CW stream class ReadOps,WriteOps ops style Ops fill:#f8f8f8,stroke:#e8e8e8 ``` ### Code location `packages/core/data-transfer/src/engine/index.ts` ### The transfer process ```mermaid sequenceDiagram participant Client participant TE as Transfer Engine participant SP as Source Provider participant DP as Destination Provider participant DH as Diff Handler rect rgb(230, 245, 230) Note over Client,DP: 1️⃣ Bootstrap & Initialize Phase Client->>+TE: Start Transfer TE->>SP: Bootstrap Note right of SP: Connect to files,
          databases, etc. TE->>DP: Bootstrap Note right of DP: Setup connections,
          prepare storage TE->>SP: Get Metadata SP-->>TE: Source Metadata TE->>DP: Get Metadata DP-->>TE: Destination Metadata end rect rgb(230, 240, 250) Note over Client,DP: 2️⃣ Integrity Check Phase TE->>TE: Version Strategy Check Note right of TE: Compare Strapi versions alt Version Mismatch TE->>Client: Version Validation Error end TE->>SP: Get Schemas SP-->>TE: Source Schemas TE->>DP: Get Schemas DP-->>TE: Destination Schemas TE->>TE: Schema Strategy Check alt Schema Differences TE->>+DH: onSchemaDiff Note right of DH: Resolve schema
          differences alt Resolved DH-->>-TE: Continue else Unresolved DH-->>TE: Schema Validation Error end end end rect rgb(240, 230, 230) Note over Client,DP: Before Transfer (Restore) TE->>DP: beforeTransfer Note right of DP: Clear destination data
          matching transfer scope end rect rgb(245, 235, 250) Note over Client,DP: 3️⃣ Data Transfer Phase Note over TE,DP: Stage 1: Schemas TE->>SP: Create Schemas ReadStream TE->>DP: Create Schemas WriteStream SP-->>DP: Stream Schema Data Note over TE,DP: Stage 2: Entities TE->>SP: Create Entities ReadStream TE->>DP: Create Entities WriteStream SP-->>DP: Stream Entity Data Note over TE,DP: Stage 3: Assets TE->>SP: Create Assets ReadStream TE->>DP: Create Assets WriteStream SP-->>DP: Stream Asset Files Note over TE,DP: Stage 4: Links TE->>SP: Create Links ReadStream TE->>DP: Create Links WriteStream SP-->>DP: Stream Relation Data Note over TE,DP: Stage 5: Configuration TE->>SP: Create Config ReadStream TE->>DP: Create Config WriteStream SP-->>DP: Stream Config Data end rect rgb(255, 240, 230) Note over Client,DP: 4️⃣ Cleanup Phase TE->>SP: Close SP-->>TE: Closed TE->>DP: Close DP-->>TE: Closed TE->>-Client: Transfer Complete end ``` #### Restore strategy and data loss When the destination is a Strapi instance and `strategy` is `restore` (the only supported conflict strategy today), the local destination provider prepares the instance during `beforeTransfer()` — **after** version and schema integrity checks pass and **before** any transfer stages run. In that step the provider may back up the local `uploads` folder, delete existing media files, and call `restore.deleteRecords()` to remove destination entities, internal models, and configuration records scoped by the `restore` options (see [Local Strapi destination restore options](../02-providers/04-local-strapi/02-destination.md)). The CLI builds those options from `--only` and `--exclude`; by default `admin::` content types are not cleared. This avoids ID conflicts during the `entities` and `links` stages, but it is **not a merge**: once the transfer completes successfully, pre-existing destination data in scope is replaced. If the transfer fails after `beforeTransfer()`, database changes run inside a transaction and may be rolled back; asset backup behavior is described in the destination provider documentation. **Supported workflow — content replace, keep destination config:** `--only content` (or `--exclude config`) replaces in-scope content on the destination while leaving core store and webhooks untouched. That is intentional restore scoping, not incremental sync. Incremental / merge sync remains a possible future product direction; designs should prefer opt-in strategies over changing restore defaults. #### `--only` / `--exclude` deletion vs transfer matrix Stage presets: | Preset | Stages | Typical payload | | --------- | --------------- | ---------------------------------------------------------------- | | `content` | entities, links | Content-type rows (incl. media library DB records) and relations | | `files` | assets | Upload binaries under `public/uploads` | | `config` | configuration | Core store and webhooks | Schemas always transfer with the CLI (independent of these presets). | Flags | Transferred | Pre-transfer wipe | Destination preserved | | ------------------------- | --------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------ | | (none) | content + files + config | All transferable entities, assets, config | Admin / ignored types only | | `--only content` | content | User content types only (not core store / webhooks) | Config + upload binaries | | `--only content,config` | content + config | All transferable entities + config | Upload binaries | | `--exclude config` | content + files | User content types only | Config (core store / webhooks) | | `--exclude files` | content + config | Entities + config (assets skipped) | Upload binaries; media library DB rows still transfer with content | | `--exclude media-library` | content (without upload types) + config | Entities except upload types + config | Upload binaries + `plugin::upload.file` / `folder` | Content-type filters (`--only-content-types` / `--exclude-content-types`) further narrow the entities/links stream and restore wipe. When upload types are out of scope, the files stage is skipped automatically. When `--only` omits one or more of `content` / `files` / `config`, the CLI prints a dim clarity line listing stages that are not transferred so destination data for those stages is obviously preserved. A transfer starts by bootstrapping and initializing itself and the providers. That is the stage where providers attempt to make any necessary connections to files, databases, websockets, etc. After that, the integrity check between the source and destination is run, which validates the requirements set by the chosen schemaStrategy and versionStrategy. Note: Schema differences during this stage can be resolved programmatically by adding an `onSchemaDiff` handler. However, be aware that this interface is likely to change to a more generic engine handler (such as `engine.on('schemaDiff', handler)`) before this feature is stable. Source providers can then validate each included stage with `validateStage`. This validation runs before `beforeTransfer`, the point where a restore can begin changing destination data. Archive and directory imports use it to check that every included upload has valid matching sidecar metadata. An invalid archive therefore fails before destination content or files are changed. Asset validation is skipped when the files stage is excluded. Once the integrity and stage validation checks have passed, the transfer begins by opening streams from the source to the destination one stage at a time. The following is a list of the stages in the order they are run: 1. schemas - content type schemas. Note: with all built-in Strapi destination providers, only the Strapi file provider makes use of this data 2. entities - all entities (including components, dynamic zones, and media data but not media files) _without their relations_ 3. assets - the files from the /uploads folder 4. links - the relations between entities 5. configuration - the Strapi project configuration data #### Media library vs upload binaries Strapi stores media in two parts that transfer on different stages: - **entities** stage — media library database records (`plugin::upload.file`, `plugin::upload.folder`) are transferred as part of `content`, along with other entities - **assets** stage — the binary files on disk under `public/uploads` The CLI `--exclude files` (or `--only` without `files`) skips only the **assets** stage. Media library records are still transferred unless you exclude them programmatically. To exclude upload content types from the entities stage, use entity and link transform filters: ```typescript const UPLOAD_TYPES = ['plugin::upload.file', 'plugin::upload.folder']; const options = { transforms: { entities: [ { filter(entity) { return !UPLOAD_TYPES.includes(entity.type); }, }, ], links: [ { filter(link) { return !UPLOAD_TYPES.includes(link.left.type) && !UPLOAD_TYPES.includes(link.right.type); }, }, ], }, }; ``` When skipping the assets stage, sync upload binaries separately — for example with `rsync -avz public/uploads/ user@host:/path/to/strapi/public/uploads/`. Once all stages have been completed, the transfer waits for all providers to close and then emits a finish event and the transfer completes. If the transfer throws after bootstrap but before provider closing begins, the engine rolls back the destination provider and then closes both providers before rethrowing. This matters for programmatic callers: the local providers disable database lifecycles on bootstrap and only re-enable them on close, so a failure that skipped the close would leave the caller's Strapi instance with lifecycles off. Failures during rollback or failure-path cleanup are reported as warning diagnostics so they cannot mask the error that caused the transfer to fail. #### Excluding content types from the CLI Export, import, and transfer accept `--exclude-content-types` and `--only-content-types` with comma-separated content type UIDs (for example `plugin::upload.file,plugin::upload.folder`). These filters apply to the **entities** and **links** stages: - Excluded types are omitted from the transfer stream and related links touching those types are skipped automatically. - On import/transfer restore, excluded types are not deleted on the destination before data is inserted. - `--only-content-types` limits transferred entities and links to the listed UIDs and scopes pre-transfer deletion to those types only. To skip upload binaries as well, combine with `--exclude files`, use `--exclude media-library`, or rely on automatic files-stage skipping when upload content types are out of scope. Programmatic transfers can use the same rules via entity and link transform filters. ##### Excluding the media library (issue #25008) To omit **all** media — upload binaries and media library database records — use `--exclude media-library`. That preset expands to `--exclude files` plus both upload content types (`plugin::upload.file`, `plugin::upload.folder`). When upload content types are not in transfer scope (via `--exclude-content-types`, `--only-content-types`, or the `media-library` preset), the **files** stage is skipped automatically so export and import stay consistent. `--exclude files` alone only skips the **assets** stage; media library DB records still transfer unless you also exclude their content types (or use `media-library`). ```bash # Export without media (matches the intent behind #25008) yarn strapi export --no-encrypt --exclude media-library # Import the same archive with matching filters yarn strapi import -f export_YYYYMMDDHHMMSS.tar --force --exclude media-library # Equivalent explicit form yarn strapi export --no-encrypt --exclude files \ --exclude-content-types plugin::upload.file,plugin::upload.folder ``` npm users: `npm run strapi export -- --no-encrypt --exclude media-library` ### Setting up the transfer engine A transfer engine object is created by using `createTransferEngine`, which accepts a [source provider](../02-providers/01-source-providers.md), a [destination provider](../02-providers/02-destination-providers.md), and an options object. Note: By default, a transfer engine will transfer ALL data, including admin data, api tokens, etc. Transform filters must be used if you wish to exclude, as seen in the example below. A helper function called `isIgnoredContentType` is available from the CLI utils and is used by the import, export, and transfer commands to exclude content types that should not be transferred. All content types in the `admin::` namespace are automatically excluded by prefix, along with certain explicitly listed types (such as content releases). If you need to transfer admin data, you must implement your own engine transforms without this filter. ```typescript const engine = createTransferEngine(source, destination, options); ``` #### Engine Options An example using every available option: ```typescript const options = { versionStrategy: 'ignore', // see versionStragy documentation schemaStrategy: 'strict', // see schemaStragey documentation exclude: [], // exclude these classifications of data; see CLI documentation of `--exclude` for list only: [], // transfer only these classifications of data; see CLI documentation of `--only` for list throttle: 0, // add a delay of this many milliseconds between each item transferred // the keys of `transforms` are the stage names for which they are run transforms: { links: [ { // exclude all relations to ignored content types filter(link) { return !isIgnoredContentType(link.left.type) && !isIgnoredContentType(link.right.type); }, }, // Note: map exists for links but is not recommended ], entities: [ { // exclude all ignored content types (admin:: prefix and explicit list) filter(entity) { return !isIgnoredContentType(entity.type); }, }, { map(entity) { // remove somePrivateField from privateThing entities if (entity.type === 'api::privateThing.privateThing') { entity.somePrivateField = undefined; } return entity; }, }, ], }, }; ``` ##### versionStrategy The following `versionStrategy` values may be used: `'ignore'` - allow transfer between any versions of Strapi `'exact'` - require an exact version match (including tags such as -alpha and -beta) `'major'` - require only the semver major version to match (but allow minor, patch, and tag to differ) `'minor'` - require only the semver major and minor versions to match (but allow patch to differ) `'patch'` - require only the semver major, minor, and patch version to match (but allow tag differences such as -alpha and -beta) The default `versionStrategy` used when one is not provided is `'ignore'`. ##### schemaStrategy The follow `schemaStrategy` values may be used: `'ignore'` - bypass schema validation (transfer will attempt to run but throw errors on incompatible data type inserts) `'strict'` - disallow mismatches that are expected to cause errors in the transfer, but allow certain non-data fields in the schema to differ `'exact'` - schema must be identical with no changes Note: The "strict" schema strategy is defined as "anything expected to cause errors in the transfer" and is the default method for the import, export, and transfer CLI commands. Therefore, the technical functionality will always be subject to change. If you need to find the definition for the current version of Strapi, see `packages/core/data-transfer/src/engine/validation/schemas/index.ts` The default `schemaStrategy` used when one is not provided is `'strict'`. ###### Handling Schema differences When a schema diff is discovered with a given schemaStrategy, an error is throw. However, before throwing the error the engine checks to see if there are any schema diff handlers set via `engine.onSchemaDiff(handler)` which allows errors to be bypassed (for example, by prompting the user if they wish to proceed). A diff handler is an optionally asynchronous middleware function that accepts a `context` and a `next` parameter. `context` is an object of type `SchemaDiffHandlerContext` ```typescript // type Diff can be found in /packages/core/data-transfer/src/utils/json.ts type SchemaDiffHandlerContext = { ignoredDiffs: Record; diffs: Record; source: ISourceProvider; destination: IDestinationProvider; }; ``` `next` is a function that is called, passing the modified `context` object, to proceed to the next middleware function. ```typescript const diffHandler = async (context, next) => { const ignoreThese = {}; // loop through the diffs Object.entries(context.diffs).forEach(([uid, diffs]) => { for (const [i, diff] of diffs) { // get the path of the diff in the schema const path = [uid].concat(diff.path).join('.'); // Allow a diff on the country schema displayName if (path === 'api::country.country.info.displayName') { if (!isArray(context.ignoredDiffs[uid])) { context.ignoredDiffs[uid] = []; } context.ignoredDiffs[uid][i] = diff; } } }); return next(context); }; engine.onSchemaDiff(diffHandler); ``` After all the schemaDiffHandler middlewares have been run, another diff is run between `context.ignoredDiffs` and `context.diffs` and any remaining diffs that have not been ignored are thrown as fatal errors and the engine will abort the transfer. #### Progress Tracking events The transfer engine allows tracking the progress of your transfer either directly with the engine.progress.data object, or with listeners using the engine.progress.stream PassThrough stream. The engine.progress.data object definition is of type TransferProgress. Here is an example that logs a message at the beginning and end of each stage, as well as a message after each item has been transferred ```typescript const progress = engine.progress.stream; progress.on(`stage::start`, ({ stage, data }) => { console.log(`${stage} has started at ${data[stage].startTime}`); }); progress.on('stage::finish', ({ stage, data }) => { console.log(`${stage} has finished at ${data[stage].endTime}`); }); progress.on('stage::progress', ({ stage, data }) => { console.log('Transferred ${data[stage].bytes} bytes / ${data[stage].count} entities'); }); ``` Note: There is currently no way for a source provider to give a "total" number of records expected to be transferred, but it is expected in a future update. The following events are available: `stage::start` - at the start of each stage `stage::finish` - at the end of each stage `stage::progress` - after each entity in that stage has been transferred `stage::skip` - when an entire stage is skipped (eg, when 'only' or 'exclude' are used) `stage::error` - when there is an error thrown during a stage `transfer::init` - at the very beginning of engine.transfer() `transfer::start` - after bootstrapping and initializing the providers, when the transfer is about to start `transfer::finish` - when the transfer has finished `transfer::error` - when there is an error thrown during the transfer #### Diagnostics events The engine includes a diagnostics reporter which can be used to listen for diagnostics information (debug messages, errors, etc). Here is an example for creating a diagnostics listener: ```typescript // listener function const diagnosticListener: DiagnosticListener = (data: GenericDiagnostic) => { // handle the diagnostics event, for example with custom logging }; // add a generic listener engine.diagnostics.onDiagnostic(diagnosticsListener); // add an error listener engine.diagnostics.on('error', diagnosticListener); // add a warning listener engine.diagnostics.on('warning', diagnosticListener); ``` To emit your own diagnostics event: ```typescript const event: ErrorDiagnostic = { kind: 'error', details: { message: 'Your diagnostics message' createdAt: new Date(), }, name: 'yourError', severity: 'fatal', error: new Error('your error message') } engine.diagnostics.report(event); ``` Here is an excerpt of the relevant types used in the previous examples: ```typescript // engine/diagnostic.ts // format of the data sent to the listener export type GenericDiagnostic = { kind: K; details: { message: string; createdAt: Date; } & T; }; export type DiagnosticKind = 'error' | 'warning' | 'info'; export type Diagnostic = ErrorDiagnostic | WarningDiagnostic | InfoDiagnostic; export type ErrorDiagnosticSeverity = 'fatal' | 'error' | 'silly'; export type ErrorDiagnostic = GenericDiagnostic< 'error', { name: string; severity: ErrorDiagnosticSeverity; error: Error; } >; export type WarningDiagnostic = GenericDiagnostic< 'warning', { origin?: string; } >; export type InfoDiagnostic = GenericDiagnostic< 'info', { params?: T; } >; ``` #### Transforms Transforms allow you to manipulate the data that is sent from the source before it reaches the destination. ### Filter (excluding data) Filters can be used to exclude data sent from the source before it is streamed to the destination. They are methods that accept an entity, link, schema, etc and return `true` to keep the entity and `false` to remove it. Here is an example that filters out all entities with an id higher than 100: ```typescript const options = { ...otherOptions, transforms: { entities: [ { // exclude all ignored content types (admin:: prefix and explicit list) filter(entity) { return !isIgnoredContentType(entity.type); }, }, { // exclude all entities with an id higher than 100 filter(entity) { return Number(entity.id) <= 100; }, }, ], links: [ { // exclude all relations to ignored content types filter(link) { return !isIgnoredContentType(link.left.type) && !isIgnoredContentType(link.right.type); }, }, { // remember to exclude links as well or else an error will be thrown when attempting to link an entity we filtered filter(entity) { return Number(link.left.id) <= 100 || Number(link.right.id) <= 100) }, }, ], }, }; ``` ### Map (modifying data) Maps can be used to modify data sent from the source before it is streamed to the destination. They are methods that accept an entity, link, schema, etc and return the modified version of the object. This can be used, for example, to sanitize data between environments. Here is an example that removes a field called `somePrivateField` from a content type `privateThing`. ```typescript const options = { ...otherOptions, transforms: { entities: [ { // exclude all ignored content types (admin:: prefix and explicit list) filter(entity) { return !isIgnoredContentType(entity.type); }, }, { map(entity) { // remove somePrivateField from privateThing entities if (entity.type === 'api::privateThing.privateThing') { entity.somePrivateField = undefined; } return entity; }, }, ], }, }; ``` By mapping schemas as well as entities, it's even possible (although complex!) to modify data structures between source and destination. ### Running a transfer Running a transfer simply involves calling the asynchronous engine.transfer() method. ```typescript const engine = createTransferEngine(source, destination, options); try { await engine.transfer(); } catch (e) { console.error('Something went wrong: ', e?.message); } ``` Be aware that engine.transfer() throws on any fatal errors it encounters. Note: The transfer engine (and the providers) current only support a single `engine.transfer()` and must be re-instantiated if intended to run multiple times. In the future it is expected to allow them to be used for multiple transfers in a row, but that usage is untested and will result in unpredictable behavior. --- ## Introduction(02-providers) ## Data Transfer Providers Data transfer providers are the interfaces for streaming data during a transfer. [Source providers](./01-source-providers.md) provide read streams for each stage in the transfer. [Destination providers](./02-destination-providers.md) provide write streams for each stage in the transfer. Strapi provides both source and destination providers for the following: - [Strapi file](./03-strapi-file/00-overview.md): a standardized file format designed for the transfer process - [Local Strapi](./04-local-strapi/00-overview.md): a connection to a local Strapi project which uses its configured database connection to manage data - [Remote Strapi](./05-remote-strapi/00-overview.md): a wrapper of local Strapi provider that adds a websocket interface to a running remote (network) instance of Strapi Each provider must provide the same interface for transferring data, but will usually include its own unique set of options to be passed in when initializing the provider. ### Creating your own providers To create your own providers, you must implement the interface(s) defined in `ISourceProvider` and `IDestinationProvider` found in `packages/core/data-transfer/types/providers.d.ts`. It is not necessary to create both a source and destination provider, only the part necessary for your use. For examples, see the existing providers such as the local Strapi provider. ### Asset Transfers Currently, all of the data-transfer providers only handle local media assets (the `/upload` folder). Provider media is currently in development. Therefore, everything related to asset transfers -- including Strapi file structure, restore strategy, and rollback for assets -- is currently treated as `unstable` and likely to change in the near future. --- ## Source Providers ### Source provider structure A source provider must implement the interface ISourceProvider found in `packages/core/data-transfer/types/providers.d.ts`. In short, it provides a set of `create{_stage_}ReadStream()` methods for each stage that provide a Readable stream, which will retrieve its data (ideally from its own stream) and then perform a `stream.write(entity)` for each entity, link (relation), asset (file), configuration entity, or content type schema depending on the stage. When each stage's stream has finished sending all the data, the stream must be closed before the transfer engine will continue to the next stage. --- ## Destination Providers ### Destination provider structure A destination provider must implement the interface IDestinationProvider found in `packages/core/data-transfer/types/providers.d.ts`. In short, it provides a set of `create{_stage_}WriteStream()` methods for each stage that provide a Writable stream, which will be passed each entity, link (relation), asset (file), configuration entity, or content type schema (depending on the stage) piped from the Readable source provider stream. --- ## Overview ## Strapi Data File Providers Strapi data file providers transfer data to or from a [Strapi Data File](./01-file-structure.md). The files are optionally compressed and/or encrypted using a given key (password). --- ## Strapi File Structure The Strapi file providers expect a .tar file (optionally compressed with gzip and/or encrypted with 'aes-128-ecb') that internally uses POSIX style file paths with the following structure: ``` ./ configuration entities links metadata.json schemas ./configuration: configuration_00001.jsonl ./entities: entities_00001.jsonl ./links: links_00001.jsonl ./schemas: schemas_00001.jsonl ``` ### metadata.json This file provides metadata about the original source of the data. At minimum, it should include a createdAt timestamp and the version of Strapi that the file was created with (for compatibility checks). ```json { "createdAt": "2023-06-26T07:31:20.062Z", "strapi": { "version": "4.11.2" } } ``` ### A directory for each stage of data There should also be a directory for each stage of data that includes sequentially numbered JSON Lines (.jsonl) files The files are named in the format: `{stage}\{stage}_{5-digit sequence number}.jsonl` Any number of files may be provided for each stage, as long as the sequence numbers are in order. That is, after first reading 00001, the file source provider will attempt to read file 00002 and if it is not found, it will consider the stage complete. #### JSONL files [JSON Lines](https://jsonlines.org/) files are essentially JSON files, except that newline characters are used to delimit the JSON objects. This allows the provider to read in a single line at a time, rather than loading the entire file into memory, minimizing RAM usage during a transfer and allowing files containing any amount of data. --- ## Source ## Strapi File Source Provider This provider will open and read a Strapi Data File as a data source. ### Provider Options The accepted options are defined in `ILocalFileSourceProviderOptions`. ```typescript file: { path: string; // the file to load }; encryption: { enabled: boolean; // if the file is encrypted (and should be decrypted) key?: string; // the key to decrypt the file }; compression: { enabled: boolean; // if the file is compressed (and should be decompressed) }; ``` Note: When the Strapi CLI attempts to import a file, the options for compression and encryption are set based on the extension of the file being loaded, eg a file with the .gz extension will have the "compress" option set, and a file that includes the .enc extension will have the "encrypt" option set. When using the transfer engine programmatically, you may make the determination whether the file being loaded should be decrypted or compressed by setting those options. --- ## Destination ## Strapi File Destination Provider This provider will output a Strapi Data File. Note: this destination provider does not provide a schema or metadata, and will therefore never report a schema match error or version validation error ### Provider Options The accepted options are defined in `ILocalFileDestinationProviderOptions`. ```typescript encryption: { enabled: boolean; // if the file should be encrypted key?: string; // the key to use when encryption.enabled is true }; compression: { enabled: boolean; // if the file should be compressed with gzip }; file: { path: string; // the filename to create maxSize?: number; // the max size of a single backup file maxSizeJsonl?: number; // the max lines of each jsonl file before creating the next file }; ``` --- ## Overview(04-local-strapi) ## Local Strapi Providers The local Strapi provider allows using the local Strapi instance (the same project that the data transfer engine is being run from) as a data source. Creating a local Strapi data provider requires passing in an initialized `strapi` server object to interact with that server's Entity Service and Query Engine to manage the data. Therefore if the local Strapi project cannot be started (due to errors), the providers cannot be used. **Important**: When a transfer completes, the `strapi` object passed in is shut down automatically based on the `autoDestroy` option. If you are running a transfer via an external script, it is recommended to use `autoDestroy: true` to ensure it is shut down properly, but if you are running a transfer within a currently running Strapi instance you should set `autoDestroy: false` or your Strapi instance will be shut down at the end of the transfer. --- ## Source(04-local-strapi) ## Local Strapi Source Provider This provider will retrieve data from an initialized `strapi` instance using its Entity Service and Query Engine. ### Provider Options The accepted options are defined in `ILocalFileSourceProviderOptions`. ```typescript getStrapi(): Strapi.Strapi | Promise; // return an initialized instance of Strapi autoDestroy?: boolean; // shut down the instance returned by getStrapi() at the end of the transfer ``` --- ## Destination(04-local-strapi) ## Local Strapi Destination Provider This provider will insert data into an initialized `strapi` instance using its Entity Service and Query Engine. ### Provider Options The accepted options are defined in `ILocalFileSourceProviderOptions`. ```typescript getStrapi(): Strapi.Strapi | Promise; // return an initialized instance of Strapi autoDestroy?: boolean; // shut down the instance returned by getStrapi() at the end of the transfer restore?: restore.IRestoreOptions; // the options to use when strategy is 'restore' strategy: 'restore'; // conflict management strategy; only the restore strategy is available at this time ``` `strategy` defines the conflict management strategy used. Currently, only `"restore"` is available as an option. #### Restore A conflict management strategy of "restore" deletes all existing Strapi data before a transfer to avoid any conflicts. The following restore options are available: ```typecript export interface IRestoreOptions { assets?: boolean; // delete media library files before transfer configuration?: { webhook?: boolean; // delete webhooks before transfer coreStore?: boolean; // delete core store before transfer }; entities?: { include?: string[]; // only delete these stage entities before transfer exclude?: string[]; // exclude these stage entities from deletion filters?: ((contentType: ContentTypeSchema) => boolean)[]; // custom filters to exclude a content type from deletion params?: { [uid: string]: unknown }; // params object passed to deleteMany before transfer for custom deletions }; } ``` #### Rollbacks This local Strapi destination provider automatically provides a rollback mechanism on error. For Strapi data, that is done with a database transaction wrapped around the restore and the insertion of data and committing on success and rolling back on failure. For Strapi assets (ie, the media library files) this is done by attempting to temporarily move the existing assets to a backup directory to `uploads_backup_{timestamp}`, and then deleting it on success, or deleting the failed import files and putting the backup back into place on failure. In some cases of failure, it may be impossible to move the backup files back into place, so you will need to manually restore the backup assets files. Note: Because of the need for write access, environments without filesystem permissions to move the assets folder (common for virtual environments where /uploads is mounted as a read-only drive) will be unable to include assets in a transfer and the asset stage must be excluded in order to run the transfer. --- ## Overview(05-remote-strapi) ## Remote Strapi Providers Remote Strapi providers connect to an instance of Strapi over a network using a websocket. Internally, the remote Strapi providers map websocket requests to a local Strapi provider of the instance it is running in. In order to use remote transfer providers, the remote Strapi server must have a value for `transfer.token.salt` configured in `config/admin.js` and the remote transfer feature must not be disabled. ### Disabling Remote Transfers The remote transfer feature of a server can be completely disabled by setting the server config value: ```javascript // in config/server.js { transfer: { remote: { enabled: false; } } // ...the rest of your server config } ``` --- ## Websocket ## WebSocket ### Websocket Messages / Dispatcher The remote websocket server only accepts specific websocket messages which we call transfer commands. These commands must also be sent in a specific order, and an error messages will be returned if an unexpected message is received by the server. A message dispatcher object should be created to send messages to the server. See `packages/core/data-transfer/src/strapi/providers/utils.ts` for more information on the dispatcher. The dispatcher includes #### dispatchCommand Accepts "commands" used for opening and closing a transfer. Allows the following `command` values: - `init`: for initializing a connection. Returns a transferID that must be sent with all future messages in this transfer - `end`: for ending a connection #### dispatchTransferStep Used for switching between stages of a transfer and streaming the actual data of a transfer. Accepts the following `action` values: - `start`: sent with a `step` value for the name of the step/stage - any number of `stream`: sent with a `step` value and the `data` being sent (ie, an array of entities) - `end`: sent with a `step` value for the step being ended #### dispatchTransferAction Used for triggering 'actions' on the server equivalent to the local providers. - `bootstrap` - `getMetadata` - `beforeTransfer` - `getSchemas` - `rollback` (destination only) - `close`: for completing a transfer (but doesn't close the connection) See `packages/core/data-transfer/dist/strapi/remote/handlers/pull.d.ts` and `packages/core/data-transfer/dist/strapi/remote/handlers/push.d.ts` for complete and precise definitions of the messages that must be sent. ### Transfer Lifecycle ```mermaid sequenceDiagram participant Client participant Dispatcher participant WebSocket participant Server Note over Client,Server: Connection Phase Client->>WebSocket: Create WebSocket Connection Note right of WebSocket: /admin/transfer/runner/pull or push Note right of WebSocket: Authorization: Bearer Client->>Dispatcher: Create Message Dispatcher Note over Client,Server: Initialization Phase Dispatcher->>Server: dispatchCommand("init") Server->>Dispatcher: Returns transferID Note right of Dispatcher: All future messages include transferID Note over Client,Server: Transfer Actions Phase Dispatcher->>Server: dispatchTransferAction("bootstrap") Server->>Dispatcher: Response Dispatcher->>Server: dispatchTransferAction("getMetadata") Server->>Dispatcher: Response Dispatcher->>Server: dispatchTransferAction("beforeTransfer") Server->>Dispatcher: Response Dispatcher->>Server: dispatchTransferAction("getSchemas") Server->>Dispatcher: Response Note over Client,Server: Transfer Steps Phase loop Each Step (schemas, entities, assets, links, config) Dispatcher->>Server: dispatchTransferStep(step, "start") Server->>Dispatcher: Response loop Data Streaming Dispatcher->>Server: dispatchTransferStep(step, "stream", data) alt Success Server->>Dispatcher: Response else Timeout Note over Dispatcher: Wait retryMessageTimeout Dispatcher->>Server: Retry Message end end Dispatcher->>Server: dispatchTransferStep(step, "end") Server->>Dispatcher: Response end Note over Client,Server: Closing Phase Dispatcher->>Server: dispatchTransferAction("close") Server->>Dispatcher: Response Dispatcher->>Server: dispatchCommand("end") Server->>Dispatcher: Response Client->>WebSocket: Close Connection ``` #### 1. Connection Phase When the data transfer feature is enabled for a Strapi server (an `admin.transfer.token.salt` config value has been set and `server.transfer.remote.enabled` is not set to false), Strapi will create websocket servers available on the routes `/admin/transfer/runner/pull` and `/admin/transfer/runner/push`. **Create WebSocket Connection** - Opening a websocket connection on those routes requires a valid transfer token as a bearer token in the Authorization header.: ``` Authorization: Bearer ``` - Server validates the token and establishes the connection Please see the `bootstrap()` method of the remote providers for an example of how to make the initial connection to the Strapi websocket. **Event Listener Attachment** Immediately after WebSocket creation, the following listeners are attached: - `'open'`: Handles successful connection establishment - `'close'`: Manages connection termination - `'error'`: Handles connection and transmission errors - `'message'`: Processes incoming messages from server #### 2. Initialization Phase Client sends initial command to establish transfer, server responds with unique transferID. ```javascript const transferID = await dispatcher.dispatchCommand('init'); // All subsequent messages must include this transferID ``` All subsequent messages must include this transferID #### 3. Transfer Actions Phase Sequential actions executed through `dispatchTransferAction`: 1. `bootstrap`: Initializes transfer environment 2. `getMetadata`: Retrieves transfer metadata 3. `beforeTransfer`: Performs pre-transfer preparations 4. `getSchemas`: Retrieves content type schemas, used for validation between source and destination #### 4. Transfer Steps Phase The main phase where actual data transfer occurs, processing different types of data (schemas, entities, assets, links, configuration) in sequence: **Stage Start** ```javascript dispatchTransferStep(action: "start", step) ``` **Data Streaming** ```javascript dispatchTransferStep(action:"stream", step, data) ``` **Stage Completion** ```javascript dispatchTransferStep(action:"end", step) ``` **Retry Mechanism** During data transfer: - If server response not received within `retryMessageTimeout` - System attempts retry up to `retryMessageMaxRetries` times - Automatic retry on timeout - Transfer aborts if max retries exceeded #### 5.Closing Phase **Cleanup Actions** 1. Send close action: ```javascript dispatchTransferAction('close'); ``` 2. Send end command: ```javascript dispatchCommand({ command: 'end', params: { transferID } }); ``` **Connection Termination** 1. Remove event listeners in reverse order: - Remove `message` listener - Remove `error` listener - Remove `open` listener - Remove `close` listener 2. Close WebSocket connection ### Message Timeouts and Retries ```mermaid stateDiagram-v2 [*] --> Init Init --> Ready: Connection Established Ready --> Dispatching: Send Message Dispatching --> WaitingResponse: Message Sent WaitingResponse --> Ready: Response Received WaitingResponse --> Retrying: Timeout Retrying --> WaitingResponse: Retry Attempt Retrying --> Error: Max Retries Exceeded Error --> [*] note right of WaitingResponse Configurable timeout via retryMessageTimeout end note note right of Retrying Limited by retryMessageMaxRetries end note ``` Because the transfer relies on a message->response protocol, if the websocket server is unable to send a reply, for example due to network instability, the connection would halt. For this reason, each provider's options includes `retryMessageOptions` which attempt to resend a message after a given timeout is reached and a max retry option to abort the transfer after a given number of failed retry attempts. --- ## Source(05-remote-strapi) ## Strapi Remote Source Provider The Strapi remote source provider connects to a remote Strapi websocket server and sends messages to move between stages and pull data. ### Provider Options The remote source provider accepts `url`, `auth`, and `retryMessageOptions` described below. ```typescript interface ITransferTokenAuth { type: 'token'; token: string; } export interface IRemoteStrapiDestinationProviderOptions extends Pick { url: URL; auth?: ITransferTokenAuth; retryMessageOptions?: { retryMessageTimeout: number; // milliseconds to wait for a response from a message retryMessageMaxRetries: number; // max number of retries for a message before aborting transfer }; } ``` Note: `url` must include the protocol `https` or `http` which will then be converted to `wss` or `ws` to make the connection. A secure connection is strongly recommended, especially given the high access level that the transfer token provides. --- ## Destination(05-remote-strapi) ## Strapi Remote Destination Provider The Strapi remote destination provider connects to a remote Strapi websocket server and sends messages to move between stages and push data. ### Provider Options The remote destination provider accepts the same `restore` and `strategy` options from local Strapi destination provider, plus `url`, `auth`, and `retryMessageOptions` described below. ```typescript interface ITransferTokenAuth { type: 'token'; // the name of the auth strategy token: string; // the transfer token } export interface IRemoteStrapiDestinationProviderOptions extends Pick { url: URL; // the url of the remote Strapi admin auth?: ITransferTokenAuth; retryMessageOptions?: { retryMessageTimeout: number; // milliseconds to wait for a response from a message retryMessageMaxRetries: number; // max number of retries for a message before aborting transfer }; } ``` Note: `url` must include the protocol `https` or `http` which will then be converted to `wss` or `ws` to make the connection. A secure connection is strongly recommended, especially given the high access level that the transfer token provides. --- ## @strapi/data-transfer ### Purpose `@strapi/data-transfer` moves the data of a Strapi application from one place to another. A transfer engine reads from a source provider and writes to a destination provider, stage by stage, as streams. This gives the `export`, `import` and `transfer` commands. It runs on the server and in the CLI process. The `@strapi/strapi` package uses it for the CLI commands. The `@strapi/admin` server uses its remote handlers for the push and pull routes and the transfer tokens. Users of the commands rely on it to back up, restore and copy content between environments. ### Key concepts #### Transfer engine `TransferEngine` in [`src/engine`](https://github.com/strapi/strapi/tree/develop/packages/core/data-transfer/src/engine) takes a source and a destination provider and runs the transfer. The order is: bootstrap and init the providers, run the integrity check, validate the stages, run `beforeTransfer` on the providers, transfer the stages, then close. If an error occurs before close, the engine calls `rollback` on the destination. Read [Transfer engine](./01-engine/index.md) and [Stream lifecycle](./01-engine/02-stream-lifecycle.md). #### Stages A transfer has five stages: `schemas`, `entities`, `assets`, `links` and `configuration`. The engine runs them in that order. The `only` and `exclude` options select stages through the presets `content`, `files` and `config`. A provider offers one stream factory per stage, for example `createEntitiesReadStream`. #### Providers A provider implements `ISourceProvider` or `IDestinationProvider` from [`src/types/providers.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/data-transfer/src/types/providers.ts). Both extend `IProvider` with `bootstrap`, `close`, `getMetadata`, `getSchemas` and `beforeTransfer`. Read the [providers overview](./02-providers/00-overview.md), [source providers](./02-providers/01-source-providers.md) and [destination providers](./02-providers/02-destination-providers.md). #### Provider families The package ships four families, each with a source and a destination: - Strapi file: an archive of JSON Lines files, with optional gzip compression and encryption. See [Strapi file](./02-providers/03-strapi-file/00-overview.md). - Directory: the same layout, unpacked in a folder. The CLI `export` command uses it with `--format dir`. The `import` command uses it when the path is a directory. Code: [`src/directory`](https://github.com/strapi/strapi/tree/develop/packages/core/data-transfer/src/directory). - Local Strapi: reads and writes the database of the running application. See [Local Strapi](./02-providers/04-local-strapi/00-overview.md). - Remote Strapi: wraps the local providers behind a WebSocket connection to another instance. See [Remote Strapi](./02-providers/05-remote-strapi/00-overview.md) and [WebSocket protocol](./02-providers/05-remote-strapi/01-websocket.md). #### Restore strategy The local destination provider has one conflict strategy, `restore`. Before it writes, `beforeTransfer` deletes the existing records and media files that match the transfer scope. A transfer into a Strapi instance is destructive. Read [Local Strapi destination](./02-providers/04-local-strapi/02-destination.md). #### Integrity checks Before data flows, the engine compares the Strapi versions and the schemas of both sides. The `versionStrategy` and `schemaStrategy` options decide what counts as a mismatch. The engine defaults are `ignore` for versions and `strict` for schemas. An `onSchemaDiff` handler can resolve a difference. Code: [`src/engine/validation`](https://github.com/strapi/strapi/tree/develop/packages/core/data-transfer/src/engine/validation). #### Transfer policy [`src/strapi/transfer-policy.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/data-transfer/src/strapi/transfer-policy.ts) lists content types that a transfer skips. It skips all `admin::` types and the two `content-releases` types (`release` and `release-action`). #### Remote handlers `src/strapi/remote/handlers` builds the push and pull controllers with `createPushController` and `createPullController`. The `@strapi/admin` server mounts them on the routes `/transfer/runner/push` and `/transfer/runner/pull`. A transfer token authorizes each call. ### Related - [Package map](../../../architecture/01-package-map.mdx): how the CLI, admin and data-transfer packages depend on each other. - [Authentication](../../../architecture/08-authentication.md): the transfer token strategy of the admin server. - [HTTP request path](../../../architecture/06-http-request-path.md): how the remote routes reach a controller. - [`@strapi/strapi`](../strapi/index.md): the `export`, `import` and `transfer` CLI commands. - [`@strapi/admin`](../admin/index.md): the runner routes and the transfer tokens. - [`@strapi/database`](../database/index.md): the local providers read and write through `strapi.db` and disable its lifecycles during a transfer. --- ## Polymorphic relations Strapi’s polymorphic relation kinds are **`morphToOne`**, **`morphToMany`**, and the inverse sides **`morphOne`** and **`morphMany`**. Public schema shape: `packages/core/types/src/schema/attribute/definitions/relation.ts`. Database relation shapes: `packages/core/database/src/types/index.ts`. Metadata (how they map to columns / join tables): `packages/core/database/src/metadata/relations.ts`. | Kind | Storage (summary) | | ------------------------ | ----------------------------------------------------------------------------------------------------- | | `morphToOne` | Inline columns on the **owner** row: target id + target type. | | `morphToMany` | **Join table**: owner id, polymorphic target id + type, ordering. | | `morphOne` / `morphMany` | **Inverse**; data lives on the `morphToOne` / `morphToMany` side; resolved via `morphBy` in populate. | ### Database layer **Writes** — `packages/core/database/src/entity-manager/index.ts`: `processData` maps `morphToOne` input (`id` + `typeField`, default `__type`) into inline columns; `attachRelations` / `updateRelations` handle `morphToMany` through join tables. `packages/core/database/src/entity-manager/morph-relations.ts` encodes join rows and related cleanup (e.g. when `morphOne` enforces a single inverse). **Reads (populate)** — `packages/core/database/src/query/helpers/populate/apply.ts` dispatches on `attribute.relation`. `morphToOne` and `morphToMany` load target rows per type; inverse `morphOne` / `morphMany` traverse the inverse. Polymorphic targets use `populate.on` in the query for per-UID options. The populated object for a morph item sets the type discriminator **after** target row fields so the target content-type UID (in `typeField`, default `__type`) is always authoritative if a name collides. ### Document service **Deep populate** — `packages/core/core/src/services/document-service/utils/populate.ts` (`getDeepPopulate`) must include all DB-backed relation attributes (including morph kinds, excluding `unstable_virtual` ones) so flows that copy full entry trees—**publish**, **discardDraft**, **clone** in `repository.ts`—load related data. Those call sites pass `relationalFields` (e.g. `documentId` / `locale` or `id`) as needed for the copy. **Relation transforms** — `packages/core/core/src/services/document-service/transform/relations/utils/map-relation.ts` (`traverseEntityRelations`) does **not** visit `morphToOne` like join-based relations: inline morph columns are applied in the entity manager’s `processData`, not through the `documentId` → entity `id` map used for other relations. Other polymorphic **join** relations still go through the normal extract/transform path where applicable. `publish` / `discard` entry points: `packages/core/core/src/services/document-service/entries.ts` use `transformData` with the above traversal. ### Validation, API surface, and tests **Query / validation** — Polymorphic helpers and populate rules live under `packages/core/utils/src` (e.g. `relations.ts`, query-populate and query-param conversion). Some sort/filter combinations are limited for morph relations in the relevant validation paths. **Plugins** — GraphQL helpers: `packages/plugins/graphql/server/src/services/content-api/register-functions/polymorphic.ts` and related type builders. **Tests** — REST and broader relation behavior: `tests/api/core/strapi/api/relations.test.api.ts`, `tests/api/core/strapi/api/populate/`. Document service flows with morph relations: `tests/api/core/strapi/document-service/relations/polymorphic.test.api.ts`. Unit coverage includes database populate (`query/helpers/populate` tests) and document-service populate / relation utilities where relevant. --- ## Reordering Strapi allows you to reorder a relation list. This reordering feature is available in the Content Manager and the API. ### Code location `packages/core/database/lib/entity-manager/relations-orderer.js` ### How is the order stored in DB? - We store the order value of the relation in an `order` field. - For bidirectional relations, we store the order value of the other side in an `inverse_order` field. We store order values for all type of relations, except for: - Polymorphic relations (too complicated to implement). - One to one relations (as there is only one relation per pair) #### Many to many (Addresses <-> Categories) - `category_order` is the order value of the categories relations in an address entity. - `address_order` is the order value of the addresses relations in a category entity. #### One to one (Kitchensinks <-> Tags) - there is no `order` fields as there is only one relation per pair. #### One way relation (Restaurants <-> Categories) Where a restaurant has many categories: - `category_order` is the order value of the categories relations in a restaurant entity. - There is no `restaurant_order` as it is a one way relation. ### How to reorder relations in the DB layer See more on [Strapi Docs](https://docs.strapi.io/dev-docs/api/rest/relations#connect) The database layer should receive a payload shown below: ```js category: { connect: [ { id: 6, position: { after: 1} }, // It should be after relation id=1 { id: 8, position: { end: true }}, // It should be at the end ], disconnect: [ { id: 4 } ] } ``` ### How does relations reordering work? We use fractional indexing. This means that we use decimal numbers to order the relations. See the following diagrams below for a more detailed understanding. #### Simple example #### Complex example #### Algorithm steps From the `connect` array: - For every element, **load relations by id**, **from fields `after` or `before`**. - Start computing based on the `after` and `before` relations: - **Initialize** with after/before relations (**step 1**). Let's call these ones **init relations.** - **Apply the updates** from the `connect` array, **sequentially**. - If the update is of type `before`: - Place the element with the given `id` **before** the specified element in the list. - If the specified element is an `init relation`, place the element in between that relation and the one before it. - To determine the order value, **order = beforeRelation.order - 0.5**. This ensures the element is placed before the `before` relation and after the one before it. - Else **order = beforeRelation.order** - If the update is of type `after`: - Place the element with the given `id` **after** the specified element in the list. - If the specified element is an `init relation`, place the element in between that relation and the one after it. - To determine the order value, **order = beforeRelation.order + 0.5**. This ensures the element is placed before the `after` relation and before the one after it. - Else **order = beforeRelation.order** - If the update is of type `end`: - Place at the **end** - If placing after an init relation: **order = lastRelation.order + 0.5** - Else **order = lastRelation.order** - If the update is of type `start`: -Place at the **start** - **order = 0.5** - `before/after`: If the **id does not exist in the current array**, **throw an error** - If an **id** was **already in this array, remove the previous one** - **Grouping by the order value**, and ignoring init relations - Recalculate order values for each group, so there are no repeated numbers & they keep the same order. - Example : `[ {id: 5 , order: 1.5}, {id: 3, order: 1.5 } ]` → `[ {id: 5 , order: 1.33}, {id: 3, order: 1.66 } ]` - **Insert values in the database** - **Update database order based on their order position.** (using ROW_NUMBER() clause) From the disconnect array: - Delete the relations from the database. - Reorder the remaining elements in the database based on their position, using ROW_NUMBER() clause. --- ## Transactions :::caution This is an experimental feature and is subject to change in future versions. ::: An API to wrap a set of operations in a transaction that ensures the integrity of data. ### What are transactions Transactions are a set of operations that are executed together as a single unit. If any of the operations fail, the entire transaction fails and the data is rolled back to its previous state. If all operations succeed, the transaction is committed and the data is permanently saved to the database. ### Usage Transactions are handled by passing a handler function into `strapi.db.transaction`: ```js await strapi.db.transaction(async ({ trx, rollback, commit, onCommit, onRollback }) => { // It will implicitly use the transaction await strapi.db.create(); await strapi.db.create(); }); ``` After the transaction handler is executed, the transaction is committed if all operations succeed. If any of the operations throws, the transaction is rolled back and the data is restored to its previous state. :::note Every `strapi.db.query` operation performed in a transaction block will implicitly use the transaction. ::: #### Transaction handler properties The handler function receives an object with the following properties: | Property | Description | | ------------ | ------------------------------------------------------------------------------------------- | | `trx` | The transaction object. It can be used to perform knex queries within the transaction. | | `commit` | Function to commit the transaction. | | `rollback` | Function to rollback the transaction. | | `onCommit` | Function to register a callback that will be executed after the transaction is committed. | | `onRollback` | Function to register a callback that will be executed after the transaction is rolled back. | #### Nested transactions Transactions can be nested. When a transaction is nested, the inner transaction is committed or rolled back when the outer transaction is committed or rolled back. ```js await strapi.db.transaction(async () => { // It will implicitly use the transaction await strapi.db.create(); // Nested transactions will implicitly use the outer transaction await strapi.db.transaction(async ({}) => { await strapi.db.create(); }); }); ``` #### onCommit and onRollback The `onCommit` and `onRollback` hooks can be used to execute code after the transaction is committed or rolled back. ```js await strapi.db.transaction(async ({ onCommit, onRollback }) => { // It will implicitly use the transaction await strapi.db.create(); await strapi.db.create(); onCommit(() => { // This will be executed after the transaction is committed }); onRollback(() => { // This will be executed after the transaction is rolled back }); }); ``` #### Using knex queries Transactions can also be used with knex queries, but in those cases `.transacting(trx)` must be explicitly called. ```js await strapi.db.transaction(async ({ trx, rollback, commit }) => { await knex('users').where('id', 1).update({ name: 'foo' }).transacting(trx); }); ``` ### When to use transactions Transactions should be used in cases where multiple operations should be executed together and their execution is dependent on each other. For example, when creating a new user, the user should be created in the database and a welcome email should be sent to the user. If the email fails to send, the user should not be created in the database. ### When not to use transactions Transactions should not be used for operations that are not dependent on each other since it can result in performance penalties. ### Potential problems of transactions Performing multiple operations within a transaction can lead to locking, which can block the execution of transactions from other processes until the original transaction is complete. Furthermore, transactions can stall if they are not committed or rolled back appropriately. For example, if a transaction is opened but there is a path in your code that does not close it, the transaction will be left open indefinitely and could cause instability until your server is restarted and the connection is forced to close. These issues can be difficult to debug, so use transactions with care in the cases they are necessary. --- ## Migrations Strapi manages schema and data migrations in multiple ways. As much as possible we try to automatically sync the DB schema with the application configuration. However this in not sufficient to manage data migrations or schema migrations that are not reconcilable. ![Migration flowchart](/img/database/migration-flow.png) ### Internal migrations #### Creating a migration - Add a migration file in `packages/core/database/src/migrations/internal-migrations` - Import it in the `index.ts` file of the same folder and add it to the exported array as the last element. #### Migration file format Every migration should follow this API ```ts export default { name: 'name-of-migration', async up(knex: Knex, db: Database): void {}, async down(knex: Knex, db: Database): void {}, }; ``` ### User migrations User migrations live in the application's `database/migrations` directory (or `/database/migrations` when `database.settings.useTypescriptMigrations` is enabled, in which case the compiled output is discovered). They run before schema sync, inside a transaction, and receive `(knex, db)` where `db` is the `Database` instance. In `strapi develop`, the cluster primary recompiles the app before it forks the new worker on every reload, so a `.ts` migration written to the source directory (for example by the Content-Type Builder) is emitted to `/database/migrations` and discovered on that same reload. `strapi build` does the same for production. #### File ordering Discovery lists `*.js` and `*.sql` files in the migrations directory (non-recursively) and runs them **sorted by file name**. There is no other ordering mechanism, so the file name prefix decides the order. `strapi generate migration` names files `YYYY.MM.DDTHH.mm.ss..` (see `packages/generators/generators/src/plops/utils/get-formatted-date.ts`). #### Generated rename migrations When a field is renamed in the Content-Type Builder, a migration named `YYYY.MM.DDTHH.mm.ss.SSS.rename-fields.` is written to the app's source `database/migrations` directory (`packages/core/database/src/migrations/file-builder.ts`). The file is written before the schema files. If the save fails after that (schema write or folder commit), the Content-Type Builder deletes the file while rolling the save back, so a rejected save never leaves a migration behind. The prefix is the same shape as the generator's, with milliseconds appended and expressed in **UTC**, so: - generated files interleave predictably with hand-written ones by timestamp; - two saves within the same second do not collide (if the file name is still taken, the timestamp is bumped by one millisecond until it is free, so the names keep sorting in creation order); - developers in different time zones produce files that sort in creation order. Within the same second, a generated file (whose next characters are millisecond digits) sorts before a hand-written file whose name starts with a letter. To interleave your own migration deterministically with a generated one, use the same timestamp prefix and pick a later time. The generated file only calls the guarded helpers on `db.schema` (`renameColumn`, `renameTable`, `updateRows`, `applyAttributeRenames` — see `packages/core/database/src/schema/rename-helpers.ts`). Each helper checks that the source exists and the target does not before doing anything, so the file is a safe no-op on a fresh database. When both exist (the environment drifted), the step is skipped with a warning instead of failing: the migration still records as run, and schema sync then drops the old artifact. Skipped steps are logged: at `info` level when the source is missing (expected on a fresh database) and at `warn` level when the target already exists (the rename was not applied). Other behaviours worth knowing: - `renameColumn` and `renameTable` also deal with the indexes and constraints whose names embed the old identifier. On PostgreSQL they are renamed in place (a foreign-key constraint and the index of the same name are two objects there, so both are renamed). On SQLite and MySQL the old-named ones are dropped and schema sync creates them under the new name. Without this, re-using the old name later (for example renaming `a` to `b`, then adding a new `a`) would fail on every boot with an "already exists" error. - `updateRows` (component, dynamic zone and media fields, whose name is stored as a value in a link table) first deletes rows in the same scope that already carry the target value. Such rows are orphans left behind when a field with the target name was deleted earlier; merging them would resurrect deleted components or media. When no row matches the source value, the step is skipped and nothing is deleted, so re-running a partly applied file does not remove the rows it already renamed. The limit: if the renamed field has no rows at all, orphans under the target name stay and show up under the renamed field. - MySQL does not run DDL inside transactions. If a generated file fails part-way, the steps before the failure stay applied, and re-running a file with multi-hop chains (for example a swap through a temporary name) is not guaranteed to be idempotent. This is the same as for any hand-written migration on MySQL. - With `database.settings.runMigrations: false`, generated files never run, so a renamed field falls back to the old behaviour: schema sync drops the old column or table and creates an empty new one. - A rename that cannot be carried (the type, relation or component also changed; polymorphic relations; system attributes such as `documentId` or `locale`) is left out of the file and logged when the schema is saved; schema sync then drops and re-creates that field. `strapi rename:field` refuses to run in that case, and when `renameMigrations.attributes` is `never`, instead of renaming the field without a migration. - Renaming a component or moving it to another category is not a rename migration: the component keeps its uid, file and table on a display-name edit. ##### Stores keyed by attribute name Some data outside the content tables refers to fields by name, for example the `properties.fields` of admin role and admin API token permissions. The boot cleanup removes every stored field path that is not in the new schema, so after a rename every role but the super admin would lose access to the field. Every generated file therefore ends with one logical step that carries the save's renames, composed per model from the first name to the last (`a -> tmp, b -> a, tmp -> b` becomes `{ a: 'b', b: 'a' }`): ```js await db.schema.applyAttributeRenames(knex, { renames: { 'api::article.article': { title: 'heading' }, 'default.hero': { caption: 'label' }, }, }); ``` `@strapi/database` only dispatches it: `applyAttributeRenames` calls every handler registered with `db.schema.registerAttributeRenameHandler(handler)`, in registration order, with the migration's transaction. Handlers must be registered during the register phase, because user migrations run during schema sync, before any plugin bootstrap. The admin registers one that rewrites permission field paths (including paths through components, so a parent rename and a rename inside its component compose) before its bootstrap cleanup runs. Other stores keyed by attribute name, such as Content Manager layouts, can register on the same hook. With no handler registered, the step is a logged no-op. The step is written even when every hop of the save was refused, since the logical field is the same field even when its data could not be carried. --- ## Lifecycles Content-type lifecycle hooks (`src/api//content-types//lifecycles.ts`) receive a database lifecycle `Event` object. For TypeScript projects, import the type from `@strapi/database`: ```ts import type { Event } from '@strapi/database'; export default { async beforeCreate(event: Event) { const { data } = event.params; // ... }, }; ``` The `Event` type includes `action`, `model`, `params`, `state`, and optional `result`. It is defined in `@strapi/database` alongside the lifecycle subscriber implementation. --- ## @strapi/database ### Purpose `@strapi/database` is the database layer of the server. It sits on top of Knex and turns content type schemas into tables, columns and join tables. It keeps the database schema in sync with the application, and it runs queries, relation writes, migrations, lifecycles and transactions. It runs on the server only. `@strapi/core` creates one `Database` instance and exposes it as `strapi.db`. The Document Service and the core services read and write data through it. Application developers use `strapi.db.query(uid)`, `strapi.db.transaction` and the `Event` type of lifecycles. The package README says it is meant to be used inside Strapi only. ### Key concepts #### The `Database` class `Database` in [`src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/database/src/index.ts) owns the Knex connection, the dialect and the providers listed below. The constructor builds them. `init({ models })` loads the models into the metadata and validates them. `@strapi/core` calls `init` and then `schema.sync()` during `bootstrap()`. See [Server lifecycle](../../../architecture/02-server-lifecycle.md). #### Dialects A `Dialect` class per client hides the differences between databases. The clients are `sqlite`, `postgres` and `mysql`. Each dialect has a schema inspector that reads the current database schema. The `mysql` dialect also serves MariaDB. Code: [`src/dialects`](https://github.com/strapi/strapi/tree/develop/packages/core/database/src/dialects). #### Metadata `Metadata` is a map from a model UID to its `Meta`. `loadModels` builds the table names, the column-to-attribute map and the relations from the models. `@strapi/core` builds the models from content types and components with `transformContentTypesToModels`. The database never reads content type files itself. Code: [`src/metadata`](https://github.com/strapi/strapi/tree/develop/packages/core/database/src/metadata). #### Schema sync `db.schema.sync()` converts the metadata into a schema and compares it with the stored one. It runs pending migrations first. If the schema hash changed, `syncSchema` makes a three-way diff of the database schema, the previously stored schema and the new schema, and applies it through the schema builder. The stored copy lives in the `strapi_database_schema` table. Read [Schema sync](../../../architecture/07-schema-sync.md) for the full flow. Code: [`src/schema`](https://github.com/strapi/strapi/tree/develop/packages/core/database/src/schema). #### Entity manager, repositories and query builder `db.query(uid)` returns a repository with `findOne`, `findMany`, `create`, `update`, `delete`, `count` and relation helpers. The entity manager implements them and delegates to the query builder, which builds Knex queries for filters, populate, ordering and search. Repositories work at the row level. The Document Service adds draft and publish and locale logic on top. See [Document write path](../../../architecture/05-document-write-path.md). Code: [`src/entity-manager`](https://github.com/strapi/strapi/tree/develop/packages/core/database/src/entity-manager) and [`src/query`](https://github.com/strapi/strapi/tree/develop/packages/core/database/src/query). #### Relations The entity manager writes relations into join tables and keeps their order. Polymorphic relations store the target type next to the target id, in inline columns or in the join table. Read [Polymorphic relations](./01-relations/polymorphic-relations.md) and [Reordering](./01-relations/reordering.md). #### Transactions `db.transaction(callback)` runs the callback in a Knex transaction. It stores the transaction in an async context (`transactionCtx`), so queries inside the callback use it without an explicit argument. Nested calls reuse the outer transaction. Read [Transactions](./02-transactions.md). #### Migrations Two providers run migrations: the user provider for files in `database/migrations` of the application, and the internal provider for the migrations in `src/migrations/internal-migrations`. Each provider records the names it ran in its own table, `strapi_migrations` for user migrations and `strapi_migrations_internal` for internal ones. Read [Migrations](./03-migrations.md). #### Lifecycles `db.lifecycles` runs subscribers around each query with an `Event` (`action`, `model`, `params`, `state`, `result`). Two subscribers exist by default: one sets timestamps, and one calls the `lifecycles` functions of a model. Read [Lifecycles](./04-lifecycles.md). #### Repairs `db.repair` holds two data repair operations, `removeOrphanMorphType` and `processUnidirectionalJoinTables`. `@strapi/core` runs them during `bootstrap()` after a schema sync. Code: [`src/repairs`](https://github.com/strapi/strapi/tree/develop/packages/core/database/src/repairs). ### Related - [Schema sync](../../../architecture/07-schema-sync.md): how content types become tables at boot. - [Document write path](../../../architecture/05-document-write-path.md): how the Document Service calls this package. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): where `init` and `sync` run. - [Glossary](../../../architecture/11-glossary.md): naming traps for lifecycles and hooks. - [`@strapi/core`](../core/index.md): creates `strapi.db` and runs the repairs. - [`@strapi/utils`](../utils/index.md): shared helpers and errors that this package uses. --- ## @strapi/email ### Purpose `@strapi/email` is a core plugin that sends emails from a Strapi application. It hides the email backend behind one `send` function and one config key, `provider`. It runs on the server, with a small admin part for the settings page. The admin panel uses it to send password reset emails. The `users-permissions` plugin uses it for its confirmation and reset emails. Application developers call it from their own code. Provider packages such as `@strapi/provider-email-nodemailer` implement the actual delivery. ### Key concepts #### Provider A provider is a module that exports `init(providerOptions, settings)` and returns an object with a `send` function. It can also return `verify`, `isIdle`, `close` and `getCapabilities`. The `bootstrap` function reads `plugin::email` config, loads the module `@strapi/provider-email-` (or the plain module name when no such package exists), and stores the instance in `strapi.plugin('email').provider`. If the module fails to load, bootstrap throws `Could not load email provider`. See [`bootstrap.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/email/server/src/bootstrap.ts). The default config uses the `sendmail` provider and a `defaultFrom` address. The package depends on `@strapi/provider-email-sendmail` for this default. See [`config.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/email/server/src/config.ts). #### Email service The `email` service has three functions: `send`, `sendTemplatedEmail` and `getProviderSettings`. `send` passes the options to the provider. `sendTemplatedEmail` fills `subject`, `text` and `html` with a lodash template, and allows only the variables that exist in the data object. Other code calls it as `strapi.plugin('email').service('email')`. See [`services/email.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/email/server/src/services/email.ts). #### Routes The plugin has two route sets. The `content-api` route `POST /` sends an email and validates its input and response with an `EmailRouteValidator`. The `admin` routes (`send`, `test`, `settings`, `verify`) serve the settings page. The `test`, `settings` and `verify` routes require the `plugin::email.settings.read` permission, which `bootstrap` registers in the admin action provider. #### Rate limit middleware The plugin defines the middleware `plugin::email.rateLimit`, built on `koa2-ratelimit`. It keys the limit on `request.body.email`. The admin authentication routes use it. The email routes of the plugin do not. Read `plugin::email` config `ratelimit` to change the limits. See [`middlewares/rateLimit.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/email/server/src/middlewares/rateLimit.ts). #### Settings page The admin part adds an `Email Plugin` section to the admin settings with `app.addSettingsLink`. The page shows the config from the `settings` route and offers a test email. It also offers a connection check when the provider has a `verify` function. The `capabilities` and `isIdle` fields come from optional provider functions. The types are in [`shared/types.ts`](https://github.com/strapi/strapi/blob/develop/packages/core/email/shared/types.ts). ### Related - [Extension points](../../../architecture/04-extension-points.md): how plugins register config, routes, services and middlewares. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): when the plugin `bootstrap` runs and loads the provider. - [Authentication](../../../architecture/08-authentication.md): password reset flows send email through this plugin. - [`@strapi/provider-email-sendmail`](../../providers/email-sendmail/index.md): default provider. - [`@strapi/provider-email-nodemailer`](../../providers/email-nodemailer/index.md): SMTP provider that supports `verify`. - [`@strapi/plugin-users-permissions`](../../plugins/users-permissions/index.md): sends confirmation and reset emails. --- ## Technologies This section provides an overview of the technologies used in the OpenAPI package. --- ### Tools The OpenAPI package uses the same tools as the rest of the Strapi monorepo packages: | Tool | Purpose | | -------- | --------------- | | Jest | Unit testing | | Rollup | Build | | Prettier | Code formatting | | ESLint | Code linting | ### Dependencies The package focuses on using a limited set of external dependencies to keep a light footprint. --- #### [Zod](https://github.com/colinhacks/zod) Route objects carry Zod schemas on `route.request` and `route.response`. Assemblers convert those schemas into OpenAPI parameters, request bodies, and response objects via `z.toJSONSchema` (see `src/utils/zod.ts` and the `ComponentsWriter` post-processor). :::info Keeping schemas on routes avoids leaking OpenAPI domain logic into Strapi core. ::: :::warning Cyclic dependencies between models (_relations, components, dynamic zones, media_) remain difficult to represent accurately in the final component schemas. ::: #### [Debug](https://github.com/debug-js/debug) > A tiny JavaScript debugging utility modelled after Node.js core's debugging technique. Works in Node.js and web browsers. Used to provide detailed information about what is happening during the document generation processes. The root namespace for the package is `strapi:core:openapi` and each component adds its own suffix. :::tip A small wrapper that automatically binds the correct default namespace can be found in the package utils: `src/utils/debug.ts` Enable debug output with `DEBUG=strapi:core:openapi:*`. ::: #### Types - [`@strapi/types`](https://github.com/strapi/strapi/tree/develop/packages/core/types) provides types to represent core Strapi objects like routes or an application - [`openapi-types`](https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-types) provides types to represent a full OpenAPI document and its sub-objects --- ## Architecture This section explores the overall architecture and main components of the OpenAPI package. --- ### Overview The package is organized into two main domains: #### The Route Collection This domain provides objects that extract different sets of routes from a Strapi application. You can find related files in `src/routes`. :::info This domain could potentially be moved to the Strapi core or Strapi utils in the future since it doesn't have any direct dependencies on OpenAPI itself. ::: #### The OpenAPI Document Generation This domain contains all the components necessary to generate a valid OpenAPI document from a Strapi application: | Component | Path | | ----------------- | ----------------------------------------- | | OpenAPI Generator | `src/generator` | | Assemblers | `src/assemblers` | | Contexts | `src/context` | | Registries | `src/registries` | | Processors | `src/pre-processor`, `src/post-processor` | --- Additionally, the package centralizes all exports for the public API in an `src/exports.ts` file. This approach decouples the package root exports from the public programmatic API it exposes. For step-by-step extension guides, see the [Contributing](./contributing/00-overview.mdx) section. ### Design Pattern The package follows Object-Oriented Design principles and patterns to ensure code that's both maintainable and extensible: - **SOLID Principles**: Adheres to single responsibility, open/closed, Liskov substitution, interface segregation, and dependency inversion principles - **Dependency Injection**: Components receive dependencies through their constructors rather than creating them internally - **Factory Pattern**: Used to instantiate complex objects with configurable behaviors - **Registry Pattern**: Maintains global state and configuration in a controlled manner (see Registries and Context) The architecture emphasizes: - Atomic components with single responsibilities, particularly for the assembly process (see Assemblers) - Clear separation of concerns (orchestrator, routes collection, assembler, lifecycle, etc.) - Extensible design that enables adding new capabilities without modifying existing code (see Assembler, Processors) This results in a modular system where each component focuses on a specific task while maintaining clear boundaries with other parts of the system. ### Components #### Route Collector Orchestrates the route collection process based on provided providers and matchers: - **Providers** - Collect sets of routes for specific purposes (Admin, Core API, plugins, etc.) - **Matchers** - Filter route collections based on rules - **Rules** - Match (or exclude) a single route based on specific criteria ```mermaid erDiagram RouteCollector ||--o{ Provider: orchestrates RouteCollector ||--o{ Matcher: uses Provider ||--|{ Route: collects Matcher ||--|{ Rule: contains Provider ||--o{ Matcher: routesFilteredBy Rule }o--|| Route: evaluates Provider { string type string source } Route { object info string path string method string handler } Matcher { array rules function match } Rule { function match } RouteCollector { array providers array matchers function collect } ``` #### OpenAPI Generator Generates valid OpenAPI documents from a Strapi application: - **Processors** - Lifecycle objects and methods - **Pre-processors**: Prepare data and registries before assembly - **Post-processors**: Clean up and finalize the output after assembly - **Assemblers** - Generate specific parts of the final document - Each has a single responsibility - **Sub-Assemblers** - Specialized assemblers for nested components (operations, etc.) - Are provided with custom contexts - **Context** - Holds information and instances for assembly processes - Maintains reference to the final built object in `context.output.data` - **Registries** - Reserved for shared assembly state (`RegistriesFactory` currently returns an empty object) ```mermaid erDiagram OpenAPIGenerator ||--|{ PreProcessor: "uses before assembly" OpenAPIGenerator ||--|{ PostProcessor: "uses after assembly" OpenAPIGenerator ||--|{ Assembler: orchestrates OpenAPIGenerator ||--|| Context: maintains Context ||--|{ Registry: manages PreProcessor }o--|| Context: "prepares" PostProcessor }o--|| Context: "finalizes" Assembler }o--|| Context: updates Assembler ||--o{ Assembler: "contains nested" OpenAPIGenerator { function generate array assemblers array preProcessors array postProcessors } PreProcessor { function preProcess } PostProcessor { function postProcess } Assembler { function assemble Assembler[] children } Context { object output Strapi strapi array routes array registries } Registry { array entries function add function get function has } ``` --- ## Usage This section explores the Strapi OpenAPI toolset. --- ### `generate` Generate an OpenAPI JSON document based on the given Strapi application. By default, it collects content API routes registered in the application, transforms them into OpenAPI path objects, and fills in other OpenAPI components. #### Signature ```typescript function generate(strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput; ``` #### Parameters - `strapi` — the Strapi application to generate an OpenAPI specification for - `options.type` — optional route set to document: `'content-api'` (default) or `'admin'` #### Return value A generation output object containing: - `document` — the generated OpenAPI specification as JSON - `durationMs` — elapsed generation time in milliseconds #### Example ```typescript import { generate } from '@strapi/openapi'; const { document, durationMs } = generate(strapi, { type: 'content-api' }); ``` --- ### CLI An experimental CLI wrapper is available in `@strapi/strapi`: ```bash strapi openapi generate [-o, --output ] ``` The command loads the current application, calls `generate()`, and writes the result to a JSON file (default: `specification.json` in the project root). :::warning The OpenAPI generation feature is experimental. Its behavior and output may change without following semver. ::: --- ## Overview(Contributing) Guides for extending `@strapi/openapi`. Start with [Architecture](../02-architecture.md) if you need the overall pipeline. --- ### Extension points | Guide | Use when you need to… | | -------------------------------------------------- | --------------------------------------------- | | [Routes provider](./01-routes-provider.md) | Collect routes from a new Strapi source | | [Routes matcher rule](./02-routes-matcher-rule.md) | Filter which routes are documented | | [Assemblers](./03-assemblers.md) | Build or extend OpenAPI document sections | | [Context factory](./04-context-factory.md) | Add a new assembly level with its own context | | [Processors](./05-processors.md) | Run logic before or after assembly | | [Testing](./06-testing.md) | Write or debug unit tests | Most changes touch **assemblers** (especially operation-level leaf assemblers) or **route collection**. Context factories and processors are needed less often. import DocCardList from '@theme/DocCardList'; import { useCurrentSidebarCategory } from '@docusaurus/theme-common'; --- ## Routes Provider Learn how to create a new routes provider --- ### Adding a Routes Provider Considering you want to add a new provider called `xxx`. ##### 1. Add a new `xxx.ts` file in `src/routes/providers` and paste the following snippet: ```typescript import type { Core } from '@strapi/types'; import { createDebugger } from '../../utils'; import { AbstractRoutesProvider } from './abstract'; const debug = createDebugger('routes:provider:xxx'); export class XXXRoutesProvider extends AbstractRoutesProvider { public get routes(): Core.Route[] { const routes = []; // ^ The routes collection logic goes here // Access to this._strapi to interact with the Strapi application debug('found %o routes in xxx', routes.length); return routes; } } ``` ##### 2. Export the provider from `src/routes/providers/index.ts` ```typescript // ... export { XXXRoutesProvider } from './xxx'; // ^ export the provider from here ``` ##### 3. Re-export the provider from `src/routes/index.ts` ```typescript export { // ... other providers XXXRoutesProvider, // ^ re-export the provider from here } from './providers'; ``` ##### 4. Finally, modify the `generate` function in `src/exports.ts` to use the newly created provider ```typescript // ... import { // ... XXXRoutesProvider, // ^ import the newly created provider } from './routes'; export const generate = (strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput => { // ... const routeCollector = new RouteCollector( [ // ... other providers new XXXRoutesProvider(strapi), // ^ instantiate the new provider here ] // ... ); // ... }; ``` --- ## Routes Matcher Rule Learn how to create a new routes' matcher rule --- ### Adding a Routes Matcher Rule Considering you want to add a new route matcher rule to only select routes with certain methods. ##### 1. Add a new file named `is-method-in.ts` in `src/routes/rules` and paste the following snippet: ```typescript import type { MatcherRule } from '../types'; export const isMethodIn = (methods: string[]): MatcherRule => { return (route) => methods.includes(route.method); }; ``` ##### 2. Next, export the rule from `src/routes/rules/index.ts` ```typescript // ... other exports export { isMethodIn } from './is-method-in'; // ^ export the rule from here ``` ##### 3. Finally, pass the new rule to the route collector instance created in the `generate` process in `src/exports.ts` ```typescript // ... export const generate = (strapi: Core.Strapi, options?: GenerationOptions): GeneratorOutput => { // ... const routeCollector = new RouteCollector( [ /* ... */ ], new RouteMatcher([ // ... other rules rules.isMethodIn(['POST', 'PUT']), // ^ pass the new rule to the matcher instance ]) ); // ... }; ``` --- ## Assemblers Learn how to add assemblers to the document generation pipeline. --- ### Assembler levels Assemblers build the OpenAPI document in layers. Each level has a matching context type (see [Context factory](./04-context-factory.md)). | Level | Interface | Example | Factory | | --------- | --------------------- | ------------------------------ | --------------------------- | | Document | `Assembler.Document` | `DocumentInfoAssembler` | `DocumentAssemblerFactory` | | Path | `Assembler.Path` | `PathItemAssembler` | `PathAssemblerFactory` | | Path item | `Assembler.PathItem` | `OperationAssembler` | `PathItemAssemblerFactory` | | Operation | `Assembler.Operation` | `OperationParametersAssembler` | `OperationAssemblerFactory` | Most day-to-day changes are **operation-level leaf assemblers** (parameters, body, responses, etc.). Add a **composite assembler** when you need to orchestrate a new nested structure. --- ### Adding a leaf assembler Consider adding an operation assembler called `OperationSummaryAssembler` that sets `summary` from route metadata. ##### 1. Create `summary.ts` in `src/assemblers/document/path/path-item/operation` ```typescript import type { Core } from '@strapi/types'; import type { OperationContext } from '../../../../../types'; import { createDebugger } from '../../../../../utils'; import type { Assembler } from '../../../..'; const debug = createDebugger('assembler:summary'); export class OperationSummaryAssembler implements Assembler.Operation { assemble(context: OperationContext, route: Core.Route): void { const summary = route.info.apiName ?? route.handler; debug('assembling summary for %o %o: %o', route.method, route.path, summary); context.output.data.summary = summary; } } ``` Leaf assemblers receive the context for their level plus any extra arguments defined on the interface (operation assemblers also receive the `route`). Write results into `context.output.data`. That object is merged into the parent output by the composite assembler that invoked yours. ##### 2. Export from `src/assemblers/document/path/path-item/operation/index.ts` ```typescript export { OperationSummaryAssembler } from './summary'; ``` ##### 3. Register in `OperationAssemblerFactory.createAll()` ```typescript import { OperationSummaryAssembler } from './summary'; export class OperationAssemblerFactory { createAll(): Assembler.Operation[] { return [ // ... existing assemblers new OperationSummaryAssembler(), ]; } } ``` For a **document-level** leaf assembler (e.g. a new top-level OpenAPI field), follow the same pattern under `src/assemblers/document/` and register it in `DocumentAssemblerFactory`. --- ### Adding a composite assembler Composite assemblers create a child context, run sub-assemblers, then merge the result into the parent output. `OperationAssembler` (`src/assemblers/document/path/path-item/operation/operation.ts`) is the reference implementation. Use this pattern when you introduce a new nested document section that needs its own context and multiple sub-assemblers. ##### 1. Implement the composite class ```typescript import type { Core } from '@strapi/types'; import { OperationContextFactory } from '../../../../../context'; import type { PathItemContext } from '../../../../../types'; import type { Assembler } from '../../../..'; export class OperationAssembler implements Assembler.PathItem { constructor( private readonly _assemblers: Assembler.Operation[], private readonly _contextFactory: OperationContextFactory = new OperationContextFactory() ) {} assemble(context: PathItemContext, path: string, routes: Core.Route[]): void { const { output, ...sharedProps } = context; for (const route of routes) { const operationContext = this._contextFactory.create(sharedProps); for (const assembler of this._assemblers) { assembler.assemble(operationContext, route); } Object.assign(output.data, { [route.method.toLowerCase()]: operationContext.output.data }); } } } ``` ##### 2. Wire it through a factory Create or extend a factory (e.g. `PathItemAssemblerFactory`) that instantiates your composite assembler with its sub-assemblers and context factory, then register that factory in the parent level (`PathAssemblerFactory` or `DocumentAssemblerFactory`). :::tip Reuse `timer` and `registries` from the parent context when creating child contexts so timing and shared state stay consistent. Pass them via `PartialContext` (see [Context factory](./04-context-factory.md)). ::: --- ## Context Factory Learn how to add a context factory for a new assembly level. --- ### When to add one Each assembler level operates on a typed context (`DocumentContext`, `OperationContext`, etc.) created by a matching factory. Add a new factory when you introduce a **new assembly level** with its own output shape. If you only add leaf assemblers at an existing level, reuse the existing factory (e.g. `OperationContextFactory`). --- ### Adding a context factory Suppose you need a new assembly level with its own output shape. Follow the pattern used by `OperationContextFactory`. ##### 1. Define the context data type in `src/types.ts` ```typescript import type { Context } from './context'; export type WidgetContextData = Partial<{ widgets: Record }>; export type WidgetContext = Context; ``` ##### 2. Create `src/context/factories/widget.ts` ```typescript import { RegistriesFactory } from '../../registries'; import type { WidgetContext, WidgetContextData } from '../../types'; import { TimerFactory } from '../../utils'; import type { PartialContext } from '../types'; import { AbstractContextFactory } from './abstract'; export class WidgetContextFactory extends AbstractContextFactory { constructor( registriesFactory: RegistriesFactory = new RegistriesFactory(), timerFactory: TimerFactory = new TimerFactory() ) { super(registriesFactory, timerFactory); } create(context: PartialContext): WidgetContext { return super.create(context, {}); } } ``` `AbstractContextFactory.create()` builds a context with: - `strapi` and `routes` (required) - `timer` and `registries` (reused from the parent when provided, otherwise created fresh) - `output.data` initialized to the `defaultValue` passed to `super.create()` ##### 3. Export from `src/context/factories/index.ts` ```typescript export { WidgetContextFactory } from './widget'; ``` ##### 4. Use in your composite assembler Pass shared props from the parent context so sub-assemblers share the same timer and registries: ```typescript const childContext = this._contextFactory.create({ strapi: context.strapi, routes: context.routes, timer: context.timer, registries: context.registries, }); ``` --- ### Registries `RegistriesFactory.createAll()` currently returns an empty object and `ContextRegistries` is an empty interface. Registries are reserved for future shared assembly state (deduplicated schemas, cross-assembler caches, etc.). No separate setup is required today. --- ## Processors Learn how to add pre- and post-processors to the generation lifecycle. --- ### Overview The generator runs components in this order: 1. **Pre-processors** — prepare the document context before assembly 2. **Assemblers** — build the OpenAPI document 3. **Post-processors** — finalize the document after assembly Both processor types receive the full `DocumentContext`. Register instances in `PreProcessorFactory` or `PostProcessorsFactory`; those factories are wired in `src/exports.ts`. --- ### Adding a post-processor `ComponentsWriter` is the current post-processor: it writes `components.schemas` from `strapi.contentAPISchemaRegistry` after assembly. Nested Zod `.meta({ id })` schemas harvested during route conversion are merged in via `context.registries.extractedComponentSchemas` so `$ref`s resolve. ##### 1. Create a processor class ```typescript import type { DocumentContext } from '../types'; import type { PostProcessor } from './types'; export class ExamplePostProcessor implements PostProcessor { postProcess(context: DocumentContext): void { // mutate context.output.data } } ``` ##### 2. Register it in `PostProcessorsFactory` ```typescript import { ComponentsWriter } from './component-writer'; import { ExamplePostProcessor } from './example'; export class PostProcessorsFactory { createAll(): PostProcessor[] { return [new ComponentsWriter(), new ExamplePostProcessor()]; } } ``` Pre-processors implement `PreProcessor` with a `preProcess(context: DocumentContext)` method and are registered the same way in `PreProcessorFactory`. :::tip Prefer assemblers for building document sections. Use processors only for cross-cutting work that must run before or after the full assembly pass. ::: --- ## Testing(Contributing) How to test changes in `@strapi/openapi`. --- ### Location and tooling Unit tests live in `packages/core/openapi/__tests__/`. Run them from the package directory: ```bash cd packages/core/openapi && yarn test:unit ``` Shared helpers are in `__tests__/fixtures/` and `__tests__/mocks/`. --- ### Testing route providers Mock the Strapi instance with only the properties your provider reads. `StrapiMock` in `__tests__/mocks/strapi.mock.ts` covers common `apis` and `plugins` shapes. ```typescript import type { Core } from '@strapi/types'; import { ApiRoutesProvider } from '../../src/routes'; import { StrapiMock } from '../mocks'; it('returns registered API routes', () => { const strapi = new StrapiMock() as unknown as Core.Strapi; const provider = new ApiRoutesProvider(strapi); expect(provider.routes.length).toBeGreaterThan(0); }); ``` --- ### Testing matcher rules Build a minimal `Core.Route` and pass it to `RouteMatcher`: ```typescript import type { Core } from '@strapi/types'; import { RouteMatcher } from '../../src/routes'; const route: Core.Route = { method: 'GET', path: '/api/articles', handler: '', info: { type: 'content-api' }, }; expect(new RouteMatcher([(r) => r.method === 'GET']).match(route)).toBe(true); ``` --- ### Testing assemblers Create a context through the matching factory, run the assembler, and assert on `context.output.data`: ```typescript import type { Core } from '@strapi/types'; import * as z from 'zod/v4'; import { OperationParametersAssembler } from '../../src/assemblers/document/path/path-item/operation'; import { OperationContextFactory } from '../../src/context'; const context = new OperationContextFactory().create({ strapi: {} as Core.Strapi, routes: [] }, {}); new OperationParametersAssembler().assemble(context, { method: 'GET', path: '/api/articles/:id', handler: '', info: { type: 'content-api' }, request: { params: { id: z.string() }, query: { locale: z.string().optional() }, }, }); expect(context.output.data.parameters).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'id', in: 'path' }), expect.objectContaining({ name: 'locale', in: 'query' }), ]) ); ``` Route `request` and `response` Zod schemas are the primary inputs to operation assemblers. See `__tests__/operation-assemblers.test.ts` for examples that mirror content API `addQueryParams` / `addInputParams` behaviour. --- ### Debugging failing tests Enable package debug output from the package directory: ```bash cd packages/core/openapi && DEBUG=strapi:core:openapi:* yarn test:unit ``` See [Technologies](../01-technologies.md) for the debug namespace conventions. --- ## @strapi/openapi ### Purpose The Strapi OpenAPI package offers a comprehensive set of utilities for creating and manipulating OpenAPI specifications based on Strapi applications. It features a user-friendly API, a maintainable and extensible codebase, and thorough documentation. The package runs on the server. `@strapi/core` uses it to serve the `/openapi.json` endpoints, and the `strapi openapi generate` command in `@strapi/strapi` uses it to write a specification file. :::info For more information about OpenAPI, please refer to [the official specification](https://swagger.io/specification/) ::: #### Context From Strapi `v3-alpha` through `v5` (since its [initial release in December 2018](https://medium.com/strapi/introducing-the-api-documentation-swagger-plugin-29092af2c880)), the [official documentation plugin](https://www.npmjs.com/package/@strapi/plugin-documentation) has been the standard solution for integrating Strapi with OpenAPI. This plugin provided automatic documentation generation, extensive customization options, and a Swagger UI interface. However, after six years of evolving the CMS, the documentation plugin has become outdated and prone to bugs. Meanwhile, new requirements have emerged from both the community (SDK generation, Swagger support, etc.) and internal tooling needs (Strapi client, API playground, etc.). It's within this context that we've developed this new package, designed to offer specialized features while remaining flexible enough for broad application. ### Key concepts The [Architecture](./02-architecture.md) page describes each concept with diagrams. The [Contributing](./contributing/00-overview.mdx) guides explain how to extend each one. - **`generate()`**: the public entry point, exported from `src/exports.ts`. It builds the pipeline and returns `{ document, durationMs }`. It is experimental. See [Usage](./03-usage.md). - **Route collection**: a `RouteCollector` reads routes from providers (`AdminRoutesProvider`, `ApiRoutesProvider`, `PluginRoutesProvider`) and keeps the routes that a `RouteMatcher` accepts. A matcher holds rules, for example `isOfType`, which compares `route.info.type`. See [Routes provider](./contributing/01-routes-provider.md) and [Routes matcher rule](./contributing/02-routes-matcher-rule.md). - **Generator**: `OpenAPIGenerator` in `src/generator` creates the context, then runs the pre-processors, the assemblers and the post-processors in this order. - **Assemblers**: each assembler writes one part of the document. The default set covers metadata, info, servers, security and paths. The paths assembler delegates to sub-assemblers with their own contexts. See [Assemblers](./contributing/03-assemblers.md). - **Context**: a `Context` holds the Strapi instance, the collected routes, the registries and the output in `context.output.data`. Context factories create the document, path, path item and operation contexts. See [Context factory](./contributing/04-context-factory.md). - **Processors**: pre-processors prepare the context before assembly, and post-processors finish the output after it. The default post-processor, `ComponentsWriter`, converts the schemas in `strapi.contentAPISchemaRegistry` (Zod) into `components.schemas`. There is no default pre-processor. See [Processors](./contributing/05-processors.md). - **Registries**: the shared state of one generation run. `RegistriesFactory` creates them. Today they hold `extractedComponentSchemas`. #### Scope **What it does ✅** This package provides APIs and tools to: - Programmatically generate OpenAPI documents **specifically** tailored for Strapi applications - Customize the document generation process through providers, matchers, assemblers, and processors **What it's not intended to be ❌** - A direct replacement for the documentation plugin (including Swagger UI) - A generic OpenAPI specification generator for non-Strapi applications - A standalone file writer (generation returns an in-memory document; see [Usage](./03-usage.md) for the experimental CLI wrapper) #### Limitations - Cyclical references between models (_relations, components, dynamic zones, media_) are not fully represented in component schemas yet - Limited customization capabilities in the current version; these will be expanded in future releases (our priority is making it ready for client use before iterating further) ### Related - [Extension points](../../../architecture/04-extension-points.md): how plugins and APIs declare the routes that the providers collect. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): the route providers read `strapi.apis` and `strapi.plugins`, which the load phase fills. - [`@strapi/core`](../core/index.md): serves the `/openapi.json` endpoints through its server service. - [`@strapi/strapi`](../strapi/index.md): hosts the `strapi openapi generate` command. - [`documentation` plugin](../../plugins/documentation/index.md): the older OpenAPI solution that this package does not replace. - [Technologies](./01-technologies.md): tools and dependencies of the package. --- ## Permission Engine The permission engine is the entity responsible for generating new ability instances based on provided permissions, actions, and conditions. This is the abstraction used by other Strapi systems to create their custom permissions' engine: - [RBAC](https://github.com/strapi/strapi/blob/develop/packages/core/admin/server/src/services/permission/engine.ts) - [Transfer Tokens](https://github.com/strapi/strapi/blob/develop/packages/core/admin/server/src/services/transfer/permission.ts) - [API Tokens](https://github.com/strapi/strapi/blob/develop/packages/core/core/src/services/content-api/permissions/engine.ts) ### Usage ```typescript import * as permissions from '@strapi/permissions'; const engine = permissions.engine.new({ providers: { action: myActionProvider, condition: myConditionProvider, }, }); const permission = { action: 'read', subject: 'article', conditions: ['isPublished'], }; const ability = engine.generate([permission]); console.log('Can read article?:', ability.can('read', 'article')); ``` ### Ability Generation Diagram ```mermaid sequenceDiagram participant User participant Engine participant Hooks participant Providers participant Ability Builder User ->> Engine: Generate Ability loop permissions Engine ->> Hooks: before-format::validate.permission Hooks -->> Engine: Validation Result Engine ->> Hooks: format.permission Hooks -->> Engine: Formatted Permission Engine ->> Hooks: after-format::validate.permission Hooks -->> Engine: Validation Result Engine ->> Hooks: before-evaluate.permission alt permission has conditions Engine ->> Providers: Resolve conditions Providers -->> Engine: Resolved conditions loop conditions alt forbidden note over Engine: skip permission end end end Engine ->> Hooks: before-register.permission Engine ->> Ability Builder: Register permission in Ability end Engine -->> User: Ability ``` ### Hook System #### Available Hooks | Hook Name | Phase | Purpose | | ---------------------------------- | ---------------- | ------------------------------------------------------------ | | before-format::validate.permission | Pre-formatting | Initial validation | | format.permission | Formatting | Transform permission | | after-format::validate.permission | Post-formatting | Validate formatted permission | | before-evaluate.permission | Pre-evaluation | Transform the permission after formatting and validation | | before-register.permission | Pre-registration | Final modifications after evaluation but before registration | #### Hook Implementation Example ```typescript const engine = permissions.engine.new({ providers }); // Validation hook engine.on('before-format::validate.permission', ({ permission }) => { if (permission.action === 'delete' && !permission.conditions.includes('isAdmin')) { return false; // Prevent non-admin delete } return true; }); // Format hook engine.on('format.permission', ({ permission }) => { // Add timestamp to tracking actions if (permission.action.startsWith('track')) { return { ...permission, timestamp: Date.now(), }; } return permission; }); ``` ### Engine Configuration Options ```typescript interface EngineConfiguration { providers: { action: ActionProvider; condition: ConditionProvider; }; abilityBuilderFactory?: () => CustomAbilityBuilder; } ``` #### Action Provider A provider instance that contains all available actions that can be used in permissions when generating abilities. #### Condition Provider A provider instance mapping the conditions' name to their definitions. Used to resolve conditions by name when evaluating and registering a permission during the ability creation process. #### Ability Builder Factory A factory that is used to create an ability instance out of multiple permissions. It must implement the `CustomAbilityBuilder` interface. ### Advanced Usage Examples #### Custom Ability Builder ```typescript const customAbilityBuilder = () => { const { can, build } = new AbilityBuilder(Ability); return { can(permission) { // Custom permission logic const { action, subject, properties = {}, condition } = permission; return can(action, subject, properties.fields, condition); }, build() { return build({ // Custom matcher implementation conditionsMatcher: (rule, context) => { // Custom condition matching logic return true / false; }, }); }, }; }; const engine = permissions.engine.new({ providers, abilityBuilderFactory: customAbilityBuilder, }); ``` #### Conditional Permission Chain ```typescript const permission = domain.permission.create({ action: 'update', subject: 'article', conditions: ['isOwner'], }); engine.on('before-evaluate.permission', ({ permission }) => { // Add dynamic conditions based on context if (permission.action === 'update') { permission.conditions.push('isNotLocked'); permission.conditions.push('isWithinBusinessHours'); } }); ``` --- ## @strapi/permissions ### Purpose The `@strapi/permissions` package is a sophisticated permission management system designed to provide flexible, granular control over access rights in Strapi systems. Built on top of CASL's ability system, it extends the basic permission model with advanced features like parametrized actions, conditional evaluation, and a hook system for custom behaviors. It serves as the backbone for building advanced implementations in Strapi, enabling developers to design customized permission systems tailored to specific Strapi business objectives and application demands like RBAC, users and permissions, or API tokens. The package runs on the server. Admin RBAC, transfer tokens and the Content API permissions each create their own engine from it. ### Key concepts The [Permission engine](./01-engine.md) page covers the hook order, the engine options and the custom ability builder in detail. #### Engine ```mermaid graph TB B[/Action Provider/]:::provider --> A([Permission Engine]):::engine C[/Condition Provider/]:::provider --> A A --> D{{Hook System}}:::hookSystem A --> E([Ability Generator]):::generator D --> H([Lifecycle Hooks]):::lifecycle E --> I([CASL Integration]):::integration H --> N([Permission Validation]):::hook H --> O([Permission Formatting]):::hook H --> P([Permission Evaluation]):::hook ``` #### Domain ```mermaid graph TB N([Domain]):::namespace --> A([Permission]):::permission A --> B([Action]):::action A --> C([Subject]):::subject A --> D([Conditions]):::conditions A --> E([Properties]):::properties A --> F([ActionParameters]):::actionParameters ``` #### Dynamic evaluation Runtime permission checking at the time of the request, ensuring that permissions align with the most current context and data. _Example Use Case_: API endpoint access control, where user roles and data states impact access decisions dynamically. #### Parametrized actions Actions that leverage context-specific parameters to enable fine-grained control, allowing flexibility in defining permissions. _Example Use Case_: `publish?postId=123`, restricting operation to a specific post identified by its ID. #### Conditional logic Implementation of complex permission rules that consider different conditions, enabling nuanced access control tailored to resource state or user data. _Example Use Case_: Validate resource ownership by checking if the requesting user is the owner of a specific resource. #### Hook system A modular mechanism to inject custom behaviors during various stages of the permission validation process, offering extensibility and adaptability. _Example Use Case_: Implement audit logging for permission evaluations or perform additional data validation before granting access. #### Integration example ```typescript import { engine, domain } from '@strapi/permissions'; import { providerFactory } from '@strapi/utils'; // 1. Define Providers const providers = { action: providerFactory(), condition: providerFactory(), }; // 2. Register Custom Conditions providers.condition.register({ name: 'isOwner', handler: (ctx) => ctx.user.id === ctx.resource.ownerId, }); // 3. Create Engine const permissionEngine = engine.new({ providers }); // 4. Define Permissions const permissions = [ domain.permission.create({ action: 'read', subject: 'article', conditions: ['isPublished'], }), domain.permission.create({ action: 'update', subject: 'article', conditions: ['isOwner'], properties: { fields: ['title', 'content'], }, }), ]; // 5. Generate Ability const ability = await permissionEngine.generateAbility(permissions); // 6. Evaluate Permission const canReadArticle = ability.can('read', 'article'); ``` ### Related - [Permission engine](./01-engine.md): hook order, engine options and the custom ability builder. - [`@strapi/admin` permissions](../admin/permissions/00-intro.mdx): the admin RBAC system that builds on this engine. - [`@strapi/core`](../core/index.md): creates the engine for the Content API permissions. - [`@strapi/utils`](../utils/index.md): provides `providerFactory` and the hook helpers that the engine uses. - [Authentication](../../../architecture/08-authentication.md): sessions and JWT, the step before an engine checks permissions. --- ## Review Workflows ### Summary The review workflow feature is only available in the Enterprise Edition. That is why, in part, it is completely decoupled from the code of the Community Edition. The purpose of this feature is to allow users to assign a tag to the various entities of their Strapi project. This tag is called a 'stage' and is available within what we will call a workflow. ### Detailed backend design The Review Workflow feature have been built with one main consideration, to be decoupled from the Community Edition. As so, the implementation can relate a lot to how a plugin would be built. All the backend code related to Review Workflow can be found in `packages/core/admin/ee`. This code is separated into several elements: - Content-types - _strapi_workflows_: `packages/core/admin/ee/server/content-types/workflow/index.js` - _strapi_workflows_stages_: `packages/core/admin/ee/server/content-types/workflow-stage/index.js` - Controllers - _workflows_: `packages/core/admin/ee/server/controllers/workflows/index.js` - _stages_: `packages/core/admin/ee/server/controllers/workflows/stages/index.js` - _assignees_: `packages/core/admin/ee/server/controllers/workflows/assignees/index.js` - Middlewares - [_DEPRECATED_] _contentTypeMiddleware_: `packages/core/admin/ee/server/middlewares/review-workflows.js` - Routes - `packages/core/admin/ee/server/routes/index.js` - Services - _review-workflows_: `packages/core/admin/ee/server/services/review-workflows/review-workflows.js` - _workflows_: `packages/core/admin/ee/server/services/review-workflows/workflows.js` - _stages_: `packages/core/admin/ee/server/services/review-workflows/stages.js` - _metrics_: `packages/core/admin/ee/server/services/review-workflows/metrics.js` - _weekly-metrics_: `packages/core/admin/ee/server/services/review-workflows/weekly-metrics.js` - _validation_: `packages/core/admin/ee/server/services/review-workflows/validation.js` - _assignees_: `packages/core/admin/ee/server/services/review-workflows/assignees.js` - _stage-permissions_: `packages/core/admin/ee/server/services/review-workflows/stage-permissions.js` - Utils file - _Review workflows utils_: `packages/core/admin/ee/server/utils/review-workflows.js` - A bootstrap and a register part - `packages/core/admin/ee/server/bootstrap.js` - `packages/core/admin/ee/server/register.js` #### Content types ##### strapi_workflows This content type stores the workflow information and is responsible for holding all the information about stages and their order. In MVP, only one workflow is stored inside the Strapi database. ##### strapi_workflows_stages This content type store the stage information such as its name. #### Controllers ##### workflows Used to interact with the `strapi_workflows` content-type. ##### stages Used to interact with the `strapi_workflows_stages` content-type. ##### assignees Used to interact with the `admin_users` content-type entities related to review workflow enabled content types. #### Middlewares ##### contentTypeMiddleware - _DEPRECATED_ In order to properly manage the options for content-type in the root level of the object, it is necessary to relocate the `reviewWorkflows` option within the `options` object located inside the content-type data. By doing so, we can ensure that all options are consistently organized and easily accessible within their respective data structures. This will also make it simpler to maintain and update the options as needed, providing a more streamlined and efficient workflow for developers working with the system. Therefore, it is recommended to move the reviewWorkflows option to its appropriate location within the options object inside the content-type data before sending it to the admin API. #### Routes The Admin API of the Enterprise Edition includes several routes related to the Review Workflow feature. Here is a list of those routes: ##### GET `/review-workflows/workflows` This route returns a list of all workflows. ##### POST `/review-workflows/workflows` This route creates a new workflow. ##### GET `/review-workflows/workflows/:id` This route returns the details of a specific workflow identified by the id parameter. ##### PUT `/review-workflows/workflows/:id` This route updates a specific workflow identified by the id parameter. ##### DELETE `/review-workflows/workflows/:id` This route deletes a specific workflow identified by the id parameter. ##### GET `/review-workflows/workflows/:workflow_id/stages` This route returns a list of all stages associated with a specific workflow identified by the workflow_id parameter. ##### GET `/review-workflows/workflows/:workflow_id/stages/:id` This route returns the details of a specific stage identified by the id parameter and associated with the workflow identified by the workflow_id parameter. ##### PUT `/review-workflows/workflows/:workflow_id/stages` This route updates the stages associated with a specific workflow identified by the workflow_id parameter. The updated stages are passed in the request body. ##### PUT `/content-manager/(collection|single)-types/:model_uid/:id/stage` This route updates the stage of a specific entity identified by the id parameter and belonging to a specific collection identified by the model_uid parameter. The new stage value is passed in the request body. ##### GET `/content-manager/(collection|single)-types/:model_uid/:id/stages` Returns a list of stages that a user has permission to transition into (based on the permission settings of a stage). ##### PUT `/content-manager/(collection|single)-types/:model_uid/:id/assignee` This route updates the assignee of the entity identified by the model_uid and id parameters. The updated entity is passed to the request body. #### Services The Review Workflow feature of the Enterprise Edition includes several services to manipulate workflows and stages. Here is a list of those services: ##### review-workflows This service is used during the bootstrap and register phases of Strapi. Its primary responsibility is to migrate data on entities as needed and add the stage field to the entity schemas. ##### workflows This service is used to manipulate the workflows entities. It provides functionalities to create, retrieve, and update workflows. ##### stages This service is used to manipulate the stages entities and to update stages on other entities. It provides functionalities to create, retrieve, update, and delete stages. ##### metrics This is the telemetry service used to gather information on the usage of this feature. It provides information on the number of workflows and stages created, as well as the frequency of stage updates on entities. ##### weekly-metrics Once a week we report on review workflows usage statistic. This service is used to set up the cron job responsible for gathering and sending statistics on: number of active workflows, average number of stages in a workflow, maximum number of stages across all workflows and the content types on which review workflows is activated. ##### assignees This service is used to interact with admin user assignee relations on review workflow enabled content types. It provides the ability to: find the Id of an entity assignee, update and delete (unassign) the assignee on an entity. ##### stage-permissions This service is used to enable RBAC functionality for review workflow stages. Each entry of the `strapi_workflows_stages` has a manyToMany relation with `admin_permissions`. The permissions held in this relation indicate which roles can change the review stage of an entry in this stage. The service provides the ability to: register and unregister new stage permissions based on stage and role Ids and to find out whether a role can transition from a given stage. ##### validation This service is used to ensure the feature is working as expected and validate the data to be valid. ### Alternatives The Review Workflow feature is currently included as a core feature within the Strapi repository. However, there has been discussion about potentially moving it to a plugin in the future. While no decision has been made on this subject yet, it is possible that it may happen at some point in the future. ### Resources - https://docs.strapi.io/user-docs/settings/review-workflows - https://docs.strapi.io/user-docs/content-type-builder/creating-new-content-type#creating-a-new-content-type - https://docs.strapi.io/user-docs/users-roles-permissions/configuring-administrator-roles#plugins-and-settings - [Content Manager Review Workflows](./03-content-manager.md) --- ## Review Workflows(Review-workflows) ### Summary There are 3 views available to manage workflows and stages: List, Edit and Create. These pages are only **accessible in enterprise mode** and if the user has the permission `admin::review-workflows.read`. Upon mount the settings pages injects themselves into the global redux store under the namespace `settings_review-workflows`. Redux is then used for all state management updates. `Formik` is used to render and validate the edit and create views. It is integrated with redux, so that all input components are controlled components. #### License limits Most licenses have feature-based usage limits configured through Chargebee. These limits are exposed to the frontend through [`useLicenseLimits`](../admin/ee/hooks/use-license-limits.md). Offline licenses do not have these limits and therefore the endpoint does not return any. For offline licenses hard-coded limits apply: max. 200 workflows, max. 200 stages per workflow. The frontend as well as the admin API restrict actions on the settings pages in various ways (more below), if the license limits are about to exceed or already exceeded. #### List view The list-view displays all available workflows. ##### License limits Displays a modal in case the user is at or above the license limit on page load. In case a user attempts to create a new workflow the same modal is shown again, in case that creation would exceed the limit. #### Edit & Create views The edit and create views allow workflows and stages to be edited (existing ones) and new workflows to be created. To enable a review-workflow on a content-type users select from a list of all content-types (collections as well as single-types) for each workflow. In case a content-type is already assigned to another content-type, the content-type will be re-assigned to the new workflow. Stages always have a color assigned to them. Colors are stored as hex-codes in the database, to avoid coupling with the design-system and developers to know about possible color-names. The admin app renders the color badges for a stage based in the matching theme-color and in case it doesn't find any the stored hex-code. It was not possible to normalize the hex-codes to be uppercase everywhere (e.g. if they are created via lifecycle methods), so the frontend does the normalization. ##### Form submission The form wrapping both views submits all stages at once, because we expect the number of stages per workflow to be rather small. Because of this we can simply re-order stages by sending them in a different order. Every stage that sends a corresponding `id` attribute will be re-ordered and not created. Stages without an `id` property will be created in the database on submission. Stages that existed already, but are not submitted again will be deleted on submission. When editing a workflow and the `permissions` of a stage have not been modified by a user, they are sent as `undefined`. This special case allows users to edit a workflow without having to have permissions to read `roles`, because the API won't update anything in the database. Users without read permissions for roles are able to create workflows, but they will not be able to define which roles can change a stage, so that stages of that workflow can only be changed by the super-admin unless the roles are set. #### Edit view Displays a license-limits modal in case the user is above the license limit on page load and upon form submission. ##### Deletion of a stage If a stage is deleted, all **entities which are connected to that stage are moved to the previous stage**. Because a stage deletion might have big effects on the database, a confirmation is shown before users save changes. Changes are only applied once the user clicks "Save". It is not possible to remove all stages from a workflow (neither in the UI nor the API). ##### Create view Displays a license-limit modal in case the user is at or above the license limit on page load and upon form submission. #### Hooks ##### `useReviewWorkflows(queryParams)` This hook allows to fetch either one (if `params` contains an `id`) or all workflows. By default stages are populated. The hooks returns a partial react-query result. ```ts useReviewWorkflows(queryParams: object): { meta: { workflowCount: number } workflows: Workflow[], isLoading: boolean, status: string, refetch: () => Promise, } ``` ##### `useReviewWorkflowsStages({ id, layout }, reactQueryOptions)` This hook allows to fetch stages for a given workflow, that a user has permissions to transition to. ```ts type ContentTypeLayout { uid: string; kind: 'collectionType' | 'singleType'; } useReviewWorkflowsStages({ id: number, layout: ContentTypeLayout }, reactQueryOptions): { meta: { workflowCount: number, stagesCount: number } stages: Stage[], isLoading: boolean, refetch: () => Promise, } ``` #### Data shapes ```ts type Stage { id: number; color: string; // hex code name: string; // max-length: 255 characters permissions: Permission[]; createdAt: Date; updatedAt: Date; } type Workflow { id: number; name: string; // max-length: 255 characters, unique contentTypes: number[]; stages: Stage[]; createdAt: Date; updatedAt: Date; } ``` #### Endpoints For a list of all available endpoints please refer to the [detailed backend design documentation](./01-backend.md). --- ## Review Workflows(3) ### Summary Review workflows are disabled for all content-types by default and have to be enabled for each of them individually. More about how to enable review-workflows for a content-type. The feature itself is visible in two places of the content-manager: list and edit view. #### Nullish stages Entities which are not created through the admin API, content API or entity service will not have a stage assigned by default (e.g. lifecycle methods). This means at any place in the admin app displaying a stage, it has to be prepared to receive `null` and should not crash. #### List view If the feature is enabled for a content-type a new column will show up, displaying the current stage of an entity. If no stage was assigned to an entity, the column is displayed as empty. The information which stage is currently assigned to an entity is sent as part of the content-type response payload for each entity in the attribute `strapi_stage`. Please refer to the [type definitions](./02-settings.md) for more information. ```ts { // ... entity attributes strapi_stage?: Stage | null } ``` `http://localhost:1337/content-manager/content-types` returns whether the feature is enabled for the content-type. `options.reviewWorkflows` is either `true`, `false` or `undefined`. **Note**: Downgrading from EE to CE won't delete the associated review workflow data and `http://localhost:1337/content-manager/content-types` still returns true. The admin app had to add an additional check if the feature toggle returned in `http://localhost:1337/admin/project-type` is enabled. #### Edit view If the feature is enabled on the current content-type, the selected stage will show up in the information sidebar next to the edit view. Users can select any other stage of the current workflow provided they have the necessary permissions to change the current stage see [EE Review Workflows](./01-backend.md). Stage assignments are decoupled from entities, meaning updating entity attributes won't set the selected stage at the same time. Instead the stage select component will trigger an atomic update on the admin API to assign/ update a stage to the current entity. Because of this decoupling, stages **can not be assigned on entity creation** and only after they have been created. Information about which stage is currently assigned to an entity is sent as part of the entity response payload in the attribute `strapi_stage`. Please refer to the [type definitions](./02-settings.md) for more information. ```ts { // ... entity attributes strapi_stage?: Stage | null } ``` ##### License limits If the user has exceeded the license limit (based on their license) for either the number of workflows or the number of stages of the workflow assigned to the entities content-type, any stage update action will trigger a license-limit overlay and abort the request. --- ## @strapi/review-workflows ### Purpose `@strapi/review-workflows` lets a team track the review state of content. An administrator defines workflows. A workflow has ordered stages, for example "To do" or "Reviewed", and content types are assigned to it. Each entry of such a content type has a stage and an optional assignee. The package is an Enterprise Edition plugin. It has a server part (`strapi-server`) and an admin part (`strapi-admin`). The plugin loader always enables it, and the EE license decides whether it is active. `@strapi/strapi` depends on it and bundles its admin part. Application developers do not import from it. ### Key concepts #### Enterprise gating `server/src/index.ts` returns the full plugin only when `strapi.ee.features.isEnabled('review-workflows')` is true. Otherwise it returns only the content types, so that the data survives a downgrade. The admin routes use `enableFeatureMiddleware('review-workflows')`. The admin part registers its UI only when `window.strapi.features.isEnabled('review-workflows')` is true. See [Enterprise Edition](../../../architecture/09-enterprise-edition.md). #### Workflow and stage content types `plugin::review-workflows.workflow` has a unique `name`, its `stages`, an optional `stageRequiredToPublish` and a JSON list `contentTypes`. `plugin::review-workflows.workflow-stage` has a `name`, a `color`, its `workflow` and a many-to-many relation `permissions` to `admin::permission`. Both are hidden from the content manager. On the first boot, `bootstrap` creates a workflow named "Default" with four stages when the database has none. #### Stage and assignee attributes In `register`, `extendReviewWorkflowContentTypes` adds two one-to-one relations to every content type that is visible in the content manager: `strapi_stage` and `strapi_assignee`. The relations use a join table. An `afterSync` hook persists these join tables, so that the data survives a downgrade to the Community Edition. See [Extension points](../../../architecture/04-extension-points.md). #### Document Service middlewares Three middlewares in `services/document-service-middleware.ts` connect the workflow to the write path. `assignStageOnCreate` sets the first stage on `create` and `clone`. `handleStageOnUpdate` emits the `review-workflows.updateEntryStage` event when the stage changes. `checkStageBeforePublish` rejects `publish` when the entry is not at `stageRequiredToPublish`. See [Document write path](../../../architecture/05-document-write-path.md). #### Stage permissions A role can change the stage of an entry only with the `admin::review-workflows.stage.transition` permission. The permission has a parameter: `to` for the target stage, or `from` for the source stage. The `stage-permissions` service writes these parametrized permissions to roles and checks them. The super admin role always passes the check. The settings pages use the actions `admin::review-workflows.create`, `read`, `update` and `delete`. See [`@strapi/permissions`](../permissions/index.md). #### Workflow assignment and limits The `workflows` service creates and updates workflows. The `workflow-content-types` service moves the entries of a content type to a stage when the content type changes workflow, and updates the content manager configuration. The `validation` service applies the limits `numberOfWorkflows` and `stagesPerWorkflow`. The license feature options set them, and the default is 200 each. #### Admin part The admin entry adds the Review Workflows settings page, a column and a filter in the content manager list view, a side panel with the stage and assignee selects in the edit view, and the "Assigned to me" homepage widget. See [Settings](./02-settings.md), [Content manager](./03-content-manager.md) and [Backend](./01-backend.md). ### Related - [Enterprise Edition](../../../architecture/09-enterprise-edition.md): how the license gates this plugin. - [Schema sync](../../../architecture/07-schema-sync.md): the `beforeSync` and `afterSync` hooks. This package adds `afterSync` handlers. - [Document write path](../../../architecture/05-document-write-path.md): where the three middlewares run. - [`@strapi/permissions`](../permissions/index.md): the parametrized actions that stage transitions use. - [`@strapi/admin`](../admin/index.md): admin users, roles and permissions that the stages rely on. - [`@strapi/content-manager`](../content-manager/index.md): hosts the list view hooks and the edit view panel. --- ## Overview(Commands) The CLI is largely contained to the `@strapi/strapi` package, however some packages are able to inject their own commands: - [`@strapi/data-transfer`](../../data-transfer/index.md) ### Structure of command The CLI is built with `commander` and as such every command we create can be described as: ```ts import { createCommand, Command } from 'commander'; type StrapiCommand = (params: { command: Command; argv: string[]; ctx: CLIContext }) => Command; // usage const myCommand: StrapiCommand = ({ argv, ctx }) => { // do something return createCommand('develop') .alias('dev') .option( '--no-build', '[deprecated]: there is middleware for the server, it is no longer a separate process' ) .action((options) => { // do something with options & ctx }); }; ``` These actions are then combined together in the `@strapi/strapi` package to be used as a complete CLI, the context provided to each command supplies a number of useful properties: ```ts interface CLIContext { cwd: string; logger: Logger; tsconfig?: TsConfig; } ``` #### `logger` ```ts interface Logger { warnings: number; errors: number; debug: (...args: unknown[]) => void; info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; log: (...args: unknown[]) => void; spinner: (text: string) => Pick; } ``` The shared logger means that any command can accept `--debug` and `--silent` flags to control the output of the CLI. Due to incorporating `ora` into the logger, we can also provide a spinner for long running tasks but more importantly, easily silence them when `--silent` is passed. #### `tsconfig` If `tsconfig` is not _defined_ we can conclude that the project is not a TS project. However, if it is we have access to where the tsconfig is located as well as the parsed config itself. ```ts interface TsConfig { config: ts.ParsedCommandLine; path: string; } ``` --- ## Build The `build` command is used to build the strapi admin panel as a SPA ready to be served by the strapi server. ### Usage ```bash strapi build ``` #### Options ```bash Build the strapi admin app Options: --bundler [bundler] Bundler to use (webpack or vite) (default: "vite") -d, --debug Enable debugging mode with verbose logs (default: false) --minify Minify the output (default: true) --silent Don't log anything (default: false) --sourcemap Produce sourcemaps (default: false) --stats Print build statistics to the console (default: false) --install-deps Auto-install missing admin dependencies (default: false) -h, --help display help for command ``` ### How it works The build process for the admin panel is designed to be bundler agnostic, this means we can easily experiment and perhaps transition to new bundlers as they become available in the ecosystem. This is facilitated by the use of a [`BuildContext`](#buildcontext) that contains all the information needed to build the admin panel – if it's found more information is required this context can be tweaked to provide it. #### Dependencies The first step of running the build command is to check if the required dependencies are installed at the root of the project. This provides better DX for: - miss-installed project - monorepos - incorrect/incompatible versions of packages for _certain_ packages like `styled-components` or `react`. The list of packages we explicitly check for are: - `react` - `react-dom` - `styled-components` - `react-router-dom` This is because there should only be one instance of these packages installed and used by the project at any one time, failure to do so can and most likely will, lead to bugs. This also means an incompatible version of these packages could cause unintended side effects e.g. if `react@19` was suddenly released but we had not tested it against the admin panel. We run a prompt to encourage the user to install these deps – however, this functionality has not yet been built. #### BuildContext The build context is the heart of how the admin builds, as said above it's agnostic, it doesn't care if we're using webpack or vite or parcel. It's an object of data that can be used to prepare any bundler. It's shape looks like: ```ts interface BuildContext { /** * The absolute path to the app directory defined by the Strapi instance */ appDir: string; /** * If a user is deploying the project under a nested public path, we use * this path so all asset paths will be rewritten accordingly */ basePath: string; /** * internal path to serve the admin panel */ adminPath: string; /** * The bundler to use for building & watching */ bundler: 'webpack' | 'vite'; /** * The customisations defined by the user in their app.js file */ customisations?: AppFile; /** * The current working directory */ cwd: string; /** * The absolute path to the dist directory */ distPath: string; /** * The relative path to the dist directory */ distDir: string; /** * The absolute path to the entry file */ entry: string; /** * The environment variables to be included in the JS bundle */ env: Record; /** * Features object with future flags */ features?: Modules.Features.FeaturesService['config']; logger: CLIContext['logger']; /** * The build options */ options: Pick & Pick; /** * The plugins to be included in the JS bundle * incl. internal plugins, third party plugins & local plugins */ plugins: PluginMeta[]; /** * The absolute path to the runtime directory */ runtimeDir: string; /** * The Strapi instance */ strapi: Core.Strapi; /** * The browserslist target either loaded from the user's workspace or falling back to the default */ target: string[]; tsconfig?: CLIContext['tsconfig']; } ``` #### Static Files The next step is to create a `runtime` folder in the root of the strapi project, a generic name `.strapi` is used and the build specifically uses a subfolder called `client`. This leaves more space for us to expand as and when we require it. We only generate two files for this – an `index.html` which is a static rendered React Component from the `@strapi/admin` package (DefaultDocument) & an `app.js` file which calls the `renderAdmin` function & provides a mount point & plugin object. #### Bundling We currently support both `webpack` & `vite` bundlers, with `vite` being the default. Because there is no global `strapi.config` file we don't have an already existing API to pass your own bundler. In the future we may decide to do this if there is a need. Each bundler supplies a build function & a develop function. We don't need a serve function because they're expected to produce the same `index.html` output defined by the static files step described above. ### Node Usage ```ts // internal to @strapi/strapi – the CLI command imports it directly import { build, type BuildOptions } from '../../node/build'; const args: BuildOptions = { // ... }; await build(args); ``` #### Options ```ts interface BuildOptions extends CLIContext { /** * The directory to build the command was ran from */ cwd: string; /** * The logger to use. */ logger: Logger; /** * Which bundler to use for building. * * @default vite */ bundler?: 'webpack' | 'vite'; /** * Minify the output * * @default true */ minify?: boolean; /** * Generate sourcemaps – useful for debugging bugs in the admin panel UI. */ sourcemap?: boolean; /** * Print stats for build */ stats?: boolean; /** * Auto-install missing admin dependencies * * @default false */ installDeps?: boolean; /** * The tsconfig to use for the build. If undefined, this is not a TS project. */ tsconfig?: TsConfig; } interface Logger { warnings: number; errors: number; debug: (...args: unknown[]) => void; info: (...args: unknown[]) => void; success: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; log: (...args: unknown[]) => void; spinner: (text: string) => Pick; progressBar: ( totalSize: number, text: string ) => Pick; } interface TsConfig { config: ts.ParsedCommandLine; path: string; } ``` --- ## Develop The `develop` command is used to develop your strapi node application e.g. creating content-types in the `content-type-builder` as well as "watching" the admin panel with webpack for local development whether in the strapi monorepo or in a standalone project. ### Usage ```bash strapi develop ``` #### Options ```bash Start your Strapi application in development mode Options: --polling Watch for file changes in network directories (default: false) --watch-admin Watch the admin panel for hot changes --open Open the admin in your browser (default: true) -h, --help Display help for command ``` ### How it works The develop command sets itself up much like the [build](build) command. Once we've injected our middlewares, we load the strapi instance and then generate the types based off the user's instance as well as potentially compiling any TS server code if we're in a TS project. The final step is to watch the project directory so we can restart the strapi instance in real-time as a user is developing their project & being the actual strapi instance. ### Node Usage ```ts import { develop, DevelopOptions } from '@strapi/admin/_internal'; const args: DevelopOptions = { // ... }; await develop(args); ``` #### Options ```ts interface DevelopOptions extends CLIContext { /** * The directory to build the command was ran from */ cwd: string; /** * The logger to use. */ logger: Logger; /** * Whether or not to open the browser after the build is complete. */ open?: boolean; /** * Watch for file changes in network directories */ polling?: boolean; /** * The tsconfig to use for the build. If undefined, this is not a TS project. */ tsconfig?: TsConfig; /** * Watch the admin for changes */ watchAdmin?: boolean; } interface Logger { warnings: number; errors: number; debug: (...args: unknown[]) => void; info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; log: (...args: unknown[]) => void; spinner: (text: string) => Pick; } interface TsConfig { config: ts.ParsedCommandLine; path: string; } ``` --- ## @strapi/strapi ### Purpose `@strapi/strapi` is the package that a Strapi application installs. It gives the application the `strapi` command, the build tooling for the admin panel, and the public exports of the framework. It runs in Node.js for the CLI and the build. The `@strapi/strapi/admin` export runs in the browser. `@strapi/core` holds the `Strapi` class. This package does not: it re-exports `@strapi/core` and the types of `@strapi/types`, and it depends on the core packages and plugins that make up a default installation. ### Key concepts #### Entry exports The `exports` field of `package.json` has four entries. - `.` (`src/index.ts`) re-exports everything from `@strapi/core` and the types from `@strapi/types`. - `./admin` (`src/admin.ts`) exports `renderAdmin`, which mounts the admin application with the built-in admin plugins and the plugins of the application. It also exports hooks and helpers of `@strapi/admin` and `@strapi/content-manager`. - `./admin/test` re-exports the test helpers of `@strapi/admin`. - `./admin/styles.css` is the base stylesheet of the admin panel. #### The `strapi` binary `bin/strapi.js` calls `runCLI` from `src/cli`. The CLI uses `commander`. Each command is a `StrapiCommand` factory: it receives `{ command, argv, ctx }` and returns a `Command`. The list is in `src/cli/commands/index.ts`. The context `ctx` holds `cwd`, a `logger` and a lazy `tsconfig`. The `logger` honors the `--debug` and `--silent` flags. See [CLI commands](./commands/00-overview.md). #### Commands from other packages Some commands delegate their work. `export`, `import` and `transfer` use `@strapi/data-transfer`. `generate` starts `@strapi/generators`. `ts:generate-types` uses `@strapi/typescript-utils`. The cloud commands come from `@strapi/cloud-cli`. `openapi generate` calls `@strapi/openapi`. #### Admin build tooling `src/node` builds the admin panel of an application. `createBuildContext` reads the application and lists the plugins for the bundle. A module plugin is a dependency whose `package.json` has `strapi.kind: 'plugin'`. A local plugin is declared in `config/plugins` with `enabled` and `resolve`. `writeStaticClientFiles` then writes a generated entry module that imports `renderAdmin` from `@strapi/strapi/admin`. The bundle is built with Vite by default. Webpack is still available with `--bundler webpack` and logs a deprecation warning. See [build](./commands/01-build.md). #### Development mode `strapi develop` uses the Node `cluster` module. The primary process compiles TypeScript and forks a worker. The worker creates the Strapi instance with `autoReload` on, starts the admin watcher and watches the project files. A file change sends `reload` to the primary process, which recompiles and restarts the worker. See [develop](./commands/02-develop.md). ### Related - [Architecture overview](../../../architecture/index.md): the server and admin split, and the `strapi-server` and `strapi-admin` entry points. - [Package map](../../../architecture/01-package-map.mdx): where this package sits between the application and the core packages. - [Server lifecycle](../../../architecture/02-server-lifecycle.md): the `start`, `console` and `develop` commands create and load the instance. - [`@strapi/core`](../core/index.md): defines the `Strapi` class that this package re-exports. - [`@strapi/types`](../types/index.md): the types that this package re-exports. - [`@strapi/admin`](../admin/index.md): the React application that `renderAdmin` mounts. - [`@strapi/data-transfer`](../data-transfer/index.md): provides the export, import and transfer engine. - [`@strapi/openapi`](../openapi/index.md): generates the document for `strapi openapi generate`. - [`@strapi/cloud-cli`](../../cli/cloud/index.md): provides the cloud commands. --- ## @strapi/types ### Purpose `@strapi/types` is the single source of truth for the TypeScript types that the Strapi packages share. It has no runtime logic. The server packages use it to type the `Strapi` instance, the container, plugins, services, controllers, schemas and the Document Service. The package README marks it as internal. Application developers import the same types from `@strapi/strapi`, which re-exports `@strapi/types` with `export type *`. The type system is built to adapt to the schemas of one application, and the [Type System](./type-system/index.mdx) pages explain how. ### Key concepts #### Namespaces `src/index.ts` exports each area as a type namespace. - `Core`: the server runtime types. It holds `Strapi`, the container, controllers, services, policies, middlewares, routes, plugins and configuration. - `Modules`: the types of the core services, for example the Document Service, event hub, cron, core store, server and session manager. - `Data`: the shape of entities, content types and components as data. - `Struct`: the base schema types and the schema definitions. - `Schema`: the high-level schema types and `Schema.Attribute`. They read the public registries. - `UID`: the UID types. They read the public registries. - `Internal`: the low-level `Namespace`, `UID` and `Registry` types. They do not depend on an application. - `Public`: the public registries that applications augment. - `Plugin`: the `LoadedPlugin` shape, the `IsEnabled` helper and the `Config` types for the `strapi-server` and `strapi-admin` entry points. - `Utils`: type helpers, such as `Get`, `If` and the `Object`, `String` and `Guard` groups. #### Schema A schema describes the structure of a content type or a component. The base `Schema` type has a `modelType` discriminant (`contentType` or `component`). A content type schema has a `kind` (`collectionType` or `singleType`). A schema type describes a loaded schema, not a raw definition or a database model. See [Schema](./type-system/02-concepts/01-schema.mdx). #### UID A UID is a string literal type that identifies a resource, for example `api::article.article`, `plugin::upload.file`, `admin::user` or `default.seo`. Different kinds of resources can share a UID string, and TypeScript cannot tell them apart. See [UID](./type-system/02-concepts/02-uid.md). #### Public registries A public registry is an interface with an index signature, for example `ContentTypeSchemas`, `ComponentSchemas`, `Services`, `Controllers`, `Policies` and `Middlewares` in `src/public/registries.ts`. An application adds its own entries with module augmentation, and Strapi types then resolve to the application schemas. The `strapi ts:generate-types` command writes these augmentations. When a registry is empty, the types fall back to their generic form. See [Public registries](./type-system/02-concepts/03-public-registry.mdx). #### Global declaration `src/index.ts` also declares a global `strapi` variable of type `Strapi`. This matches the `global.strapi` assignment in `createStrapi()`. New code must receive `strapi` as a parameter. #### Working with the types Read the [philosophy](./type-system/01-philosophy.md) before you change a type, because a change can slow down TypeScript for every application. The [cheatsheet](./type-system/03-cheatsheet.mdx) lists common patterns. ### Related - [Container and registries](../../../architecture/03-container-and-registries.md): the runtime side of UIDs and namespaces. - [Glossary](../../../architecture/11-glossary.md): the UID and namespace terms. - [`@strapi/strapi`](../strapi/index.md): re-exports these types to applications. - [`@strapi/core`](../core/index.md): implements the `Core.Strapi` interface. - [`@strapi/utils`](../utils/index.md): the runtime helpers that go with the shared types. - [`@strapi/typescript-utils`](../../utils/typescript/index.md): generates the application types that augment the public registries. --- ## Philosophy #### Context Since its release, Strapi's strength has lain in its **flexibility** and **extensibility**. It can handle a wide array of content and customizations, from content-types to plugins and extensions, most of them are defined dynamically in user applications. With TypeScript imposing itself as a standard in the JS ecosystem over the years, it has become critical to develop a uniform system that streamlines this heavily customized content management's developer experience. It is in this context that a type system becomes an essential tool, as it provides a single source of truth for standardized data structures, like schema, attributes, and entities, whether they've been statically or dynamically created. On top of this, it offers a wide range of utilities to manipulate those types, and creates a unified, typed developer experience across the whole codebase. #### Application ##### For Contributors One of its key advantages is the plethora of utilities it provides, which simplify the manipulation of complex data structures, including content types, documents, and UIDs. Another significant advantage is the consistency it enforces within the codebase. This is achieved by defining shared types once and reusing them throughout the codebase. Moreover, the type system can effectively handle high levels of customization, thus allowing developers to focus their efforts on feature development rather than TypeScript headaches. ##### For Users Although Strapi users don't often engage directly with the type system, it nonetheless profoundly influences their Strapi experience. It ensures the provision of type-safe APIs (such as the document service), which greatly reduces runtime errors, offers a simple way of discovering APIs via autocompletion and code hinting, and makes the system more reliable overall. Moreover, it arms users with ready-made types, which they can use to type-proof their applications or customizations. Lastly, the type system also provides users with type-manipulation utilities. #### Challenges The main challenge faced by the type system is being able to maintain a high-quality TypeScript developer experience for different contexts, each with their own nuances. ##### For Contributors Contributors expect to create and/or use APIs that handle very generic data structures in the context of Strapi internals, which become context-aware and strongly typed when used by a user within their application. ##### For Users Users expects to have a TypeScript experience tailored around their own application, with their content-types, components, and plugins being strongly typed and recognized by the Strapi APIs. :::warning Creating such experiences without slowing down developers (both contributors and users) is a huge undertaking and should be considered a top priority every time the type system is modified. ::: #### Key Principles ##### 🧩 Flexibility and Extensibility Strapi's strength lies in its ability to handle a wide range of content and customizations, the types should reflect and adapt to that. ##### 📏 Uniformity The goal of the type system is to provide a consistent and smooth TypeScript developer experience for both users' applications and Strapi internals. ##### 🧰 Fully-Featured The type system exposes utilities that enable the manipulation of complex data structures, regardless of potential heavy customization involved. ##### 🪡 Tailor-made Experience The type system should provide a TypeScript experience that is tailored to each application, while keeping a generic stance within Strapi internals. ##### 🏖️ Easy to Use The type system should prioritize not slowing down developers, whether contributors or users. Any complexity should be handled by the type system internals. --- ## Schema The schema is the primary data structure leveraged within the Strapi Type System, defining how content is structured and managed in the application. It serves several key functions: - **Representation**: At its core, a schema outlines and defines the structure of Strapi content. This is useful when dealing with features that need access to low level schema properties (_e.g. attributes, plugin options, etc..._). - **Inference**: The schema allows inferring and configuring numerous other types. This includes entities like `ContentType` or `Component`, among others. #### Scope Schema types represent **loaded** schemas in the context of a Strapi server application and should be used accordingly. :::caution Database models and raw schema definitions (_aka schemas before being loaded by the Strapi server_) are **not** the same types and can't be used interchangeably. ::: #### Sub-Types Each box is a type that extends the base Schema interface. In between each box is text that represents the discriminant used to differentiate the subtype from others. ```mermaid flowchart TB; Schema -- "modelType: contentType" ---- ContentTypeSchema Schema -- "modelType: component" ---- ComponentSchema ContentTypeSchema -- "kind: collectionType" ---- CollectionTypeSchema ContentTypeSchema -- "kind: singleType" ---- SingleTypeSchema ``` #### Properties Schema types contain useful information that helps other types know how to interact with the Strapi content. This is facilitated through multiple properties. ##### Options A set of properties used to configure the schema. It contains information on features activation among other things. This can be really useful to make the types adapt to a given schema. For instance, the document service uses the `options.draftAndPublish` property to determine whether it should add publication methods to the service type. ##### Plugin Options These options provide the ability to alter or enhance the behaviour of the system based on specific values. If a plugin is enabled, it might bring functionality that can affect how types interact with each other. For example, it's possible to add or remove certain entity-service filters from the query type based on whether a plugin is enabled. ##### Attributes Strongly typed schema attributes allows the Type System to infer actual entities types based on their properties. For instance, a string attribute will resolve to a primitive string in an entity, whereas a repeatable component attribute will resolve to an array of objects. #### Usage import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; **Public** When designing public APIs (and in most other scenarios), it's advised to use the high-level schema types found in the `Schema` namespace. Schema definitions exported from the `Schema` namespace are targeting the dynamic types found in the public schema registries, and will dynamically adapt to the current context while extending the base Schema types. :::info If the public registries are empty (_e.g. types are not generated yet, not in the context of a Strapi application, ..._), schema types will fallback to their low-level definitions. ::: ```typescript import type { Schema } from '@strapi/strapi'; declare const schema: Schema.Schema; declare const contentType: Schema.ContentType; declare const component: Schema.Component; declare function processAnySchema(schema: Schema.Schema): void; processAnySchema(schema); // ✅ processAnySchema(contentType); // ✅ processAnySchema(component); // ✅ declare function processContentTypeSchema(schema: Schema.ContentType): void; processContentTypeSchema(schema); // ✅ processContentTypeSchema(contentType); // ✅ processContentTypeSchema(component); // ❌ Error, a component schema is not assignable to a content-type schema declare function processComponentSchema(schema: Schema.Component): void; processComponentSchema(schema); // ✅ processComponentSchema(contentType); // ❌ Error, a content-type schema is not assignable to a component schema processComponentSchema(component); // ✅ ``` **Internal** Schema definitions exported from the `Struct` namespace defines the low level type representation of Strapi schemas. :::caution Those types can be useful when you want to validate other types against the base ones, but realistically, the public Schema types should almost always be preferred. ::: ```typescript import type { Struct } from '@strapi/strapi'; declare const schema: Struct.Schema; declare const contentType: Struct.ContentTypeSchema; declare const component: Struct.ComponentSchema; declare function processAnySchema(schema: Struct.Schema): void; processAnySchema(schema); // ✅ processAnySchema(contentType); // ✅ processAnySchema(component); // ✅ declare function processContentTypeSchema(schema: Struct.ContentTypeSchema): void; processContentTypeSchema(schema); // ✅ processContentTypeSchema(contentType); // ✅ processContentTypeSchema(component); // ❌ Error, a component schema is not assignable to a content-type schema declare function processComponentSchema(schema: Struct.ComponentSchema): void; processComponentSchema(schema); // ✅ processComponentSchema(contentType); // ❌ Error, a content-type schema is not assignable to a component schema processComponentSchema(component); // ✅ ``` --- ## UID :::note On this page, **a resource** is considered as **anything that can be identified by a UID**. This includes (but is not limited to) controllers, schema, services, policies, middlewares, etc... ::: In the Type System, UIDs play a crucial role in referencing various resources (such as schema and entities) by attaching a unique identifier. To put it simply, a UID is a unique (string) literal key used to identify, locate, or access a particular resource within the system. :::tip This makes it the perfect tool to index type registries or to use as a type parameter for resource-centric types. ::: #### Format A UID is composed of 3 different parts: 1. A namespace ([link](#1-namespaces)) 2. A separator ([link](#2-separators)) 3. A name ([link](#3-names)) ##### 1. Namespaces There are two main families of namespaces: - Scoped (_aka parametrized_) - Non-scoped (_aka constants_) A third kind exists for component UIDs and is defined only by a dynamic category: ``. ###### Scoped Scoped namespaces are defined by a base name, followed by a separator (`::`) and any string. In Strapi there are two of them: | Name | Definition | Description | | ------ | :---------------: | ---------------------------------------------------- | | API | `api::` | Represent a resource present in the `` API | | Plugin | `plugin::` | Represent a resource present in the `` plugin | ###### Non-Scoped These namespaces are used as a simple prefix and define the origin of a resource. Strapi uses three of them to create UIDs | Name | Definition | Description | | ------ | :--------: | ----------------------------------------------------------------------------- | | Strapi | `strapi` | Represent a resource present in the core of strapi | | Admin | `admin` | Represent a resource present in Strapi admin | | Global | `global` | Rarely used (_e.g. policies or middlewares_), it represents a global resource | ##### 2. Separators There are only two kind of separators: - `.` for scoped namespaces (`api::`, `plugin::`) and components (``) - `::` for others (`admin`, `strapi`, `global`) ##### 3. Names UID names can be any alphanumeric string. :::caution A UID is unique for the kind of resource it's attached to, but **different resource can share the same UID**. For instance, it's completely possible to have both a `service` and a `schema` identified by `api::article.article`. Since **TypeScript is a structural type system**, it means that **different UIDs resolving to the same literal type can match each other**, thus making it possible to send a service UID to a method expecting a schema UID (if they share the same format). ::: #### Compatibility Table The following table shows, for each kind of UID, what resource they can be associated with. :::note ContentType and Component are referring to both the related schema and entity. ::: | | ContentType | Component | Middleware | Policy | Controller | Service | | ------------------------ | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | | `api::.` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `plugin::.` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `.` | :x: | :white_check_mark: | :x: | :x: | :x: | :x: | | `strapi::` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | | `admin::` | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | `global::` | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | #### Usage When referencing resource by their UID you'll need to use the `UID` namespace exported from `@strapi/types`. ```typescript import type { UID } from '@strapi/types'; ``` This namespace contains shortcuts to dynamic UID types built from the public registries so that they always adapt to the current context. :::danger The `UID` namespace is designed to be the main interface used by developers. Do not use the `Internal.UID` namespace unless you know what you're doing (low level extends clause, isolated internal code, etc...). ::: ##### Basic Example A common usage is to declare a function that takes a UID as a parameter. For our example, let's imagine we want to fetch an entity based on the provided resource UID. ```typescript import type { UID, Data } from '@strapi/types'; declare function fetch(uid: UID.ContentType): Data.ContentType; ``` :::tip To find an exhaustive list of available UID types, take a look at the [related API reference](http://foo) ::: ##### Parameter Type Inference Now let's say we want to adapt the return type of our function, so that it matches the given UID. ```typescript fetch('api::article.article'); // ^ this should return a Data.Entity<'api::article.article'> fetch('admin::user'); // ^ this should return a Data.Entity<'admin::user'> ``` To do that, we'll need the function to be able to provide us with the current `uid` type based on usage. ```typescript import type { UID, Data } from '@strapi/types'; declare function fetch(uid: T): Data.ContentType; ``` So what's changed here? 1. We've forced the `uid` type to be inferred upon usage and stored in a type variable called `T`. 2. We've then re-used `T` to parametrize the `Data.ContentType` type. `fetch` will now always return the correct entity depending on which `UID` is sent. :::caution When writing actual code, avoid using `T` as a type variable, and always use meaningful names that will help other developers understand what the variable represents. For instance, in our example we could use `TContentTypeUID` instead of just `T`. ::: ##### Going Further It's completely possible to reference `T` in other generic parameters. Let's add the possibility to select which fields we want to return for our entity. ```typescript import type { UID, Data, Schema } from '@strapi/types'; declare function fetch>( uid: T, fields: F[] ): Data.ContentType; ``` :::tip You may have noticed that we're using the inferred UID type (`T`) to reference both: - An entity (`Data.Entity`) - A schema (`Schema.AttributeNames`) This is because they share the same format and can be used interchangeably. For more information, take a look at the [format](#format) and [compatibility table](#compatibility-table) sections. ::: --- ## Public Registries import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; #### Context ##### Why? The Strapi Type System is designed to provide developers with a fully customizable experience. It's engineered to adapt and modify its type definitions automatically according to the context of each application and associated resources. This adaptability extends to various components such as schemas, services, controllers, and more. :::note See the [type system principles](../philosophy#key-principles) page for more information about the mission. ::: ##### How? To meet this requirement, the Type System employs "public registries". In simple terms, public registries are basic indexed interface definitions that are made publicly available by the types package. ```typescript export interface MyRegistry { [key: string]: GenericResourceDefinition; } ``` Every resource comes with its own registry that has a set of rules acting as the default state (the index). Because every registry can be augmented (publicly exported), developers have the freedom to inject their own definitions. Doing so allows Strapi APIs to respond in a way that aligns with the developers' custom definitions. :::info[Did you know?] The `Schema` and `UID` namespaces rely solely on public registries to infer their type definitions. This is why it's heavily encouraged to use them over their low level counter-parts. ::: #### How it works ##### Architecture ```mermaid flowchart TB; %% Main node ContentTypeRegistry("Content-Types Registry") %% App subgraph Application %% Nodes UserApp["User Application"] UserSchema(["Application Types"]) %% Links UserApp -- "generates" ---> UserSchema end %% Type System subgraph TypesPackage["@strapi/types"] %% Nodes TypeSystem["Type System"] Types{{Types}} Registries{{Registries}} UID{{UID}} Schema{{Schema}} %% Links TypeSystem -- "exports" ---> Types & Registries Types -- "exports" ---> UID & Schema -- "uses" ---> ContentTypeRegistry Registries -- "exports" ----> ContentTypeRegistry end %% Strapi subgraph StrapiPackage["@strapi/strapi"] %% Nodes Strapi["Strapi"] APIs[["APIs"]] %% Links Strapi -- "exports" --> APIs APIs -- "uses" --> Types end %% This link needs to be placed last to preserve the layout UserSchema -- "augments" -----> ContentTypeRegistry ``` ##### Usage ###### 1. Registry Definition Creating a new registry is as simple as exporting an indexed interface from the right namespace. Let's declare the content-type schema registry. It should accept: - Content-type UIDs as keys - Content-type schemas as values ```ts title="@strapi/types/public/registries.ts" import type { Internal, Struct } from '@strapi/types'; export interface ContentTypeSchemas { [key: Internal.UID.ContentType]: Struct.ContentTypeSchema; } ``` :::note We use low level types to define our index (`Internal`/`Struct`) to keep it as generic as possible. ::: ###### 2. Dynamic Type Definitions ###### UID.ContentType To define `UID.ContentType`, we extract every key (`Internal.Registry.Keys`) from the public content-type registry (`Public.ContentTypeSchemas`) that matches with the base definition of a content-type UID (`Internal.UID.ContentType`). ```ts title="@strapi/types/uid.ts" import type { Internal, Public } from '@strapi/types'; export type ContentType = Internal.Registry.Keys< Public.ContentTypeSchemas, Internal.UID.ContentType >; ``` :::note Only selecting keys that extend `Internal.UID.ContentType` ensures we don't end up with manually added malformed keys, and tells the type-checker we're confident about what's in our union type. ::: ###### Schema.ContentType To declare `Schema.ContentType`, we simply query the content-type schema registry (`Public.ContentTypeSchemas`) with the provided content-type UID (`TUID`). :::note Since `UID.ContentType` (`TUID`) is [dynamically built based on actual keys](#uidcontenttype), we know for sure that there will be a valid corresponding schema in the registry. ::: ```ts title="@strapi/types/schema.ts" import type { UID, Public } from '@strapi/types'; export type ContentType = Public.ContentTypeSchemas[TUID]; ``` ###### 3. API Design To create types for a dynamic API (_aka one that reacts to its context_), simply use dynamic type definitions such as `UID`, `Data` or `Schema`. ```ts title="@strapi/core/document-service.ts" import type { Data, UID } from '@strapi/types'; export type findOne(uid: TUID): Data.ContentType; ``` :::caution Remember to use dynamic type definitions (`UID`, `Data`, `Schema`) and not static ones (`Internal`, `Struct`). ::: :::info[Reminder] Registries are **indexed**, which means that: - **When augmented** (_e.g. in users' applications_), they'll return **strongly typed values** that correspond to the defined types. - **When empty** (_e.g. in Strapi codebase_), they'll return **generic low level types** based on their index definition. **Augmented** ```ts import type { UID } from '@strapi/types'; const uid: UID.ContentType; // ^ 'api::article.article' | 'admin::user' ``` **Empty** ```ts import type { UID } from '@strapi/types'; const uid: UID.ContentType; // ^ `admin::${string}` | `api::${string}.${string}` | `plugin::${string}.${string}` | `strapi::${string}` ``` ::: ###### 4. Type Augmentation ###### Manual Augmentation It's possible to manually augment the public registries to create tailor-made experiences. ```ts title="my-app/index.d.ts" import type { Struct } from '@strapi/strapi'; interface ApiArticleArticle extends Struct.ContentTypeSchema { // ... } declare module '@strapi/strapi' { export module Public { export interface ContentTypeSchemas { 'api::article.article': ApiArticleArticle; } } } ``` This will force every type that depends on the `Public.ContentTypeSchemas` registry to recognize `'api::article.article'` as the only valid UID and `ApiArticleArticle` the only valid schema. :::note In the context of a Strapi application, developers are strongly encouraged to use types exported by `@strapi/strapi` and not `@strapi/types`. This is to allow having both an internal (`@strapi/types`) and a public (`@strapi/strapi`) types API. ::: ###### Automated Augmentation To ease the registries augmentation, Strapi offers an automated way of generating types and extending the registries. The process will generate type definitions based on the user application state (`schemas`), then augment the registry using the created types. **Manual Run** Generate the types once. ```shell title="my-app/" yarn strapi ts:generate-types ``` **During Development** Start the application in dev mode, and generate types on every server restart. Useful when working with the content-type builder. ```shell title="my-app/" yarn develop ``` The generated types will automatically augment the corresponding registries. ```ts title="my-app/types/generated/contentTypes.d.ts" import type { Struct } from '@strapi/strapi'; interface ApiArticleArticle extends Struct.ContentTypeSchema { // ... } interface AdminUser extends Struct.ContentTypeSchema { // ... } declare module '@strapi/strapi' { export module Public { export interface ContentTypeSchemas { 'api::article.article': ApiArticleArticle; 'admin::user': AdminUser; } } } ``` --- When coupling everything together, the end result is a TypeScript developer experience automatically adjusted to the current context. {/* prettier-ignore-start */} **User Application** ```ts title="my-app/src/index.ts" export default () => ({ bootstrap() { strapi.findOne('ap'); // ^ TypeScript will autocomplete with "api::article.article" strapi.findOne('ad'); // ^ TypeScript will autocomplete with "admin::user" strapi.findOne('api::blog.blog'); // ^ Error, TypeScript will complain } }) ``` **Strapi Codebase** ```ts title="@strapi/strapi/document-service.ts" import type { UID } from '@strapi/types'; export const findOne(uid: TUID) { // ... } findOne('admin::foo'); // ^ Valid, matches 'admin::${string}' findOne('plugin::bar.bar'); // ^ Valid, matches 'plugin::${string}.${string}' findOne('baz'); // ^ Error, does not correspond to any content-type UID format ``` {/* prettier-ignore-end */} --- ## Concepts import DocCardList from '@theme/DocCardList'; import { useCurrentSidebarCategory } from '@docusaurus/theme-common'; --- ## CheatSheet import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import Admonition from '@theme/Admonition'; #### Common Use Cases --- ##### Function that Returns Any Entity ```typescript import type { Data } from '@strapi/types'; function find(): Data.Entity {} ``` --- ##### Using UIDs as Resource Selectors in Function Params **Dynamic** :::info Infer the UID type based on usage and allow passing it to other generic types. ::: ```typescript import type { Schema, UID } from '@strapi/types'; function getSchema(uid: TSchemaUID): Schema.Schema {} ``` **Static** :::info Simpler definition than the dynamic one. Useful when the uid type doesn't need to be re-used by other type. ::: ```typescript import type { Schema, UID } from '@strapi/types'; function getSchema(uid: UID.Schema): Schema.Schema {} ``` --- ##### Loop Over Schema Attributes Given a schema such as: ```typescript import type { Schema } from '@strapi/types'; declare const schema: Schema.Schema; ``` Extract its attributes ```typescript const { attributes } = schema; ``` Then loop over them, using `attribute.type` as a discriminator to infer the correct type for each attribute type. ```typescript for (const attributeName in attributes) { // attribute is Schema.Attribute.AnyAttribute const attribute = attributes[attributeName]; switch (attribute.type) { case 'string': // attribute is Schema.Attribute.String console.log(attribute.minLength, attribute.maxLength); break; case 'relation': // attribute is Schema.Attribute.Relation console.log(attribute.relation); break; } } ``` --- ##### Type Guard ```typescript import type { Schema } from '@strapi/types'; function isContentTypeSchema(schema: Schema.Schema): schema is Schema.ContentType { return schema.modelType === 'contentType'; } ``` --- ##### Assertion Function ```typescript import type { Schema } from '@strapi/types'; function assertIsContentTypeSchema(schema: Schema.Schema): asserts schema is Schema.ContentType { if (schema.modelType !== 'contentType') { throw new Error('schema is not a content-type'); } } ``` --- #### Data ```typescript import type { Data } from '@strapi/types'; ``` **Data.Entity** #### Represent any entity ```typescript type Entity = Data.Entity; ``` #### Represent a specific content-type entity ```typescript type Article = Data.Entity<'api::article.article'>; ``` #### Represent a specific component entity ```typescript type Metadata = Data.Entity<'seo.metadata'>; ``` #### Represent any content-type ```typescript type Entity = Data.Entity; ``` #### Only select a subset of an entity ```typescript type PartialArticle = Data.Entity<'api::article.article', 'title' | 'description' | 'author'>; ``` **Data.ContentType** #### Represent any content-type entity ```typescript type ContentTypeEntity = Data.ContentType; ``` #### Represent a specific content-type entity ```typescript type Article = Data.ContentType<'api::article.article'>; ``` #### Only select a subset of a content-type entity ```typescript type PartialArticle = Data.ContentType<'api::article.article', 'title' | 'description' | 'author'>; ``` **Data.Component** #### Represent any component entity ```typescript type ComponentEntity = Data.Component; ``` #### Represent a specific component entity ```typescript type Metadata = Data.Component<'seo.metadata'>; ``` #### Only select a subset of a component entity ```typescript type PartialMetadata = Data.Component<'seo.metadata', 'description' | 'tags'>; ``` --- #### UID ```typescript import type { UID } from '@strapi/types'; ``` ##### Represent any schema UID ```typescript type SchemaUID = UID.Schema; ``` ##### Represent a specific content-type UID ```typescript const articleUID = 'api::article.article' satisfies UID.ContentType; ``` ##### Represent a specific component entity ```typescript const metadataUID = 'seo.metadata' satisfies UID.Component; ``` ##### Represent any content-type ```typescript type ContentTypeUID = UID.ContentType; ``` ```typescript declare const contentTypeUID: UID.ContentType; ``` ##### Check if a given UID is a ContentType UID ```typescript type IsContentType = UID.IsContentType<'api::article.article'>; ``` :::note `IsContentType` will either be `true` or `false`. ::: --- ## Type System import DocCardList from '@theme/DocCardList'; import { useCurrentSidebarCategory } from '@docusaurus/theme-common'; --- ## Providers ## Provider Extends the upload plugin, to connect to different external services or applications, such as Amazon S3 buckets, Cloudinary, etc. **In the case of the Upload plugin, the provider should be able to upload files to a remote server and delete them.** ### Using a provider To use a provider, you need to install it and configure it in the `./config/plugins.js` file. More info about installing providers on [strapi docs](https://docs.strapi.io/developer-docs/latest/development/providers.html#installing-providers). ### Provider development To create a provider, you need to create a package that exports a function that returns an object with the following methods: - `isPrivate()` (optional) - Returns a boolean indicating if the provider is private or not. If it is, the `getSignedUrl` method will be used to get the URL of the file. (default: `false`) - `getSignedUrl(file)`. (optional) - Returns a signed URL to access the file if it requires authentication - `upload(file)` - Uploads a file to the provider - `uploadStream(stream)` (optional) - Uploads a stream to the provider - `delete(file)` - Deletes a file from the provider --- ## MIME type validation This page documents how the Upload plugin validates file MIME types when upload security is configured (`plugin::upload.security` with `allowedTypes` and/or `deniedTypes`). All accept/deny decisions go through a single helper so the logic stays simple and consistent. New projects scaffolded with `create-strapi-app` explicitly deny `image/svg+xml` even though their allowlist includes `image/*`. This is a generated configuration default only: it does not change existing projects, projects without an explicit upload policy, or objects that were already uploaded. Projects that intentionally accept SVG can adjust their explicit policy, while remembering that `deniedTypes` takes precedence over `allowedTypes`. Upload providers and CDNs control final delivery headers, so MIME admission configuration does not guarantee how a remote object is delivered. ### Inputs - **declared** — Client-provided Content-Type (may be generic or missing). - **fileName** — Upload filename (used to derive extension). - **file** — File object (path or buffer) for content detection. - **config** — `{ allowedTypes?: string[], deniedTypes?: string[] }`. ### Derived values - **extension** — Filename extension (e.g. `.jpg`). - **expectedMimeFromExt** — MIME type for that extension (`mime-types.lookup`), or null if unknown. - **detected** — Result of `detectMimeType(file)` (content-based, e.g. via `file-type`); undefined if detection fails or throws. - **Generic declared** — Declared is empty or `application/octet-stream`. ### Helper: validateAllowBanLists(mimetype) All allow/deny outcomes go through this helper. It is the only place that decides accept vs reject for a given type. - If mimetype is in **deniedTypes** → return REJECT. - If **allowedTypes** is undefined → return ACCEPT (with stored type = mimetype). - If **allowedTypes** is empty array → return REJECT. - If mimetype matches any entry in **allowedTypes** (exact or pattern, e.g. `image/*`) → return ACCEPT (with stored type = mimetype). - Else → return REJECT. **Rule:** Whenever we have a type to evaluate, we **return validateAllowBanLists(that type)**. No extra branching on accept/deny outside this helper. ### Helper predicates - **declaredMatchesExtension** — Declared is specific, expectedMimeFromExt is present, and declared equals (or pattern-matches) expectedMimeFromExt. - **detectedMatchesDeclared** — Detected is defined and equals (or pattern-matches) declared. --- ### Validation steps (in order) Steps are evaluated in sequence. Once a step returns, later steps are not run. **1. Reject if declared type is denied.** If the client explicitly declared a type that is on the deny list, reject immediately. We do not use or trust that type for storage. **2. Reject if extension's type is denied.** If the filename extension maps to a known MIME and that MIME is on the deny list, reject. This blocks dangerous types by extension even when declaration or detection might differ. **3. Run content detection.** Set **detected** = `detectMimeType(file)`. If detection throws, treat **detected** as undefined. Detection is used later to confirm declared type, to block denied types, and as a fallback when declaration is generic or missing. **4. Trusted declaration: declared matches extension and detection confirms.** If **declaredMatchesExtension** and **detectedMatchesDeclared**, we have a consistent declaration and content that matches it. Return **validateAllowBanLists(declared)**. We do not reject here just because detected might also match a denied type (e.g. polyglot -- for example, if we ban html but allow PDF, it is possible that a valid PDF with a .pdf extension could be accidentally rejected because it is detectable as html); we store the declared type. We only allow when detection confirms the file is valid for that type, so we never "trust" declared + extension without detection agreeing. This is safe because even if someone uploaded, for example, a polyglot jpg/exe file, if it has a .jpg extension and stored mimetype of jpg, the only way it could potentially be dangerous is if a user explicitly downloads, renames, AND sets the executable flag on their operating system **5. Reject if detected type is denied.** If we have a detected type and it is on the deny list, reject. This applies when we did not already accept in step 4 (e.g. declared did not match extension or detection did not match declared). Blocking on detected here avoids storing content that is identified as a denied type when we are not in the "trusted declaration" path. **5b. Reject if allow list is set and detected is not in it.** If **allowedTypes** is set, **detected** is defined, and detected does not match any allowed type, reject. This prevents extension or declared type from overriding content-based detection (e.g. a JPEG sent as `.pdf` with declared `application/pdf` is rejected when only PDF is allowed). **Exception:** When detected is `application/zip` and the extension maps to a type that is in the allow list (e.g. Office formats like docx, xlsx), do not reject here; step 6 will use the extension type. This is because content detection often returns `application/zip` for Office files when the buffer is small. **6. Use detected type when it is defined and matches extension or declared is generic.** If **detected** is defined and (expectedMimeFromExt is missing, or detected matches extension, or declared is generic), we have a usable content-derived type. Return **validateAllowBanLists(detected)**. This is the main fallback when we do not have a trusted declaration (step 4). **Implementation note:** When detected is `application/zip` and the extension maps to a specific type in the allow list (e.g. docx), the implementation may return **validateAllowBanLists(expectedMimeFromExt)** so Office uploads are allowed. **7. Use extension's type when present.** If **expectedMimeFromExt** is present, use it. Return **validateAllowBanLists(expectedMimeFromExt)**. This covers cases where detection failed or was undefined and we still have a known extension. **8. Use declared type as last resort.** If **declared** is present (including generic), use it. Return **validateAllowBanLists(declared)**. For example, when allowedTypes is undefined, this allows the upload with the declared type (e.g. generic); when allowedTypes is set, only allowed declared types pass. **9. Reject when no type can be chosen.** If we reach this step, we have no declared type and did not accept in 4, 6, or 7. Return REJECT (e.g. "cannot verify file type"). --- ### Pseudocode (reference) ``` 1. If declared is specific and declared is in deniedTypes → return REJECT. 2. If expectedMimeFromExt is present and expectedMimeFromExt is in deniedTypes → return REJECT. 3. Set detected = detectMimeType(file); if it throws, detected is undefined. 4. If declaredMatchesExtension and detectedMatchesDeclared → return validateAllowBanLists(declared). 5. If detected is defined and detected is in deniedTypes → return REJECT. 5b. If allowedTypes is set and detected is defined and detected is not in allowedTypes → return REJECT, except when detected is application/zip and expectedMimeFromExt is in allowedTypes (then continue). 6. If detected is defined and (expectedMimeFromExt is missing or detected matches extension or declared is generic) → return validateAllowBanLists(detected); when detected is application/zip and extension type is in allow list, implementation may use expectedMimeFromExt. 7. If expectedMimeFromExt is present → return validateAllowBanLists(expectedMimeFromExt). 8. If declared is present → return validateAllowBanLists(declared). 9. return REJECT. ``` --- ### Notes - **Deny list:** Declared and extension are checked first (steps 1–2). Detected is only used to reject when we did not accept via trusted declaration (step 5), so e.g. "allow PDF, deny HTML" does not de-facto block PDF when the file is a PDF/HTML polyglot and we correctly trust declared + extension + detection for PDF. - **Trusted declaration:** We only allow "declared + extension" when detection confirms the same type (step 4). We never store a type without either detection confirming it or using it via steps 6–8. - **Single gate:** All accept/deny decisions go through **validateAllowBanLists** with one of declared, detected, or expectedMimeFromExt; step 9 is the only reject that does not call the helper (no type to pass). --- ## @strapi/upload ### Purpose `@strapi/upload` is the plugin that stores files for a Strapi application. It provides the Media Library in the admin panel, the upload routes of the admin API and the Content API, and the file and folder content types. It processes images and hands the files to an upload provider. The plugin has a server part (`strapi-server`), an admin part (`strapi-admin`) and shared code (`shared/`, exported as `./_internal/shared`). The plugin loader always enables it. The `media` attribute of any content type points to its files. The provider packages under `packages/providers/upload-*` implement the storage. ### Key concepts #### Content types The plugin declares two content types: `plugin::upload.file` and `plugin::upload.folder`. Both are hidden from the content manager and the content-type builder. A file has a morph relation named `related` to the entries that use it. A folder has `parent`, `children` and `files` relations, and a unique `path` and `pathId`. The relations build the folder tree of the Media Library. #### Upload provider `register` in `server/src/register.ts` creates the provider and stores it in `strapi.plugin('upload').provider`. `createProvider` reads `provider`, `providerOptions` and `actionOptions` from the `plugin::upload` config. The default provider is `local`. It loads `@strapi/provider-upload-`, or the module of that name, and calls its `init(providerOptions)`. The provider must implement `delete` and either `upload` or `uploadStream`. See [Providers](./01-backend/00-providers.md). #### Upload flow The controllers call `prepareUploadRequest` to validate the MIME types before any work starts. The `upload` service in `server/src/services/upload.ts` then builds the file info, optimizes images and checks the size limit. It sends the file to the provider service, which prefers `uploadStream` over `upload`. It saves the `plugin::upload.file` entry and emits a `media.create` event, or `media.update` and `media.delete` for the other actions. The option `concurrentUploadSize` sets how many files one request processes at the same time. #### MIME type validation `server/src/utils/mime-validation.ts` decides whether a file is accepted. It compares the declared type, the type from the file extension and the type detected from the content against the `allowedTypes` and `deniedTypes` lists. See [MIME type validation](./01-backend/01-mime-validation.md). #### Image processing The `image-manipulation` service uses `sharp`. It checks that a file is a valid image, optimizes it, and generates a thumbnail and the responsive formats. The stored settings `sizeOptimization`, `responsiveDimensions` and `autoOrientation` control these steps. `bootstrap` writes their defaults into the plugin store. #### Private providers and signed URLs A provider can return `true` from `isPrivate()` and implement `getSignedUrl(file)`. In that case, `signFileUrlsOnDocumentService` adds a document service middleware. The middleware signs the file URLs in the results and removes signatures from the data that callers write. #### Admin part The admin entry adds the Media Library menu link and its settings page, registers the `media` field input, and registers the `media-library` dialog component. `bootstrap` on the server registers the permission actions, for example `plugin::upload.assets.create`, and the webhook events. The redesigned Media Library is the default. The `useLegacyMediaLibrary` key in `config/features` selects the legacy one from `admin/src/legacy`. ### Related - [Extension points](../../../architecture/04-extension-points.md): the plugin loader enables this plugin, and the plugin adds a document middleware. - [Document write path](../../../architecture/05-document-write-path.md): the signing middleware runs in the Document Service chain. - [`@strapi/provider-upload-local`](../../providers/upload-local/index.md): the default provider. - [`@strapi/provider-upload-aws-s3`](../../providers/upload-aws-s3/index.md): a remote provider. - [`@strapi/provider-upload-cloudinary`](../../providers/upload-cloudinary/index.md): a remote provider. - [`@strapi/admin`](../admin/index.md): hosts the field registry and the menu API that the admin part uses. - [`@strapi/utils`](../utils/index.md): provides the file helpers and error classes. --- ## Async utils functions ## Async utils ### Summary Async utils are grouping all function that interact with async stuff like Promises. ### Detailed design #### map The `map` function is an asynchronous version of the `Array.prototype.map` method. Example usage: ```js import { async } from '@strapi/utils'; const input = [1, 2, 3]; const output = await async.map(input, async (item) => { return item * 2; }); console.log(output); // [2, 4, 6] ``` #### reduce The `reduce` function is an asynchronous version of the `Array.prototype.reduce` method. Example usage: ```js import { async } from '@strapi/utils'; const input = [1, 2, 3]; const reducer = async.reduce(input); const output = await reducer(async (accumulator, item) => { return accumulator + item; }, 0); console.log(output); // 6 ``` #### pipe The `pipe` function is a utility function for composing asynchronous functions. It takes a list of functions as input, and returns a new function that applies each function in turn to the input. Example usage: ```js import { async } from '@strapi/utils'; async function addOne(input: number): Promise { return input + 1; } async function double(input: number): Promise { return input * 2; } const addOneAndDouble = async.pipe(addOne, double); const output = await addOneAndDouble(3); console.log(output); // 8 ``` #### When to use Every time the code has to act with promises and iterate other them, an async utils function should be used. #### Should I add my function here ? Any util function that manipulates promises can be included in this utils section. Please consider the next point if a lots of functions are available in the async section. ### Potential improvements Some ideas of functions that could be added: - Other `Array.prototype` methods: `filterAsync`, `someAsync`, `everyAsync`, `findAsync`, `findIndexAsync`, `flatMapAsync`. - `retryAsync`: A function that retries an asynchronous operation a specified number of times if it fails. It takes an asynchronous operation and a number of retries as input, and returns the result of the operation if it succeeds within the specified number of retries, or throws an error if it fails after all retries. - `timeoutAsync`: A function that adds a timeout to an asynchronous operation. It takes an asynchronous operation and a timeout duration as input, and returns the result of the operation if it completes within the specified timeout, or throws an error if it takes longer than the timeout. If we begin to use lots of async utils function, we may consider to migrate to a specialized library like [asyncjs](http://caolan.github.io/async/v3/) ### Resources - [Async file in Strapi](https://github.com/strapi/strapi/blob/9b36c3b10adaa00fd3596853abc63122632c36fe/packages/core/utils/lib/async.js) - http://caolan.github.io/async/v3/ --- ## @strapi/utils ### Purpose `@strapi/utils` holds the code that several Strapi packages share. It covers hooks, providers, errors, content type helpers, query parameter conversion, schema traversal, and the sanitize and validate layers of the Content API. The package runs on the server. The admin code imports only types from it, for example `errors`. Most server packages depend on it, including `@strapi/core`, `@strapi/database`, `@strapi/permissions` and the plugins. `src/index.ts` is the only entry point. It exports most helpers as a namespace, for example `errors`, `hooks`, `sanitize` and `validate`. ### Key concepts #### Provider factory `providerFactory` creates a keyed store with `register`, `get`, `delete`, `has`, `keys`, `values` and `clear`. Each provider has `hooks` (`willRegister`, `didRegister`, `willDelete`, `didDelete`). By default, `register` throws when the key already exists. The option `throwOnDuplicates: false` turns this off. The permission engine uses providers for its actions and conditions. #### Hooks The `hooks` namespace creates hook objects: `createAsyncSeriesHook`, `createAsyncSeriesWaterfallHook`, `createAsyncParallelHook` and `createAsyncBailHook`. A hook object has `register`, `delete`, `getHandlers` and `call`. The factory decides how `call` runs the handlers. The [hooks registry](../../../architecture/04-extension-points.md#hooks-registry) and the permission engine build on them. #### Errors The `errors` namespace defines `ApplicationError` and its subclasses, for example `ValidationError`, `NotFoundError`, `ForbiddenError`, `UnauthorizedError` and `PolicyError`. Each error has a `name`, a `message` and `details`. The errors middleware in `@strapi/core` formats these errors into the HTTP error response. #### Schema traversal `traverseEntity` walks a data entity along its schema. It calls a `Visitor` for each attribute, and it recurses into relations, media, components and dynamic zones. The `traverse` namespace does the same for query parameters: fields, filters, sort and populate. See [traverseEntity](./traverse-entity.md). #### Sanitize and validate `sanitize.createAPISanitizers` and `validate.createAPIValidators` build the Content API input, output and query checks from `traverseEntity` visitors. The `sanitize` visitors (`remove-*`) drop what is not allowed. The `validate` visitors (`throw-*`) throw a `ValidationError` instead. `@strapi/core` creates both in its `content-api` service. #### Async helpers The `async` namespace has `pipe`, `map` and `reduce` for promises. See [Async utils](./async.md). #### Content type and query helpers The `contentTypes` namespace holds the attribute name constants (for example `ID_ATTRIBUTE` and `DOC_ID_ATTRIBUTE`), the reserved attribute names and predicates on schemas. The `queryParams` namespace converts REST query parameters into the query format of the database layer. ### Related - [Extension points](../../../architecture/04-extension-points.md): the hooks registry uses the hook objects of this package. - [HTTP request path](../../../architecture/06-http-request-path.md): how a request reaches a controller. Controllers call the sanitize and validate helpers, and the errors middleware formats thrown errors. - [`@strapi/permissions`](../permissions/index.md): builds its engine on `providerFactory` and `hooks`. - [`@strapi/core`](../core/index.md): applies sanitize, validate and the error formats. --- ## traverseEntity ## Traverse Entity Utility The `traverseEntity` function is designed to recursively traverse a data entity based on its schema definition. It provides hooks to execute custom logic at each level of the traversal through the `Visitor` function. ### Overview #### Purpose The utility is used for structured data manipulation. It supports various types of attributes, such as relations, components, dynamic zones, and media, allowing seamless traversal and transformation of deeply nested entities. ### Function: `traverseEntity` #### Signature ```typescript async function traverseEntity( visitor: [Visitor](#visitoroptions), options: TraverseOptions, entity: Data ): Promise; ``` #### Parameters - **`visitor`**: A function executed for each attribute of the entity. It provides context and utilities for modifying the entity. - **`options`**: Configuration for traversal, including: - `schema` _(Model)_: The schema definition of the current entity. - `path` _(Path, optional)_: Tracks the current traversal path. - `parent` _(Parent, optional)_: Information about the parent attribute. - `getModel` _(Function)_: A function to retrieve the schema of a model by its UID. - **`entity`**: The data entity to be traversed. #### Returns A `Promise` that resolves with the transformed entity. ### Related Types #### Visitor ```typescript type Visitor = (visitorOptions: VisitorOptions, visitorUtils: VisitorUtils) => void; ``` A function executed during traversal for each attribute. **Parameters**: - **`visitorOptions`** _(VisitorOptions)_: Provides details about the current state of traversal. - **`visitorUtils`** _(VisitorUtils)_: Utilities for modifying the entity (e.g., `set`, `remove`). #### VisitorOptions ```typescript interface VisitorOptions { data: Record; schema: Model; key: string; value: Data[keyof Data]; attribute?: AnyAttribute; path: Path; getModel(uid: string): Model; parent?: Parent; } ``` - **`data`**: The current entity or its sub-section being processed. - **`schema`**: The schema definition of the current entity. - **`key`**: The current attribute key. - **`value`**: The current attribute value. - **`attribute`** _(optional)_: The schema attribute definition for the current key. - **`path`**: Tracks the traversal path. - **`getModel`**: Function to retrieve a schema definition by UID. - **`parent`** _(optional)_: Information about the parent attribute. #### VisitorUtils ```typescript interface VisitorUtils { remove(key: string): void; set(key: string, value: Data): void; } ``` Utility functions to modify the entity: - **`remove(key)`**: Deletes an attribute from the entity. - **`set(key, value)`**: Sets or updates an attribute on the entity. #### Path ```typescript interface Path { raw: string | null; attribute: string | null; rawWithIndices: string | null; } ``` Tracks the traversal path: - **`raw`**: The traversal path on the entity - **`attribute`**: The same traversal path on the schema if the attribute exists - **`rawWithIndices`**: The traversal path on the entity including array indices using dot notation (e.g., `components.4.field.relations.2.name`) #### Parent ```typescript interface Parent { attribute?: Attribute; key: string | null; path: Path; schema: Model; } ``` Tracks parent information for the current attribute. #### TraverseOptions ```typescript interface TraverseOptions { schema: Model; path?: Path; parent?: Parent; getModel(uid: string): Model; } ``` Configuration for the traversal: - **`schema`**: The schema definition of the current entity. - **`path`** _(optional)_: Tracks the current traversal path. - **`parent`** _(optional)_: Information about the parent attribute. - **`getModel`**: Function to retrieve a schema definition by UID. --- ## @strapi/generators ### Purpose Provides interactive and programmatic code generation for Strapi projects. Generates boilerplate for APIs, controllers, services, models, plugins, policies, and components. Strapi app developers use it via the CLI. ### Key concepts - **Plop-based**: Uses Plop templating engine for code generation. See [index.ts](https://github.com/strapi/strapi/tree/develop/packages/generators/generators). - **CLI and API**: `runCLI()` starts the interactive generator; `generate()` runs generators programmatically without user interaction. - **Generators**: api, controller, service, model, plugin, policy, and component templates. Customize with options and data. ### Related --- ## Packages import PackageTable from '@site/src/components/PackageTable'; Each workspace package under `packages/` has a page at the same path in this site: `packages/core/database` is documented in [`/packages/core/database`](/packages/core/database). The table is generated from each `package.json`. To see how the packages depend on each other, open the [package map](/architecture/package-map). --- ## @strapi/plugin-cloud ### Purpose `@strapi/plugin-cloud` adds a "Deploy" page to the admin panel. The page shows two ways to deploy a project to Strapi Cloud: the Cloud dashboard (marked "Recommended") and the CLI commands `strapi link` and `strapi deploy`. The plugin has admin code only. `create-strapi-app` adds it to the dependencies of each new project, and the `website` template lists it too. ### Key concepts #### Admin-only package The `exports` field of `package.json` has only `./strapi-admin`. There is no `strapi-server` export, so the server plugin loader skips this plugin. The admin build bundles it, because it is a project dependency with `strapi.kind` set to `plugin`. #### Registration `register(app)` in [`admin/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/cloud/admin/src/index.ts) does two things: - `app.addMenuLink` adds the route `plugins/cloud` with the `Cloud` icon, the label "Deploy" and an empty `permissions` list. No RBAC action guards the page. - `app.registerPlugin` registers the plugin with an `Initializer` component that calls `setPlugin`. `registerTrads` loads `translations/.json` and falls back to an empty object. #### Pages and components `pages/App.tsx` routes the index path to `HomePage` and redirects to `/` when `currentEnvironment` is `production`. `HomePage` renders `CloudFeatures` and `CloudDeploy`. `CloudDeploy` has a "Cloud" tab and a "CLI" tab. `CLIDeployTabs` shows the `yarn` or `npm run` commands, with a copy button. The Cloud login and documentation links are hard-coded in `CloudDeploy.tsx`. #### Plugin name and id The admin plugin id is `cloud` (`pluginId`). The `strapi.name` key in `package.json` is `strapi-cloud`. The admin build uses `strapi.name` as the key of the plugin in `appPlugins`. It also reads `enabled: false` under this key in `config/plugins` to skip the plugin. #### Coupling with `@strapi/admin` Two parts of the admin panel depend on this plugin: - `StrapiApp.tsx` in `packages/core/admin/admin/src` registers the `deploy-now` home widget when `'strapi-cloud'` is a key of `appPlugins`. - `hideCloudDeployMenuLinkInProduction` in `utils/widgetVisibility.ts` removes the `plugins/cloud` menu link in production. It matches the path in the `CLOUD_DEPLOY_MENU_LINK` constant. Renaming `strapi.name` or the route path breaks these two checks without a type error. ### Related - [Architecture overview](../../../architecture/index.md): the server and admin split, and why this plugin has no server entry. - [Cloud CLI](../../cli/cloud/index.md): the package that provides the `link` and `deploy` commands that the page shows. - [Admin](../../core/admin/index.md): the package that owns the home widget and the menu filter. --- ## @strapi/plugin-color-picker ### Purpose `@strapi/plugin-color-picker` adds one custom field named `color`. The field stores a HEX color as a string. In the Content Manager, editors choose the color with a saturation and hue picker or type the HEX value. The plugin has a server part and an admin part. Application developers add it to a project. Strapi maintains it as an official custom field plugin. ### Key concepts #### Two-sided custom field registration A custom field needs a registration on both sides. The server declares the field type. The admin declares how the field looks and behaves. - Server: [`server/src/register.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/color-picker/server/src/register.ts) calls `strapi.customFields.register({ name: 'color', plugin: 'color-picker', type: 'string' })`. The registry stores it under the UID `plugin::color-picker.color`. - Admin: [`admin/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/color-picker/admin/src/index.ts) calls `app.customFields.register` with the same `name` and `type`, and with `pluginId: 'color-picker'`. The plugin has no `bootstrap` function, no content types and no services. #### Underlying type The custom field uses the `string` type. The server registry accepts only a fixed list of types for custom fields, and `string` is one of them. An attribute that uses the field has `type: 'string'` and `customField: 'plugin::color-picker.color'` in its schema. #### Admin registration options The admin call sets `icon` (`ColorPickerIcon`), `intlLabel`, `intlDescription` and `components.Input`. `Input` is a lazy import of `ColorPickerInput`, so the picker code loads only when a form needs it. The `options.advanced` list adds two settings to the Content-Type Builder form: a `regex` text option with a default HEX pattern, and a `required` checkbox. #### Input component `ColorPickerInput` in `admin/src/components/` reads and writes the value with `useField` from `@strapi/strapi/admin`. It shows a toggle button and a design system `Popover`. The popover holds a `HexColorPicker` from `react-colorful` and a text input for the HEX value. An empty value means no color, and the component does not default to black. #### Translations `registerTrads` loads `translations/.json` and prefixes the keys with the plugin id. Locales without a file get an empty object. ### Related - [Container and registries](../../../architecture/03-container-and-registries.md): the `custom-fields` registry and the `strapi.customFields` service. - [Extension points](../../../architecture/04-extension-points.md): how plugins add server behavior in `register`. - [Content-Type Builder](../../core/content-type-builder/index.md): shows registered custom fields when a developer adds an attribute. - [Content Manager](../../core/content-manager/index.md): renders the `Input` component in the edit view. --- ## @strapi/plugin-documentation ### Purpose `@strapi/plugin-documentation` creates an OpenAPI 3 document for the REST API of a Strapi application. It shows the document with Swagger UI. The plugin has a server part and an admin part. The server part generates the files and serves the UI. The admin part lists the versions and gives access to the settings. Application developers install it. Other plugins, such as `upload` and `users-permissions`, give it hand-written specs. ### Key concepts #### Config is a base OpenAPI document The default config in `server/src/config/default-plugin-config.ts` is an OpenAPI document with empty `paths`. Users override it under `documentation.config` in `config/plugins`. The value `info.version` names the documentation version. The key `x-strapi-config` holds two plugin options. `plugins` lists the plugins to document, and `null` means the defaults `upload` and `users-permissions`. `mutateDocumentation` is a function that can change the final document before the plugin writes it. #### Generation The `documentation` service ([`services/documentation.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/documentation/server/src/services/documentation.ts)) has `generateFullDoc(version?)`. It takes the base config and, for each API and each documented plugin, adds `paths` and `components.schemas` from the routes and content types. The helpers are in `services/helpers/`. It skips APIs that an override excludes. It then merges the registered overrides whose `info.version` is unset or equal to the version. Last, it runs `mutateDocumentation` and writes `full_documentation.json`. The file goes to `/documentation/documentation//`. In production, the extensions dir is `/src/extensions`. Otherwise it is `/src/extensions`. #### Bootstrap `bootstrap` registers four RBAC actions with `admin::permission`: `plugin::documentation.read`, `settings.update`, `settings.regenerate` and `settings.read`. It seeds `{ restrictedAccess: false }` in the plugin store. Outside production, it also calls `generateFullDoc()`, so a new `info.version` creates a new version at the next start. #### Override service The `override` service lets other plugins document their own routes. `registerOverride(spec, { pluginOrigin, excludeFromGeneration })` takes an object or a YAML string. It skips the override when `pluginOrigin` is not in the documented plugin list. `excludeFromGeneration` removes those APIs from automatic generation. The `upload` and `users-permissions` plugins call it in `register` when `strapi.plugin('documentation')` exists. #### Routes and access control `register` adds a route for the Swagger UI static files at `/plugins/documentation/(.*)`. The plugin routes serve the UI at `/documentation` and `/documentation/v..`. The `index` controller reads the JSON file and fills the `public/index.html` template. When the `restrictedAccess` setting is `true`, the `restrict-access` middleware redirects to `/documentation/login` unless `ctx.session.documentation.logged` is set. The login controller compares the password with a bcrypt hash in the plugin store. The routes `getInfos`, `regenerateDoc` and `updateSettings` use the `admin::hasPermissions` policy. The route `deleteDoc` sets an empty `policies` list. #### Admin `admin/src/index.ts` adds a menu link (`plugins/documentation`) in `register` and a `global` settings link in `bootstrap`. The main page lists versions with open, regenerate and delete actions. The settings page edits `restrictedAccess` and the password. The `PERMISSIONS` constants in `admin/src/constants.ts` map to the four RBAC actions. ### Related - [Extension points](../../../architecture/04-extension-points.md): how a plugin reaches another plugin service in `register`. - [HTTP request path](../../../architecture/06-http-request-path.md): how routes, policies and route middlewares apply. - [users-permissions](../users-permissions/index.md): registers a hand-written OpenAPI spec through the override service. - [Upload](../../core/upload/index.md): registers its own spec the same way. - [OpenAPI](../../core/openapi/index.md): a separate package that generates OpenAPI documents. This plugin does not use it. - [Admin](../../core/admin/index.md): owns the permission action provider that `bootstrap` uses. --- ## @strapi/plugin-graphql ### Purpose `@strapi/plugin-graphql` adds a GraphQL endpoint to a Strapi application. At `bootstrap`, the plugin turns the content types and components into a GraphQL schema ("shadow CRUD"). It then serves the schema with Apollo Server on a Koa route. The plugin runs on the server. Its admin entry only registers the plugin id. Application developers install it. Other plugins extend the schema through its `extension` service. ### Key concepts #### Bootstrap builds and mounts the schema The plugin has no `register` function. [`bootstrap.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/graphql/server/src/bootstrap.ts) calls `service('content-api').buildSchema()`. If the schema is empty, it logs a warning and stops. Otherwise it creates an `ApolloServer`, starts it and mounts one `ALL` route at the `endpoint` config value. The route sets `config.auth: false`, because the handler chain authenticates by itself. The chain has four steps: 1. `cors`, unless `apolloServer.cors` is `false`. 2. `koa-bodyparser`, unless `apolloServer.bodyParserConfig` is `false`. 3. An auth step. It sets `ctx.state.route.info.type` to `content-api` and calls `strapi.auth.authenticate`. It skips authentication for a `GET` that loads the landing page. 4. `koaMiddleware` from `@as-integrations/koa`. The plugin also sets `strapi.plugin('graphql').destroy` to stop Apollo Server on shutdown. #### Config The default config is in `server/src/config/default-config.ts`: `shadowCRUD` (`true`), `endpoint` (`/graphql`), `maxLimit` (`-1`), `apolloServer` (`{}`) and `v4CompatibilityMode` (from `STRAPI_GRAPHQL_V4_COMPATIBILITY_MODE`). The code also reads `depthLimit`, `defaultLimit`, `landingPage`, `generateArtifacts` and `artifacts`. `playgroundAlways` is deprecated. `bootstrap` logs a warning when `depthLimit` or `maxLimit` is not a positive finite number. The `apolloServer` value is merged into the Apollo options, and arrays are concatenated. #### Schema build The `content-api` service ([`services/content-api/`](https://github.com/strapi/strapi/tree/develop/packages/plugins/graphql/server/src/services/content-api)) creates a new `type-registry` and new `builders` on each `buildSchema()` call. It registers scalars and internal types. With `shadowCRUD` on, it registers types, filters, inputs, enums, dynamic zones, queries and mutations for every content type and component, except `admin::` ones. It then builds a Nexus schema and merges it with the SDL from extensions. Then it adds extension resolvers, wraps the resolvers and prunes unused types. #### Extension service `strapi.plugin('graphql').service('extension')` is the public hook. `use(config)` takes an object or a factory `({ strapi, nexus, typeRegistry })`. The config can hold `types`, `typeDefs`, `resolvers`, `resolversConfig` and Nexus `plugins`. `shadowCRUD(uid)` returns a manager. Its methods include `disable`, `disableQueries`, `disableMutations`, `disableActions` and `field(name)`. The schema is built once, so plugins call the service in `register`, before this plugin's `bootstrap`. The [users-permissions](../users-permissions/index.md) and [i18n](../i18n/index.md) plugins do this. #### Resolver wrapping `wrapResolvers` in `services/content-api/wrap-resolvers.ts` reads `resolversConfig`. The keys are `Type.field` paths. Each entry can set `auth` (with a `scope`), `middlewares` and `policies`. The auth check reads `context.state.auth`. Policies run last, through `createPoliciesMiddleware`. #### Landing page and CSP `determineLandingPage` picks the Apollo landing page from the `landingPage` config. The default is local outside production and hidden in production. It stores the result in the `utils` service (`playground.setEnabled`). The core `strapi::security` middleware reads `playground.isEnabled()` to add Apollo hosts to the content security policy. ### Related - [Extension points](../../../architecture/04-extension-points.md): the `register` and `bootstrap` phases that decide when to call the extension service. - [HTTP request path](../../../architecture/06-http-request-path.md): how a route added with `strapi.server.routes` handles a request. - [Authentication](../../../architecture/08-authentication.md): the Content API auth strategy that the endpoint calls. - [users-permissions](../users-permissions/index.md): adds login, register and `me`, and replaces user and role mutations. - [i18n](../i18n/index.md): adds the `I18NLocaleCode` scalar and a locale argument plugin. --- ## @strapi/i18n ### Purpose `@strapi/i18n` lets a content type keep one version of each document per locale. The plugin loader always enables it, so every Strapi application has it. It runs on the server and in the admin panel. The server part owns the locale data, the `locale` and `localizations` attributes, the sync of shared fields and the locale permissions. The admin part adds the locale UI to the Content Manager and to the Content-Type Builder. The Document Service in `@strapi/core` also calls its services. ### Key concepts #### Localized flag A content type is localized when `pluginOptions.i18n.localized` is `true` (`isLocalizedContentType` in `services/content-types.ts`). An attribute has its own `pluginOptions.i18n.localized` flag. `isLocalizedAttribute` also treats relations and `uid` attributes as localized. Attributes that are not localized hold the same value in every locale of a document. #### Attributes on every content type `register` in [`server/src/register.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/i18n/server/src/register.ts) adds a `locale` string attribute and a virtual `localizations` relation to every content type. A content type that is not localized gets both as private attributes. The relation joins on `document_id` and excludes the entries already loaded. `register` runs before the database starts, so the attributes reach the schema. Read [Schema sync](../../../architecture/07-schema-sync.md) for that step. #### Locale data and default locale The plugin adds the content type `plugin::i18n.locale` with `name` and a unique `code`. Content Manager and Content-Type Builder hide it. The default locale code is in the core store under `default_locale`. At `bootstrap`, `initDefaultLocale` creates the first locale when none exists. It uses English, or the code in `STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE`. #### Sync hook and core migrations `register` adds a handler to `strapi::content-types.afterSync`. It calls `repairPermissionsForNewlyLocalizedTypes`. Core also handles a content type that turns i18n on or off. `packages/core/core/src/migrations/i18n.ts` sets the default locale on existing rows, or deletes the non-default locales. Read [Schema sync](../../../architecture/07-schema-sync.md). #### Document Service integration Core reads `isLocalizedContentType` and `getDefaultLocale` from this plugin to set the default `locale` on each call. `bootstrap` adds a Document Service middleware for `create`, `update`, `discardDraft` and `publish`. After `next()`, it compares the non-localized fields with their previous values. If they changed, `syncNonLocalizedAttributes` copies them to the other locales of the same document and status. When `strapi.ai.admin.isAvailable()` is true, `bootstrap` also adds a middleware that can start AI localization in the background after `create` and `update`. `register` adds the model `plugin::i18n.ai-localization-job` to the `models` registry for its jobs. Read [Document write path](../../../architecture/05-document-write-path.md). #### Permissions and routes `bootstrap` registers the settings actions `plugin::i18n.locale.create`, `read`, `update` and `delete`. It also adds a `locales` property to the actions of content types. Handlers on the admin role service add all locales to the super admin permissions. A lifecycle subscriber on the locale model syncs those permissions after a locale is created or deleted. The admin routes manage locales, settings and AI localization jobs, and serve ISO locales and Content Manager helper data. One Content API route, `GET /locales`, lists the locales. A router middleware in `register` checks the `locale` on `POST` and `PUT` to the Content Manager collection and single type routes. #### Admin injection [`admin/src/index.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/i18n/admin/src/index.ts) works through the Content Manager and Content-Type Builder plugin APIs. - Hooks: `Admin/CM/pages/ListView/inject-column-in-table`, `Admin/CM/pages/EditView/mutate-edit-view-layout` and `ContentReleases/pages/ReleaseDetails/add-locale-in-releases`. - Content Manager: header actions (locale picker, fill from another locale, AI status), document actions (bulk publish and unpublish, delete locale) and `injectComponent` zones in the list view. - Content-Type Builder: a "Localization" option on content types, and a per-field option on localized types. - A settings link `internationalization`, and an RBAC middleware for the locale. #### GraphQL support When the `graphql` plugin exists, `register` calls `server/src/graphql.ts`. It disables mutations on the locale type, and the `locale` and `localizations` inputs on localized types. It also adds the `I18NLocaleCode` scalar and a locale argument plugin. ### Related - [Schema sync](../../../architecture/07-schema-sync.md): the `beforeSync` and `afterSync` hooks and the migrations that i18n uses. - [Document write path](../../../architecture/05-document-write-path.md): where Document Service middlewares run in a write. - [Extension points](../../../architecture/04-extension-points.md): the mechanisms that this plugin uses, with its `register` as an example. - [Content Manager](../../core/content-manager/index.md): owns the hooks, actions and injection zones that i18n fills. - [Content-Type Builder](../../core/content-type-builder/index.md): owns the form API that i18n extends. - [Content releases](../../core/content-releases/index.md): owns the release details hook that i18n uses. - [GraphQL](../graphql/index.md): owns the extension service that i18n calls. --- ## @strapi/plugin-sentry ### Purpose `@strapi/plugin-sentry` sends the errors that Strapi requests throw to [Sentry](https://sentry.io). The plugin runs on the server. It starts the Sentry Node SDK during `bootstrap` and adds a Koa middleware that reports errors and rethrows them. It also exposes a `sentry` service, so application code can send errors by hand. Project owners install it. The admin part only registers the plugin id and adds no UI. ### Key concepts #### Config [`server/src/config.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/sentry/server/src/config.ts) sets the defaults. The plugin validator does nothing. - `dsn`: the Sentry DSN. The default is `null`. - `sendMetadata`: when `true` (the default), the plugin attaches request data and tags to each event. - `init`: an object that the plugin spreads into `Sentry.init()`. It comes after `dsn` and `environment`, so it can override both. Set the values in `config/plugins` under `sentry.config`. #### The `sentry` service [`server/src/services/sentry.ts`](https://github.com/strapi/strapi/blob/develop/packages/plugins/sentry/server/src/services/sentry.ts) holds the SDK state. Its API has three methods: - `init()` starts Sentry once. With no `dsn`, it logs an info message and stays disabled. If `Sentry.init()` throws, it logs a warning and stays disabled. - `getInstance()` returns the SDK object, or `null` when Sentry is not running. - `sendError(error, configureScope?)` calls `captureException` inside `withScope`. It runs `configureScope` only when `sendMetadata` is `true`. It logs a warning and returns when Sentry is not ready. Other code reads the service with `strapi.plugin('sentry').service('sentry')`. #### Error middleware `bootstrap` calls [`initSentryMiddleware`](https://github.com/strapi/strapi/blob/develop/packages/plugins/sentry/server/src/middlewares/sentry.ts). The function calls `service.init()`. If Sentry did not start, it returns and adds nothing. Otherwise it registers a Koa middleware with `strapi.server.use`. The middleware wraps `next()` in a `try` block. On an `Error`, it calls `sendError` with a scope callback and then rethrows the error. The plugin does not export the middleware. It is not in the `middlewares` registry, so users cannot list it in `config/middlewares`. #### Event metadata The scope callback in the middleware adds the Koa request through `Handlers.parseRequest`. It also sets the tags `transaction` (method and matched route), `strapi_version` and `method`. These tags exist only when `sendMetadata` is `true`. #### No content types or admin UI The plugin adds no content types, routes, controllers or permissions. The admin entry calls `app.registerPlugin` and `registerTrads` only. ### Related - [Server lifecycle](../../../architecture/02-server-lifecycle.md): when plugin `bootstrap` runs, relative to the HTTP server start. - [HTTP request path](../../../architecture/06-http-request-path.md): where a Koa middleware added with `strapi.server.use` sits in a request. - [Extension points](../../../architecture/04-extension-points.md): how plugin `config` is merged and read. --- ## @strapi/plugin-users-permissions ### Purpose `@strapi/plugin-users-permissions` protects the Content API of a Strapi application. It adds end users, roles and permissions, and it registers the authentication strategy that turns a JWT into an ability. It also provides register, login, password reset, email confirmation and OAuth routes. The plugin runs on the server and in the admin panel. The admin part is a settings section where an admin user edits roles, providers, email templates and advanced settings. `create-strapi-app` adds the plugin to each new project. ### Key concepts #### Content types The plugin adds three content types. `plugin::users-permissions.user` (table `up_users`) has `username`, `email`, `provider`, `password`, `resetPasswordToken`, `confirmationToken`, `confirmed`, `blocked` and a `role` relation. `role` (`up_roles`) has `name`, `description`, a unique `type`, and relations to permissions and users. `permission` (`up_permissions`) has an `action` string and a `role` relation. `role` and `permission` are hidden from the Content Manager and the Content-Type Builder. The user type stays visible. The Content Manager reads the `layout.user.actions` config of the plugin and sends user create and update to the `contentManagerUser` controller. #### `register` [`server/src/register.js`](https://github.com/strapi/strapi/blob/develop/packages/plugins/users-permissions/server/src/register.js) does four things: - It registers the strategy `users-permissions` for the `content-api` auth scope, with `strapi.get('auth').register`. - It adds a `content-api.output` sanitizer that removes the `users` relation from role entities. - It calls `server/src/graphql` when the `graphql` plugin exists. That code disables queries and mutations on permissions, replaces the create, update and delete mutations of users and roles, and adds the `me` query and the auth mutations (`login`, `register` and the password and email flows). - It registers a hand-written OpenAPI spec (`documentation/content-api.yaml`) with the `documentation` plugin when it exists. #### Authentication strategy `strategies/users-permissions.js` exports `authenticate` and `verify`. `authenticate` reads the Bearer token with the `jwt` service and loads the user. It rejects blocked users, and unconfirmed users when `email_confirmation` is on. It then builds an ability from the role permissions with `strapi.contentAPI.permissions.engine.generateAbility`. A request with no token gets the permissions of the `public` role, if the role has any. `verify` checks each `config.scope` entry of the route against the ability. Read [Authentication](../../../architecture/08-authentication.md). #### `bootstrap` `bootstrap` seeds three plugin store keys: `grant` (provider config), `email` (templates) and `advanced` (`allow_register`, `email_confirmation`, `default_role` and others). It registers the admin RBAC actions for roles, providers, email templates and advanced settings. It calls the `users-permissions` service `initialize()`. That method creates the `Authenticated` and `Public` roles when no role exists, and runs `syncPermissions()`. `syncPermissions` deletes permissions for actions that no longer exist and creates the default ones when the table is empty. `bootstrap` then defines the `users-permissions` origin on the session manager. If `jwtSecret` is not set, it throws outside `development`. In `development`, it generates a secret and appends `JWT_SECRET` to the `.env` file. #### Config and JWT modes The defaults are in [`server/src/config.js`](https://github.com/strapi/strapi/blob/develop/packages/plugins/users-permissions/server/src/config.js): `jwtSecret` (from `JWT_SECRET`), `jwt` (`expiresIn: '30d'`), `jwtManagement`, `sessions`, `ratelimit` (60 seconds, 10 requests), `layout` and `callback.validate`. The `jwtManagement` key selects the mode. `legacy-support` (default) signs one long-lived JWT with `jsonwebtoken`. `refresh` issues short-lived access tokens and refresh tokens through the core session manager. The `jwt` service has `getToken`, `issue` and `verify`. `issue` and `verify` branch on the mode. #### Providers The `providers-registry` service holds the login providers. Each entry has `enabled`, `icon` and `grantConfig`. OAuth entries also have an `authCallback` function that maps the provider profile to `username` and `email`. The registry has `getAll`, `get`, `add`, `remove` and `run`. A plugin can call `add` to register a provider. The `providers` service `connect` looks up the user by email and provider. If there is none, it creates one with the `default_role`, subject to the `allow_register` and `unique_email` settings. The OAuth flow itself is in `utils/oauth-connect`. #### Rate limit The middleware `plugin::users-permissions.rateLimit` uses `koa2-ratelimit` on the auth routes. The key holds the request path and the IP. It also holds `body.email` on routes that use `email` as the identifier. The list `ROUTES_WITHOUT_IDENTIFIER` in `middlewares/rateLimit.js` names the routes that do not. Add a new auth route there if its body has no `email` field, or callers can vary that field to get a new key. #### Admin section [`admin/src/index.js`](https://github.com/strapi/strapi/blob/develop/packages/plugins/users-permissions/admin/src/index.js) calls `app.createSettingSection` with four pages: Roles, Providers, Email templates and Advanced settings. Each page has its own RBAC permissions. The admin routes have `type: 'admin'`. The role, settings and permission routes use the `admin::hasPermissions` policy. ### Related - [Authentication](../../../architecture/08-authentication.md): the session manager, the JWT modes and the Content API flow. - [Extension points](../../../architecture/04-extension-points.md): the `register` and `bootstrap` mechanisms that this plugin uses. - [Container and registries](../../../architecture/03-container-and-registries.md): the `auth` service and the `sanitizers` registry that `register` writes to. - [Permissions](../../core/permissions/index.md): the engine that builds the ability from permissions. - [Email](../../core/email/index.md): sends the reset password and confirmation emails. - [GraphQL](../graphql/index.md): owns the extension service that the plugin uses for its schema changes. - [Documentation](../documentation/index.md): owns the override service that receives the OpenAPI spec. --- ## @strapi/provider-email-amazon-ses ### Purpose Connects Strapi to Amazon SES to send transactional emails. The email plugin loads this provider when configured with `provider: 'amazon-ses'`. Use it in production environments that are already on AWS and need integration with SES as their mail service. ### Key concepts - Implements the email provider interface: `init(providerOptions, settings)` returns an object with `send(options)` and optional `verify()`, `isIdle()`, `close()`, and `getCapabilities()` methods. - Reads `providerOptions` keys: `region` (optional, AWS region for credentials). - Uses AWS SDK v3 SES client; credentials resolve from standard AWS credential chain (environment variables, IAM roles, credentials file). - Supports the full `SendEmailCommand` interface for addressing, content, and headers via the `send` function. - Source: [github.com/strapi/strapi/tree/develop/packages/providers/email-amazon-ses](https://github.com/strapi/strapi/tree/develop/packages/providers/email-amazon-ses) ### Related - [Email plugin](../../core/email/index.md) - [Sendgrid provider](../email-sendgrid/index.md) - [Nodemailer provider](../email-nodemailer/index.md) --- ## @strapi/provider-email-mailgun ### Purpose Connects Strapi to Mailgun to send transactional emails. The email plugin loads this provider when configured with `provider: 'mailgun'`. Use it when your email infrastructure is hosted on Mailgun. ### Key concepts - Implements the email provider interface: `init(providerOptions, settings)` returns an object with `send(options)` method. - Reads `providerOptions` keys: `key` (required, API key), `domain` (required, Mailgun domain for sending), and other `MailgunClientOptions` passed to the Mailgun client. - Uses the official Mailgun SDK with form-data multipart encoding; assertions check that both key and domain are supplied. - Accepts email fields: from, to, cc, bcc, replyTo, subject, text, html, plus custom passthrough fields (e.g. Mailgun templates). - Source: [github.com/strapi/strapi/tree/develop/packages/providers/email-mailgun](https://github.com/strapi/strapi/tree/develop/packages/providers/email-mailgun) ### Related - [Email plugin](../../core/email/index.md) - [Sendgrid provider](../email-sendgrid/index.md) - [Nodemailer provider](../email-nodemailer/index.md) --- ## @strapi/provider-email-nodemailer ### Purpose Connects Strapi to any SMTP relay using Nodemailer. The email plugin loads this provider when configured with `provider: 'nodemailer'`. Use it for custom SMTP setups, Gmail, office email, or any service with SMTP access. ### Key concepts - Implements the email provider interface: `init(providerOptions, settings)` returns an object with `send(options)`, optional `verify()`, `isIdle()`, `close()`, and `getCapabilities()` methods. - Reads `providerOptions`: any valid Nodemailer transport configuration (host, port, secure, auth, pool, maxConnections, proxy, rateLimit, DKIM, etc.). - Supports comprehensive email options: standard fields (from, to, cc, bcc, replyTo, subject, text, html), plus attachments, alternatives, headers, priority, threading (inReplyTo/references), encoding, calendar events (icalEvent), mailing list support, DKIM signing, OAuth2 authentication per-message. - Blocks file and URL access to prevent local file inclusion or SSRF via attachment sources (disableFileAccess, disableUrlAccess always enforced). - Source: [github.com/strapi/strapi/tree/develop/packages/providers/email-nodemailer](https://github.com/strapi/strapi/tree/develop/packages/providers/email-nodemailer) ### Related - [Email plugin](../../core/email/index.md) - [Amazon SES provider](../email-amazon-ses/index.md) - [Sendgrid provider](../email-sendgrid/index.md) - [Sendmail provider](../email-sendmail/index.md) --- ## @strapi/provider-email-sendgrid ### Purpose Connects Strapi to Sendgrid to send transactional emails. The email plugin loads this provider when configured with `provider: 'sendgrid'`. Use it when your team uses Sendgrid for email delivery and wants to integrate it with Strapi. ### Key concepts - Implements the email provider interface: `init(providerOptions, settings)` returns an object with `send(options)` method. - Reads `providerOptions` keys: `apiKey` (required), `region` (optional, `'eu'` or `'global'` for data residency). - Supports Sendgrid Mail API v3; uses official `@sendgrid/mail` SDK and configures data residency when specified. - Accepts standard email fields: from, to, cc, bcc, replyTo, subject, text, html, and passthrough custom fields. - Source: [github.com/strapi/strapi/tree/develop/packages/providers/email-sendgrid](https://github.com/strapi/strapi/tree/develop/packages/providers/email-sendgrid) ### Related - [Email plugin](../../core/email/index.md) - [Amazon SES provider](../email-amazon-ses/index.md) - [Nodemailer provider](../email-nodemailer/index.md) --- ## @strapi/provider-email-sendmail ### Purpose Connects Strapi to a local sendmail or SMTP server to send transactional emails. The email plugin loads this provider when configured with `provider: 'sendmail'`. Use it in on-premises or legacy deployments with direct SMTP or sendmail access. Nodemailer is recommended for most production setups. ### Key concepts - Implements the email provider interface: `init(providerOptions, settings)` returns an object with `send(options)` method. - Reads `providerOptions` keys: `silent` (optional, default true), `dkim` (optional, boolean or object with privateKey/keySelector), `smtpPort` (optional, default 25), `smtpHost` (optional, extra SMTP host), `devPort`/`devHost` (optional, for development), `rejectUnauthorized`, `autoEHLO`, and `logger` (optional, custom logger functions). - Connects directly to SMTP or sendmail; resolves MX records unless dev mode is enabled. DKIM signing is optional. - Accepts email fields: from, to, cc, bcc, replyTo, subject, text, html, plus custom passthrough fields. - Source: [github.com/strapi/strapi/tree/develop/packages/providers/email-sendmail](https://github.com/strapi/strapi/tree/develop/packages/providers/email-sendmail) ### Related - [Email plugin](../../core/email/index.md) - [Nodemailer provider](../email-nodemailer/index.md) - [Amazon SES provider](../email-amazon-ses/index.md) --- ## @strapi/provider-upload-aws-s3 ### Purpose Stores uploaded files in Amazon S3 or S3-compatible services (MinIO, Wasabi, DigitalOcean Spaces, etc.). The upload plugin loads this provider when configured with `provider: 'aws-s3'`. Use it for cloud deployments, multi-instance setups, or any application that needs scalable, remote file storage. ### Key concepts - Implements the upload provider interface: `init(providerOptions)` returns an object with `upload(file)`, `uploadStream(file)`, `replace(newFile, oldFile)`, `replaceStream(newFile, oldFile)`, `delete(file)`, plus `isPrivate()`, `getSignedUrl(file)`, and optional metadata methods. - Reads `providerOptions` keys: `s3Options` (required, S3 client config with `params.Bucket`), `baseUrl` (optional, CDN or custom domain), `rootPath` (optional, prefix for keys), `providerConfig` (optional, with `checksumAlgorithm`, `preventOverwrite`, `storageClass`, `encryption`, `tags`, `multipart` settings). - Supports private buckets with signed URL generation; uses `isPrivate()` to check if ACL is private, then `getSignedUrl()` generates temporary access links. - S3-compatible providers: warns on AWS-specific features (storage class, KMS encryption) when using non-AWS endpoints; validates multipart configuration. - Credentials resolve from AWS SDK credential chain (environment, IAM roles, config file) or explicit options. - Source: [github.com/strapi/strapi/tree/develop/packages/providers/upload-aws-s3](https://github.com/strapi/strapi/tree/develop/packages/providers/upload-aws-s3) ### Related - [Upload plugin](../../core/upload/index.md) - [Upload providers overview](../../core/upload/01-backend/00-providers.md) - [Local provider](../upload-local/index.md) - [Cloudinary provider](../upload-cloudinary/index.md) --- ## @strapi/provider-upload-cloudinary ### Purpose Stores uploaded files in Cloudinary for image and video management, transformations, and CDN delivery. The upload plugin loads this provider when configured with `provider: 'cloudinary'`. Use it when your content includes rich media and you need automatic optimization, transformations, or video processing. ### Key concepts - Implements the upload provider interface: `init(providerOptions)` returns an object with `upload(file)`, `uploadStream(file)`, `replace(newFile, oldFile)`, `replaceStream(newFile, oldFile)`, and `delete(file)` methods. - Reads `providerOptions`: Cloudinary SDK config options (cloud_name, api_key, api_secret, etc.). - Selects upload method automatically: regular stream for files under 99 MB (faster), chunked stream for larger files (required by Cloudinary for 100+ MB). - Stores metadata: `provider_metadata` includes `public_id` and `resource_type`; replaces in-place if hash matches to invalidate CDN cache. - Generates preview URLs for videos (sample GIF with configurable resolution). - Throws `PayloadTooLargeError` when Cloudinary rejects oversized uploads. - Source: [github.com/strapi/strapi/tree/develop/packages/providers/upload-cloudinary](https://github.com/strapi/strapi/tree/develop/packages/providers/upload-cloudinary) ### Related - [Upload plugin](../../core/upload/index.md) - [Upload providers overview](../../core/upload/01-backend/00-providers.md) - [Local provider](../upload-local/index.md) - [AWS S3 provider](../upload-aws-s3/index.md) --- ## @strapi/provider-upload-local ### Purpose Stores uploaded files on the local filesystem in the `public/uploads/` directory. The upload plugin loads this provider by default when no other provider is configured. Use it for development, testing, or single-server deployments. For multi-instance or cloud deployments, switch to S3, Cloudinary, or another remote provider. ### Key concepts - Implements the upload provider interface: `init(providerOptions)` returns an object with `upload(file)`, `uploadStream(file)`, `replace(newFile, oldFile)`, `replaceStream(newFile, oldFile)`, `delete(file)`, and `checkFileSize(file, options)` methods. - Reads `providerOptions` key: `sizeLimit` (optional, deprecated; moved to plugin config). - Stores files in `/uploads/` with filename ``; checks that the uploads folder exists on init. - URL set to `/` (relative to public directory). - Supports both stream and buffer upload paths; replaces by writing new file first, then deleting old if the path changed (never leaves a gap). - Source: [github.com/strapi/strapi/tree/develop/packages/providers/upload-local](https://github.com/strapi/strapi/tree/develop/packages/providers/upload-local) ### Related - [Upload plugin](../../core/upload/index.md) - [Upload providers overview](../../core/upload/01-backend/00-providers.md) - [AWS S3 provider](../upload-aws-s3/index.md) - [Cloudinary provider](../upload-cloudinary/index.md) --- ## api-tests ### Purpose Provides utilities for API integration tests in the Strapi monorepo. Used by the test suite to create Strapi instances, set up authentication, define test data models, and make API requests using supertest. ### Key concepts - **Strapi instance factory**: `createStrapiInstance()` creates a test instance with bootstrap hooks, super admin setup, and session configuration. See [strapi.js](https://github.com/strapi/strapi/tree/develop/packages/utils/api-tests). - **Test utilities**: Helpers for supertest requests, authentication, model generation, and mock fetch. Extends supertest with Strapi-specific convenience methods. - **Used by**: The API integration tests in `tests/api/` for all database backends (SQLite, PostgreSQL, MySQL). ### Related - [Testing](../../../contributing/03-testing/index.md): guide to running all test suites. --- ## eslint-config-custom ### Purpose Provides custom ESLint configurations for the Strapi monorepo packages. Extends `@strapi/eslint-config` with rules and overrides specific to the project, separating back-end and front-end configurations. ### Key concepts - **Separate configurations**: Dedicated configs for back-end (JavaScript) and front-end (TypeScript/React). See [back/](https://github.com/strapi/strapi/tree/develop/packages/utils/eslint-config-custom) and `front/`. - **Extends Strapi's base**: Builds on `@strapi/eslint-config` with monorepo-specific rules: disabled Prettier plugin, relaxed dynamic require rules, and test file exceptions for imports. - **Consumed by**: Core packages, plugins, and providers use this in their ESLint configs, including `@strapi/database`, `@strapi/core`, `@strapi/permissions`. ### Related --- ## @strapi/logger ### Purpose Provides the logging system for Strapi applications. Built on Winston, it offers configurable log levels, formats (pretty-print, detailed, filtered), and outputs (console, file). Strapi app developers use it; it is not a direct dependency outside the framework. ### Key concepts - **Winston-based**: Exports `createLogger()` factory and re-exports Winston types. Wraps Winston configuration with Strapi defaults. See [index.ts](https://github.com/strapi/strapi/tree/develop/packages/utils/logger). - **Configurable formats**: Includes pretty-print (readable), detailed (timestamp/level), exclude-colors, log-errors, and level-filter formats. Compose them for different environments. - **Output configuration**: Default console output; file output via `output-file-configuration` for logging to disk. ### Related --- ## oxlint-config ### Purpose Provides the Oxlint configuration for the entire Strapi monorepo. Oxlint runs once over all code; this config composes back-end, front-end, and test overrides into a single policy instead of per-package files. ### Key concepts - **Composed config**: Exports base, back-end, front-end, and test-specific configs as a single object with overrides. See [oxlint.config.ts](https://github.com/strapi/strapi/tree/develop/packages/utils/oxlint-config). - **Area-specific rules**: Back-end, front-end (React 18), and test overrides differ in linting rules and environments. Avoids per-package duplication. - **ESM module**: Declared as `"type": "module"` to load via `oxlint --config` without MODULE_TYPELESS_PACKAGE_JSON warnings. ### Related --- ## tsconfig ### Purpose Provides reusable TypeScript configurations for the Strapi monorepo. Extends `@tsconfig/node20` with strict type-checking and module options consistent across all packages. ### Key concepts - **Base config**: Enables strict mode, declaration maps, and ESNext modules. Extends Node 20 preset from @tsconfig/node20. See [base.json](https://github.com/strapi/strapi/tree/develop/packages/utils/tsconfig). - **Client config**: Additional configuration for client-side code. - **Consumed by**: Core packages (`@strapi/database`, `@strapi/core`, `@strapi/permissions`), plugins, and providers extend these configs in their `tsconfig.build.json`. ### Related - [TypeScript](../../../contributing/05-typescript.md): TypeScript conventions for the monorepo. --- ## @strapi/typescript-utils ### Purpose Provides TypeScript compilation utilities and code generators for Strapi. Used internally by the Strapi CLI to compile TypeScript projects and generate boilerplate code for APIs, controllers, services, and content types. ### Key concepts - **Compiler interface**: `compile()` function compiles TypeScript in a source directory using detected tsconfig. Supports configurable compiler options. See [compile.ts](https://github.com/strapi/strapi/tree/develop/packages/utils/typescript). - **Compilers**: Basic TypeScript compiler and common utilities for handling configuration, paths, and diagnostics. - **Generators**: Templates and utilities to scaffold API routes, controllers, services, models, components, and content types. ### Related --- ## @strapi/upgrade ### Purpose Provides a CLI tool for Strapi app developers to upgrade between versions. Updates package.json, runs the package installer, and applies code transforms for breaking changes. Available via `npx @strapi/upgrade`. ### Key concepts - **Version commands**: `latest`, `major`, `minor`, `patch` for upgrading to specific release tracks, and `to ` to pin a version. `codemods` runs transforms without upgrading. See [README](https://github.com/strapi/strapi/blob/develop/packages/utils/upgrade/README.md). - **Package management**: Updates dependencies, installs them, and runs registry-aware upgrades (respects `min-release-age` policies). - **Code transforms**: Applies available codemods for breaking changes in major versions. ### Related --- ## vitest-config ### Purpose Provides a reusable Vitest configuration for the Strapi monorepo unit tests. Internal tooling only; not published for external use. ### Key concepts - **Unit test preset**: Exports `unitPreset` from `vitest-config/presets/unit`. Merge it with package-specific `defineConfig()`. See [README](https://github.com/strapi/strapi/blob/develop/packages/utils/vitest-config/README.md). - **Peer dependency**: Requires `vitest >= 4.0.0` in the consumer's environment. - **Used by**: Internal packages in the monorepo reference this in their vitest configs. ### Related - [Testing](../../../contributing/03-testing/index.md): unit test execution.
loading...