Skip to content

Overview

Environments within a resolved organization + project — one level deeper than ProjectsController. Every route here carries {org}, so ResolveTenantFilter has already resolved + access-checked it before any action runs. {project} is resolved key-or-guid, scoped to the resolved org, via ResolveProjectIdAsync for every action (list needs it too, unlike ProjectsController’s org-only list). {env} (get/update/delete) is then resolved key-or-guid, scoped to the resolved project, via ResolveEnvironmentIdAsync. Dispatched commands/queries always use the resolved GUIDs, never the raw route strings. Writes (create/update/delete) require at least Member (RequireRole); a Viewer attempting one gets a 403 forbidden envelope. Deleting a project’s last environment surfaces the existing LAST_ENVIRONMENT guard (DeleteEnvironmentCommandHandler) as-is — it throws the custom ValidationException, which PublicApiExceptionFilterAttribute maps to a 400 validation_failed envelope automatically, so Delete doesn’t special-case it. ListEnvironmentsQuery has no Skip/Take (unlike ListProjectsQuery) — a project’s environment count is small, so List fetches the full set and paginates in-memory to still expose the standard cursor contract.