diff --git a/content/docs/api/environment-routing.mdx b/content/docs/api/environment-routing.mdx index 1c26bca997..e20cf466bc 100644 --- a/content/docs/api/environment-routing.mdx +++ b/content/docs/api/environment-routing.mdx @@ -20,7 +20,9 @@ depending on request context. ## Server configuration -Enable scoped route registration in `objectstack.config.ts`: +Environment scoping is a **host** decision, not an application one. The two keys +are declared on the stack's top-level `api` block, but the host that boots the +stack is what decides whether they take effect: {/* os:check */} ```typescript @@ -35,18 +37,54 @@ export default defineStack({ }); ``` + +**On the default `os serve` path this block does not turn scoping on.** A bare +`defineStack()` config carries no instantiated plugins, so the CLI boots +`createStandaloneStack()` and merges that boot result over the authored config +(`mergeBootConfig`). The boot builder ships +`api: { enableProjectScoping: false, projectResolution: 'auto' }` and wins on +exactly those two keys — deliberately, because scoping is not the author's call +on a standalone host. + +The two keys are not overridden the same way: + +- `enableProjectScoping: true` is **contradicted**. The value forwarded to the + REST and dispatcher plugins is `false`, and no scoped routes are registered. +- `projectResolution: 'auto'` is **redundant**. The boot builder pins the same + value, so writing it changes nothing; declaring any *other* strategy here is + silently replaced by `'auto'`. + +Every other authored `api` key — `enforceProjectMembership`, for example — +survives the merge untouched. + + +The block above is live when the CLI does **not** boot the standalone stack: +when the exported config is host-shaped, meaning its `plugins` array already +carries instantiated plugin objects. That is the shape a multi-environment host +assembles, and this open-core CLI supports the `standalone` boot mode only — +cloud / multi-environment hosts ship from a separate distribution. Setting +`OS_MODE=off` (or `bootMode: 'off'` on the exported config) also skips the +standalone boot, but it drops the CLI to its legacy lightweight assembler rather +than producing a scoped standalone host; `bootMode` is additionally not a +declared stack key, so `defineStack()` rejects it as an unrecognized key. + `api` is a declared top-level field on `ObjectStackDefinitionSchema`, so it survives `defineStack`'s strict parsing — you can pass it directly inside the `defineStack({ ... })` call as shown above. (Older stacks that instead spread it onto the exported config object, e.g. `export default { ...stack, api: {...} }`, still work the same way.) The CLI reads the resolved value from the exported -config (`config.api`) when registering the REST and dispatcher plugins. +config (`config.api`) when registering the REST and dispatcher plugins — but it +reads it *after* the boot result has been merged in, which is why the standalone +path forwards the boot builder's scoping decision rather than the author's. The option names are historical for compatibility with existing config files; the route, header, env var, and request context all use `environment`. +Once scoping is enabled by the host, `projectResolution` selects the route +surface: + | Strategy | Behavior | When to use | |:---|:---|:---| | `auto` | Registers both unscoped `/api/v1/...` and scoped `/api/v1/environments/:environmentId/...` routes. | Default migration mode. |