---
description: Strapi 5 backend conventions
globs: ["src/**/*.{ts,js,json}", "config/**/*.{ts,js}"]
alwaysApply: false
---

# Strapi 5

## Commands
`npm run develop` (admin + autoreload; content-type builder only works here) · `npm run build` · `npm run start` · regenerate types: `npm run strapi ts:generate-types`.

## Rules
- Content types live in `src/api/<name>/content-types/<name>/schema.json`; components in `src/components/<category>/`. Schema changes through the Content-Type Builder in develop mode or by editing `schema.json` and restarting; never change the database by hand.
- Strapi 5 identifies entries by `documentId` (string), not numeric `id`. Use it in routes, relations and client code.
- Server code uses the Document Service API: `strapi.documents('api::article.article').findMany({ filters, populate, sort, pagination, status: 'published' })`, `.findOne({ documentId })`, `.create`, `.update`, `.publish`. Don't use the deprecated Entity Service or raw Knex unless a measured need exists.
- Keep controllers thin (`factories.createCoreController` + overrides) and put business logic in services. Sanitize output with `this.sanitizeOutput` and validate query input with `this.validateQuery` in custom controllers.
- Population is explicit: request only the relations and fields needed (`populate: { cover: { fields: ['url', 'alternativeText'] } }`). Never `populate: '*'` deeply in public endpoints.
- Access control: Users & Permissions roles for public/authenticated, plus route policies in `src/policies/` for ownership checks. Never expose admin-only fields; mark them `private: true` in the schema.
- Lifecycle side effects (webhooks, cache purge) go in document service middlewares or lifecycles with idempotent handlers.
- Config reads env via `env('KEY')` in `config/*.ts`; secrets (`APP_KEYS`, `JWT_SECRET`, DB creds) only in `.env`, never committed.
- Plugins installed via npm and enabled in `config/plugins.ts`; don't patch `node_modules`.

## Done when
`npm run build` passes, types regenerated if schemas changed, and the affected REST/GraphQL endpoint returns the expected shape.
