strapi/strapi
github.com/strapi/strapi ↗ · 3998 files · 17.6 MB · scan capped
Open-source headless CMS, self-hosted or Cloud you're in control.
Overview
Strapi is a headless CMS for developers building content APIs and editors managing structured content through a React admin application. At commit 93476f908c08d6d3d5215c06483829e3c6da1b32, its Node/Koa runtime assembles content models, plugins, authentication strategies, and database services through a central Strapi container. Its distinctive persistence model separates document identity from locale and draft/published rows, with the Document Service coordinating components, relations, transactions, and publication events. This brief reflects bounded source inspection, including startup, REST creation, authentication, publication, and media persistence flows.
System map
The runtime centers on a Strapi dependency container, with a CLI entrypoint and separate HTTP/admin application boundaries. Content persistence passes through the Document Service into a Knex-backed database package. Plugins and providers assemble application capabilities, while Nx coordinates package builds and checks. Architecture diagrams and contributor documentation are separately built artifacts.
- The executable bin delegates to runCLI; createCLI loads command factories, and the start action resolves appDir/distDir before invoking createStrapi(...).start(). TypeScript projects use an existing compiled output fast path or resolveOutDir fallback.
- Strapi extends Container, loads configuration in its constructor, registers internal services, and invokes provider initialization. Its container owns server, authentication, Document Service, database, request context, and event hub services.
- createServer creates a Koa application and HTTP server, separates admin and content-api routers, installs request context, and exposes /_health with status 204. Content API routing defaults to the /api prefix.
- @strapi/admin exports distinct strapi-admin and strapi-server entrypoints. The browser StrapiApp constructs Redux state from stored preferences/token and renders a RouterProvider, keeping browser application state separate from server services.
- Database constructs dialect, Knex connection, metadata, schema, migration, lifecycle, entity-manager, and repair services. init loads model metadata and validates the database; query(uid) rejects unknown models before returning a repository.
- Plugin loading resolves each enabled package's strapi-server export, loads its implementation, merges defaults/configuration, applies application extensions, and registers it. Email providers are dynamically resolved and initialized with providerOptions/settings.
- The root is a private Yarn workspace using yarn@4.12.0; build targets run through Nx, and core packages emit Rollup code plus TypeScript declarations. Nx caches build outputs in package dist directories.
- Architecture is a separate LikeC4 workspace with HTML-build and diagram-export commands; contributor docs use Docusaurus. The contributor-doc workflow builds docs and publishes docs/build to GitHub Pages on develop pushes or manual dispatch.
Feature catalog
The inspected surfaces support model-driven APIs, browser editing, publication/localization, identity management, and media operations. Optional GraphQL and licensed editorial capabilities extend the base CMS. CLI commands cover application operation and data movement, while MCP provides a separate authenticated automation surface. These eight capability groups describe the inspected implementation; the bounded inventory is not an exhaustive screen or route catalog.
- Generated REST routes distinguish singular single types from plural collection types and expose read/write operations with request/response schemas. The GraphQL plugin builds a Content API schema and initializes Apollo only when that schema is nonempty.
- Content Manager registers a navigation entry and content-manager/* routes for collection lists, single/collection editing, cloning, list/edit/component configuration, permission errors, history, and preview. Document action UI exposes save and publish shortcut hints.
- Content-Type Builder exposes admin endpoints to list, inspect, create, update, and delete content types and create components. Mutation routes combine admin permission policies with isDevelopmentMode middleware.
- Document Service supports create, clone, update, delete, count, and reads; publish/unpublish/discardDraft are exposed only for draft-and-publish types. Localized operations apply a default locale, and multi-locale lookup accepts '*' to avoid selecting a single locale.
- Users & Permissions provides local login/registration and provider callbacks. Login rejects disabled providers, invalid passwords, unconfirmed accounts when confirmation is required, and blocked users; refresh mode supports session responses, while session-list tests specify authentication and current-session display behavior.
- Media permissions cover upload, crop/details/replace/delete, download, copy-link, and view configuration. Provider adapters support streams or buffers; inspected implementations include local filesystem storage, AWS S3 SDK integration, and Cloudinary stream/chunked uploads.
- Review Workflows and Content Releases expose their runtime modules under separate EE feature checks, retaining content types when disabled. Audit Logs routes/controllers require both admin.auditLogs.enabled and the audit-logs license feature; contributor docs describe filtered CSV export with read/export permissions.
- CLI factories cover admin-user management, configuration dump/restore, diagnostics, generation, build/develop/start, import/export/transfer, OpenAPI, and cloud commands; exports support tar or unpacked directories. Automation also includes configured cron tasks, event-driven webhooks, and MCP tool/prompt/resource registration.
Data flow
HTTP data crosses authentication and policy boundaries before controller validation and sanitization. The Document Service translates document parameters into locale/status-specific database operations and owns transaction coordination. Media bytes, metadata, session state, and event payloads have distinct persistence/output boundaries. Browser state and MCP transport responses follow separate paths from ordinary REST response handling.
- Endpoint composition attaches route information, authenticates, authorizes, runs policies and route middleware, then invokes the controller. returnBodyMiddleware copies a returned value into ctx.body only when the controller has not already set a body.
- Collection create requires an object body.data, validates/sanitizes query and input, delegates to the content service, sanitizes the returned entity, and sets 201. Base controller sanitizers receive request auth; REST strict-parameter configuration is passed into validation options.
- Collection services default fetch parameters to status:'published', whereas Document Service findMany applies defaultToDraft before status/locale lookup and query translation. API reads and direct Document Service reads therefore have different defaults.
- transformParamsToQuery allowlists query parameters, merges internal lookup into where, removes publication-only top-level parameters, and expresses publication filtering through query.filters so nested populate queries receive cohort conditions using their own model UID.
- Entry creation validates entity data, creates/assigns components, applies attribute transforms, and writes through db.query(uid).create. Database creation processes defaults, inserts a row, attaches relations, rereads selected/populated output, and runs afterCreate lifecycles.
- Document events register an onCommit callback, then reload/populate applicable entries, sanitize the payload, and emit through eventHub. Webhook listeners enqueue those events and POST JSON with configured headers and a ten-second abort timeout.
- Uploads validate/enhance inputs and write provider bytes before creating FILE_MODEL_UID metadata and emitting MEDIA_CREATE; temporary working directories are removed in finally. Read methods sign file URLs through fileService for private-provider output.
- Refresh-token generation persists active session records with origin, user/device identity, idle expiry, and absolute expiry before signing JWTs. MCP independently authenticates an admin bearer token, installs user/auditSource context, and hands the request to a per-request transport that writes directly to the HTTP response.
Behavior traces
The following traces were followed by reading implementations in execution order. Startup proceeds through registration, database synchronization, middleware/routing setup, and listening. REST creation continues through sanitization, document transforms, entry creation, and relation persistence. Authentication branches into legacy or refresh issuance, while publishing and uploading cross additional state boundaries.
- Startup begins at bin/strapi.js → runCLI → createCLI command factories → start action → createStrapi → new Strapi. The constructor loads configuration/internal services; start calls load only when isLoaded is false.
- load → register → bootstrap → isLoaded=true → listen. Registration invokes providers, plugin and user hooks; bootstrap runs db.init/schema.sync, middleware/routing, permissions and lifecycle hooks. listen mounts routers and binds a configured socket or host/port, then postListen logs startup and sends telemetry.
- REST creation follows generated POST /pluralName → scoped route registration → compose-endpoint authentication/authorization/policies → collection controller create. The controller validates query/body.data, sanitizes input, calls service.create, sanitizes output, and returns a transformed response with status 201.
- service.create → documents(uid).create → transaction-wrapped repository create → validateParams/status/locale transforms → entries.create → entity validation/components/transforms → db.query.create. Database insertion attaches relations and returns a reread entity; an explicitly published D&P create additionally calls publish before returning.packages/core/core/src/core-api/service/collection-type.ts#L65-68packages/core/core/src/services/document-service/repository.ts#L411-434packages/core/core/src/services/document-service/repository.ts#L776-785packages/core/core/src/services/document-service/entries.ts#L92-111packages/core/database/src/entity-manager/index.ts#L342-372
- Local login follows POST /auth/local → auth.callback → provider-enabled check → validateCallbackBody → user lookup → user.validatePassword/bcrypt.compare → email-confirmation and blocked checks. Legacy mode issues a JWT; refresh mode generates refresh then access tokens and returns either an HTTP-only refresh cookie or a refreshToken field.
- A protected Content API request runs authenticate → jwt.getToken → jwt.verify → fetchAuthenticatedUser → confirmation/block checks → role permissions → generateAbility. It stores user/session context; verify permits authenticated unscoped access or requires every configured scope to satisfy ability.can.packages/core/core/src/services/auth/index.ts#L99-120packages/plugins/users-permissions/server/src/services/jwt.js#L13-28packages/plugins/users-permissions/server/src/services/jwt.js#L66-101packages/plugins/users-permissions/server/src/strategies/users-permissions.js#L12-62packages/plugins/users-permissions/server/src/strategies/users-permissions.js#L85-112
- publish rejects an empty documentId, selects locale-matching drafts/old published rows, loads relation mappings, deletes old published versions, adds firstPublishedAt, creates published entries, synchronizes unidirectional/bidirectional/self relations, and schedules entry.publish events. The public publish method is transaction-wrapped and exists only for D&P types.
- Media upload follows upload → temporary directory creation → configured-size batches → enhanceAndValidateFile → uploadFileAndPersist → provider size check → image/nonimage upload → add metadata → MEDIA_CREATE. The local stream provider writes under static.public/uploads and sets a public URL; upload finally removes temporary work.
Testing signals
Tests and CI were inspected, not executed; test existence does not establish a passing result. Jest remains central, with separate Vitest projects and Playwright browser suites. Inspected API tests assert persisted document behavior, while authentication unit tests use mocked database/session dependencies. CI configuration shows conditional execution, multiple runtime/database targets, and explicit browser-coverage limitations; no comprehensive coverage measurement was performed.
- The Jest unit preset selects __tests__ files, transforms through SWC, excludes .vitest.test.ts, and disables coverage unless requested. Core's Jest config specializes discovery to *.test.ts.
- Vitest has a root project aggregator and a core package config merged with the shared unit preset. CI invokes yarn test:unit:vitest separately from Nx/Jest unit tests, so these are distinct inspected test lanes.
- API tests use Jest's Node environment and *.api.js/ts discovery. Inspected create tests build fixtures, call strapi.documents, and check database row count, default/explicit locales, draft status, published creation, and ignored caller-supplied publishedAt.
- Inspected publish API tests assert creator-field presence, equal draft/published locale counts, and no extra published variants after publishing twice. These source assertions cover selected variant persistence properties, but were not run during this investigation.
- JWT tests mock configuration, session manager, and database calls, exercising refresh issuance and error cases. Auth-session controller tests assert refresh-only availability, authentication requirements, and sanitized/current-session output; these mocks do not themselves validate real cookie transport or database integration.
- Frontend Jest uses admin-test-utils setup/environment and SWC transforms. Playwright create-content specs reset/import fixtures, log in, save/publish content, and reorder components; base configuration uses one worker, CI retries, and trace capture on first retry.
- CI builds on Node 22/24/26, uses path-filter outputs and Nx affected targets for selected checks, and defines sharded PostgreSQL/MySQL API jobs. These workflow definitions were inspected rather than executed or checked against run history.
- Shared CE E2E jobs span Chromium/WebKit/Firefox, but current and legacy media-library jobs explicitly select Chromium. Workflow comments identify WebKit drag/drop helper limitations, unknown Firefox media-suite behavior, and parked folder drag/drop specs; raw E2E failure handling also depends on the Trunk upload outcome.
Risk map
These are source observations and explicitly labeled hypotheses, not confirmed vulnerabilities. Startup and publication are persistence hotspots because they synchronize schemas or replace related rows. Authentication and MCP rely on different enforcement paths, and upload provider operations cross a storage boundary outside database transactions. Configuration compatibility, global state, and CI conditions require focused review when changing these areas.
- Startup has a broad database blast radius: bootstrap runs schema.sync, conditional orphan-morph repairs, and a one-time join-table repair. The registered discard-drafts migration clones/remaps persisted data, and its down implementation throws 'not implemented'.
- Publishing deletes old published rows before recreating variants and synchronizing multiple relation classes. Correctness depends on retaining repository transaction wrapping; Database.transaction reuses an existing transaction for nested calls and rolls back rejected callbacks.
- auth:false bypasses ordinary authenticate and verify. MCP deliberately uses this route configuration but performs admin-token authentication inside handlePost; edits must preserve that independent boundary and its ability/user propagation.
- Hypothesis: a metadata-write failure after successful provider upload could leave stored bytes without a corresponding media row. The inspected uploadFileAndPersist/add sequence writes provider bytes before database metadata, while upload's finally cleans temporary work rather than showing provider compensation. (inferred)
- Hypothesis: callers passing absolute paths to StrapiFS could escape the intended application root. normalizePath strips leading relative traversal and then calls path.resolve(app.root, normalizedPath), without an explicit containment check in that function; exploitability and caller reachability were not established. (inferred)
- Global instance coupling remains despite guidance preferring injection: createStrapi assigns global.strapi, Document Service repository/common use ambient strapi, and destroy deletes the global and removes all process listeners. This is a source-observed isolation hotspot for multi-instance or lifecycle changes.
- GraphQL warns when depthLimit/maxLimit are unbounded or invalid, but preserves depthLimit as configured rather than imposing a finite fallback. Landing-page configuration can explicitly enable the local playground in production; custom Apollo configuration is merged into defaults.
- The TypeScript CI condition checks global or backend twice and omits frontend, although the frontend filter independently matches admin source changes. This is a source-observed job-selection mismatch; actual skipped runs or downstream defects were not verified.
Extension points
Extensions are intended to enter through factories, plugin contracts, registries, and lifecycle hooks. Backend and browser plugins have separate registration interfaces. Document middleware, provider capabilities, GraphQL extension configuration, and MCP definitions offer additional integration boundaries. Several APIs enforce registration timing or preserve default behavior through explicit fallback logic.
- createCoreController and createCoreService accept objects or callbacks receiving {strapi}, fill missing methods from defaults, and install base implementations as prototypes. createCoreRouter supports prefix, per-action config, only/except filtering, and route type selection.
- Backend plugins can provide register/bootstrap/destroy, configuration defaults/validation, routes, controllers, services, policies, middleware, and content types. Application extensions overlay plugin schemas and may transform the loaded plugin through strapi-server.js.
- The registries provider installs controllers/services/modules/plugins/custom-fields/APIs and sanitizer/validator registries. It creates beforeSync/afterSync hooks, while Strapi invokes application lifecycle functions with {strapi}.
- strapi.documents.use registers Document Service middleware around cached per-content-type repositories. The wrapper excludes updateComponents and omitComponentData from normal middleware interception.
- Browser plugins register through StrapiApp, adding reducers, menu/settings links, routes, widgets, and hooks. Plugin register runs before custom register; Content Manager demonstrates adding its reducer and lazy route subtree.
- Upload providers may implement uploadStream or upload, replaceStream or replace, with delete-then-upload fallback when replacement capabilities are absent. Email providers expose init(options, settings) returning send and optional verification/lifecycle/capability methods.
- GraphQL extension.use accepts configuration objects or factories receiving strapi, nexus, and typeRegistry. generate combines type definitions, Nexus types/plugins, resolvers, and resolver configuration, alongside a shadowCRUD manager.
- MCP exposes tool/prompt/resource definition registries but rejects registration once status leaves idle. CLI extensions enter the commands factory array, while authentication strategies register per route type and may provide a verify function.
Contributor handoff
Use this brief as an anchored navigation aid for the pinned commit, then inspect the owning package and tests before changing behavior. Tests and CI were inspected, not executed; the inventory and coverage assessment are bounded. Preserve transaction, authorization, provider, and license boundaries, and distinguish current implementation from historical contributor descriptions. This investigation authorized public-source reads only and performed no repository execution or edits.
- Begin with repository conventions and the owning manifest: contributor changes target develop, shared types come from @strapi/types, and package scripts define available checks. Tests and CI cited here were inspected, not executed; their definitions establish intended checks rather than passing results.
- Treat examples as development sandboxes, with the documented examples/complex migration-fixture exception. Production framework fixes belong in the relevant packages; migration filters explicitly include the fixture and tests/migration paths.
- Before lifecycle edits, read Strapi.start/load/register/bootstrap and providers/registries. isLoaded becomes true after bootstrap, which performs database synchronization before routing and plugin/provider/user bootstrap hooks.
- For content changes, read controller/service boundaries, document-service/repository.ts, entries.ts, and transaction/event helpers. Preserve documentId versus row-id semantics, published API defaults versus draft Document Service defaults, and after-commit event emission.
- For security-sensitive changes, inspect endpoint composition, controller sanitization, users-permissions strategy/JWT mode, and MCP handlePost. Preserve confirmation/block checks and scope verification; MCP authentication lives inside the handler rather than ordinary route authentication.packages/core/core/src/services/server/compose-endpoint.ts#L85-93packages/core/core/src/core-api/controller/index.ts#L51-70packages/plugins/users-permissions/server/src/strategies/users-permissions.js#L31-61packages/plugins/users-permissions/server/src/strategies/users-permissions.js#L85-112packages/core/core/src/services/mcp/handlers/handlePost.ts#L32-53
- For admin/provider edits, preserve plugin registration interfaces, protected Content Manager routes, temporary-upload cleanup, and stream/buffer fallbacks. Keep licensed feature gates and retained content types intact when Review Workflows or Content Releases are disabled.packages/core/content-manager/admin/src/router.tsx#L60-94packages/core/admin/admin/src/StrapiApp.tsx#L243-267packages/core/upload/server/src/services/upload.ts#L255-289packages/core/upload/server/src/services/provider.ts#L13-32packages/core/review-workflows/server/src/index.ts#L9-26packages/core/content-releases/server/src/index.ts#L10-28
- In a separately authorized execution task, start with matching package tests, then API or Playwright behavior checks as appropriate. Root scripts separate Jest/Nx, Vitest, API, frontend and type-check lanes; repository guidance requires regenerating integration apps rather than trusting stale fixtures.
- Verify drift-prone documentation against source: AGENTS recommends Node 22–26 while package.json allows 20–26 and pins Yarn 4.12.0; CONTRIBUTING still states Yarn v1.2.0+. The Content Manager introduction describes a pre-V5 package location, while current source registers it from its own package.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 4428 | 83% |
| JavaScript | 913 | 17% |
| JSON | 776 | docs/config |
| Markdown | 214 | docs/config |
| YAML | 53 | docs/config |
| Shell | 7 | 0% |
| HTML | 3 | docs/config |
| CSS | 1 | docs/config |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (2443 files)
Repository structure
| Top-level path | Files |
|---|---|
packages/ | 2443 |
tests/ | 742 |
examples/ | 318 |
docs/ | 177 |
.github/ | 117 |
templates/ | 68 |
scripts/ | 37 |
architecture/ | 36 |
Docs found: AGENTS.mdCLAUDE.mdCONTRIBUTING.mdREADME.mddocs/README.mddocs/agents/domain.mddocs/agents/issue-tracker.mddocs/agents/triage-labels.mddocs/docs/docs/00-intro.mddocs/docs/docs/01-core/admin/00-intro.md
Package
package.json — strapi, 80 declared dependencies.
@babel/core@babel/eslint-parser@babel/preset-react@commitlint/cli@commitlint/config-conventional@commitlint/prompt-cli@nx/js@playwright/test@rollup/plugin-commonjs@rollup/plugin-dynamic-import-vars@rollup/plugin-image@rollup/plugin-json
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructionsCLAUDE.md— Claude instructions.github/workflows/caniuse.yml— GitHub Actions workflow.github/workflows/changeFreeze.yml— GitHub Actions workflow.github/workflows/checks.yml— GitHub Actions workflow.github/workflows/clean-up-pr-caches.yml— GitHub Actions workflow.github/workflows/close_stale_issues.yml— GitHub Actions workflow.github/workflows/commitlint.yml— GitHub Actions workflow.github/workflows/community-label.yml— GitHub Actions workflow
Structural health signals
Average score 8.5/10 across 96 analyzed source files; 6 hotspots surfaced.
packages/core/content-type-builder/admin/src/components/DataManager/reducer.ts— 1408 lines, score 5.7 (Large file, Deep nesting)packages/core/content-manager/admin/src/pages/EditView/components/DocumentActions.tsx— 1458 lines, score 5.7 (Large file, Deep nesting)packages/core/admin/server/src/services/api-token.ts— 1417 lines, score 5.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
93476f908c082026-10-02 — docs: architecture modeling (#26927)055b77de79172026-10-02 — Merge pull request #27860 from strapi/security/upgrade-vite-6eadb0c9d82a22026-09-29 — security(deps): upgrade vite to fix server.fs.deny bypass on windowsbc653e8ec3e82026-10-02 — fix(utils): explain that status is root-only when used in nested populate (#27857)aa939d34f59a2026-10-01 — Merge pull request #27869 from strapi/chore/bump-radix-toolbar