diff --git a/docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md b/docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md new file mode 100644 index 0000000..36df1f7 --- /dev/null +++ b/docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md @@ -0,0 +1,85 @@ +--- +title: "Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder" +date: 2026-07-04 +category: docs/solutions/conventions +module: "grav / plugin configuration" +problem_type: convention +component: tooling +severity: high +applies_when: + - "Editing functional config for any GPM-managed Grav plugin" + - "user/plugins/ is gitignored and only pages/config/accounts/themes are tracked" + - "Preparing a fresh install or production cutover" + - "A plugin behaves correctly locally but ships with only default config on deploy" +related_components: + - "grav" + - "gpm" + - "api plugin" + - "content repo" + - "deployment" +tags: + - grav + - plugin-config + - gpm + - config-override + - gitignore + - deployment + - api-plugin +--- + +# Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder + +## Context + +Grav resolves a plugin's config by deep-merging two layers: the plugin's own shipped file `user/plugins//.yaml` (installed by GPM, part of the package) and the tracked override `user/config/plugins/.yaml` (which wins). In this project the content repo tracks only `pages/`, `config/`, `accounts/`, `themes/`; `user/plugins/` and `user/data/` are gitignored (GPM manages plugin *code*). So any functional config a developer edits into a plugin's own `user/plugins//.yaml` is invisible to version control. + +It was this gap that left the `api` plugin unconfigured on the fresh prod install. Its `enabled`/`route`/`session_enabled`/cors/rate_limit config existed only in the untracked plugin folder locally, while the committed `user/config/plugins/api.yaml` held only a runtime `popularity.salt`. The local machine worked because the plugin folder had been hand-edited; every fresh environment got only the plugin's shipped defaults. + +## Guidance + +Put **functional** plugin configuration in the TRACKED override `user/config/plugins/.yaml`. Grav deep-merges it over the plugin's shipped defaults, so it need only carry the keys that must differ (or the full config, for clarity). Keep **secrets and per-install generated values** OUT of the tracked file — JWT secrets, salts, encrypted tokens belong in gitignored `*-private.php` companion files (e.g. `api-private.php`, `security-private.php`) or should be regenerated per-install. + +Never rely on edits to the plugin's own `user/plugins//.yaml`: it is gitignored (won't deploy) and is overwritten on the next `php bin/gpm update`. + +Concrete before/after, using the `api` plugin: + +```yaml +# WRONG: user/plugins/api/api.yaml (gitignored, GPM-managed, wiped on update) +enabled: true +route: /api +auth: + session_enabled: true +``` + +```yaml +# RIGHT: user/config/plugins/api.yaml (tracked, deploys, survives gpm update) +enabled: true +route: /api +version_prefix: v1 +auth: + session_enabled: true + # JWT secret intentionally NOT here — it lives in the gitignored api-private.php +``` + +## Why This Matters + +Reproducible deploys: a fresh clone or `make remote-install-` must produce a working site from the repo alone. Config stranded in the gitignored plugin folder silently yields a plugin with only its shipped defaults on every new environment — which, for a plugin whose behavior depends on non-default config, means it's misconfigured or effectively off. On prod the `api` plugin's route/auth simply didn't work. + +The failure is silent and per-environment: it works on the developer's machine (where the plugin folder was hand-edited) and breaks everywhere else. `gpm update` compounds it by wiping the folder edit even locally, so the "working" state is not just unshared — it is also unstable on the one machine that had it. + +## When to Apply + +- Any time you configure a Grav plugin whose non-default settings must work on a server (prod/test) or survive a plugin update. +- Especially for plugins whose function depends on config: `api` (route/auth/cors), `admin2`, `flex-objects`, form/media settings, etc. +- When auditing a fresh-install failure: check whether the "working" local config actually lives in a tracked path (`git ls-files user/config/plugins/.yaml`) or was stranded in `user/plugins//`. + +## Examples + +- **api plugin (this project):** the functional config was moved from the untracked `user/plugins/api/api.yaml` into the tracked `user/config/plugins/api.yaml`, then `make content-push` + `make remote-fetch-content-` deployed it. The JWT secret stayed in the gitignored `api-private.php`. +- **Quick audit command:** `git -C user ls-files config/plugins/` shows exactly which plugin configs are tracked/deployable; anything you rely on that isn't listed is a latent fresh-install failure. + +## Related + +- `docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md` — the config gap documented here was the *other* latent problem surfaced in that same investigation: the `api` plugin also had to be *installed* first before any config could take effect. The install gap (GPM version floor) and this config-tracking gap compounded each other on the fresh prod environment. +- `docs/working/git-sync-notes.md` — the related third config location: on prod, Grav Admin saves config into the per-environment tree `user/env//config/`, which is *also* untracked. Same "config that doesn't reach the repo" family. +- **CLAUDE.md §0 (plugin-management model):** only `pages/`, `config/`, `accounts/`, `themes/` are tracked in the `user/` repo; `plugins/` and `data/` are gitignored and GPM-managed. That tracking boundary is exactly why functional config must live under `config/plugins/`, not in the plugin's own folder. diff --git a/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md b/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md index b58c11c..231bd18 100644 --- a/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md +++ b/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md @@ -91,4 +91,4 @@ This is one of three independent gotchas from the same **2026-07-04 Grav 2.0.4 p - `docs/solutions/integration-issues/grav-double-content-encoding-garbage-page.md` — sibling: garbage-rendered pages from a double `Content-Encoding` header on a non-FastCGI host. Different root cause (HTTP compression), same deploy. - `docs/solutions/test-failures/new-user-grants-api-not-admin-on-admin2.md` — sibling: an authenticated account is denied an admin-gated page because `login new-user` auto-detect granted `api.*` but not `admin.*`. Different root cause (permission provisioning), same admin2/api area. - `docs/working/plans/2026-07-04-grav-2.0.4-upgrade.md` — the upgrade plan whose Global Constraints spell out the GPM version floors (`grav >=2.0.4`, `api >=1.0.6`) that cause the "package not found" on an rc core. -- **api plugin config** in the tracked `user/config/plugins/api.yaml` (`enabled` / `route` / `session_enabled`) — must ship on a clean clone; necessary but not sufficient (the plugin must be installed first). See CLAUDE.md §0 "Plugin management". +- `docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md` — the *other* latent problem from this same investigation: the api plugin's functional config (`enabled` / `route` / `session_enabled`) must live in the tracked `user/config/plugins/api.yaml` to deploy at all. Necessary but not sufficient here (the plugin must be installed first), but a durable convention in its own right.