# Environments (/docs/management/environments)



A tenant is one [isolation boundary](/docs/concepts/tenancy); an **environment**
is a partition *within* that boundary. Environments let a single tenant separate
`development`, `staging`, and `production` work — each with its own artifact
bindings, provider bindings, credential-visibility rules, and mutation policy —
so that promoting a workflow from staging to production is a governed step rather
than an in-place edit to live config.

Every tenant has exactly **one default** environment; unqualified reads and
writes resolve against it. Production environments are additionally marked with a
`production` flag that tightens what may change and how.

<Callout title="Where it lives">
  Environments are served by `EnvironmentService` under `/api/v2/environments`.
  The pure-domain contracts are in `domain/environment/types.go`; the application
  aggregate is `application/environment/service.go`. Reads require
  `environment:read`, mutations require `environment:write`, and toggling the
  production flag requires the elevated `environment:admin` scope.
</Callout>

## The environment object [#the-environment-object]

<TypeTable
  type="{
  id: { description: &#x22;Output-only identifier.&#x22;, type: &#x22;string&#x22; },
  key: { description: &#x22;Stable slug, unique per tenant. Lowercase, matches ^[a-z][a-z0-9_-]{0,63}$.&#x22;, type: &#x22;string&#x22; },
  display_name: { description: &#x22;Human label; defaults to the key.&#x22;, type: &#x22;string&#x22; },
  purpose: { description: &#x22;development, staging, production, sandbox, demo, preview, or custom.&#x22;, type: &#x22;Purpose&#x22; },
  status: { description: &#x22;provisioning, active, locked, or archived.&#x22;, type: &#x22;Status&#x22; },
  default: { description: &#x22;Whether this is the tenant's default environment. Exactly one is true.&#x22;, type: &#x22;boolean&#x22; },
  policy: { description: &#x22;Protection and visibility rules; see below.&#x22;, type: &#x22;EnvironmentPolicy&#x22; },
  created_at: { description: &#x22;Output-only.&#x22;, type: &#x22;timestamp&#x22; },
  updated_at: { description: &#x22;Output-only.&#x22;, type: &#x22;timestamp&#x22; },
  archived_at: { description: &#x22;Output-only; set when archived.&#x22;, type: &#x22;timestamp&#x22; },
}"
/>

### The protection policy [#the-protection-policy]

`EnvironmentPolicy` is the knob-set that governs whether changes to an environment
are allowed directly, must go through promotion, and how credentials are exposed.
When you create an environment without a policy, Ductor derives sensible defaults
from the `purpose`.

<TypeTable
  type="{
  production: { description: &#x22;Marks the environment as production. Tightens the rules below.&#x22;, type: &#x22;boolean&#x22; },
  mutable: { description: &#x22;Whether the environment accepts artifact changes at all.&#x22;, type: &#x22;boolean&#x22; },
  allow_direct_mutation: { description: &#x22;Whether artifacts can be edited in place (vs. only via promotion).&#x22;, type: &#x22;boolean&#x22; },
  require_promotion: { description: &#x22;Whether changes must arrive through a promotion flow.&#x22;, type: &#x22;boolean&#x22; },
  allow_live_provider_calls: { description: &#x22;Whether steps may call live providers.&#x22;, type: &#x22;boolean&#x22; },
  require_certification: { description: &#x22;Whether changes require certification first.&#x22;, type: &#x22;boolean&#x22; },
  credential_visibility: { description: &#x22;redacted, metadata_only, or break_glass.&#x22;, type: &#x22;string&#x22; },
  member_write_policy: { description: &#x22;admin_only, promotion_only, or scoped_writers.&#x22;, type: &#x22;string&#x22; },
  api_key_policy: { description: &#x22;How API keys may act — e.g. all_environments, no_production_writes, explicit_environments.&#x22;, type: &#x22;string&#x22; },
  region: { description: &#x22;Optional region hint.&#x22;, type: &#x22;string&#x22; },
  data_residency: { description: &#x22;Optional data-residency hint.&#x22;, type: &#x22;string&#x22; },
}"
/>

Read the knobs as a gate sequence rather than a flat list — they answer one
question in order, and a production environment simply has stricter answers:

```mermaid
flowchart TD
  ch([change an artifact]) --> m{mutable?}
  m -->|no| frozen([rejected, frozen])
  m -->|yes| d{allow_direct_mutation?}
  d -->|yes| edit([edit in place])
  d -->|no| p{require_promotion?}
  p -->|no| frozen
  p -->|yes| c{require_certification?}
  c -->|yes| cert[certify first] --> promo([promote in])
  c -->|no| promo
```

<Callout type="info">
  The defaults are purpose-driven. A non-production environment defaults to
  mutable, direct mutation allowed, live provider calls allowed,
  `metadata_only` credential visibility, and `scoped_writers`. A **production**
  environment flips to: no direct mutation, promotion required, certification
  required, `redacted` credentials, and `promotion_only` writes. The domain
  further *rejects* incoherent production policies — for example, a production
  environment that allows direct mutation but does not require certification is
  invalid.
</Callout>

## Lifecycle [#lifecycle]

### Create [#create]

`CreateEnvironment` — `POST /api/v2/environments` (`environment:write`). Only
`key` is required; `display_name`, `purpose`, `policy`, and `default` are
optional. Responses carry metadata and policy only — credentials, API-key
plaintext, and provider tokens are never returned.

```bash
curl -s -X POST https://api.ductor.io/api/v2/environments \
  -H "Authorization: Bearer $DUCTOR_TOKEN" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "key": "staging", "display_name": "Staging", "purpose": "staging" }'
```

### Read, update, list [#read-update-list]

`GetEnvironment` (`GET /api/v2/environments/{environment}`), `ListEnvironments`
(`GET /api/v2/environments`), and `UpdateEnvironment`
(`PATCH /api/v2/environments/{environment}`) cover reads and metadata/policy
edits. `ListEnvironments` filters by `status`, `purpose`, `production_only`, and
`include_archived`. The `{environment}` path segment accepts either the key or
the ID.

### Set the default [#set-the-default]

`SetDefaultEnvironment` — `POST /api/v2/environments/{environment}:set-default`
(`environment:write`) — moves the tenant's default. Because a tenant must always
have exactly one default, this is an atomic switch, not an additive flag.

### Set the production flag [#set-the-production-flag]

`SetProductionFlag` — `POST /api/v2/environments/{environment}:set-production`
(`environment:admin`) — marks or unmarks an environment as production and updates
its policy semantics atomically. This is the one environment operation that
requires `environment:admin` rather than `environment:write`.

```bash
curl -s -X POST "https://api.ductor.io/api/v2/environments/production:set-production" \
  -H "Authorization: Bearer $DUCTOR_TOKEN" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{ "environment": "production", "production": true }'
```

### Archive [#archive]

`ArchiveEnvironment` — `POST /api/v2/environments/{environment}:archive`
(`environment:write`) — archives a **non-default** environment. Archived
environments fail closed for mutable artifact bindings: they stop accepting the
artifact changes an active environment would.

## Readiness and comparison [#readiness-and-comparison]

Environments exist so that promoting change is deliberate. Two operations support
that governance directly.

### Validate readiness [#validate-readiness]

`ValidateEnvironmentReadiness` —
`POST /api/v2/environments/{environment}:validate-readiness` (`environment:read`)
— answers "can this environment safely accept governed changes right now?" It
returns an `EnvironmentReadinessReport`:

<TypeTable
  type="{
  environment: { description: &#x22;The environment being evaluated.&#x22;, type: &#x22;Environment&#x22; },
  ready: { description: &#x22;Whether the environment can accept governed changes.&#x22;, type: &#x22;boolean&#x22; },
  blocking_reasons: { description: &#x22;Hard blockers that make ready false.&#x22;, type: &#x22;repeated string&#x22; },
  warnings: { description: &#x22;Non-blocking advisories.&#x22;, type: &#x22;repeated string&#x22; },
}"
/>

Use it as the gate before a promotion: a non-empty `blocking_reasons` means the
target environment is not in a state to receive the change.

### Compare two environments [#compare-two-environments]

`CompareEnvironments` — `GET /api/v2/environments:compare?from={a}&to={b}`
(`environment:read`) — diffs the artifact bindings between two environments and
returns redacted `added` / `removed` / `changed` bindings, an `allowed` verdict,
and any `blocking_reasons`. This is what you inspect before promoting to see
exactly what would move.

```bash
curl -s "https://api.ductor.io/api/v2/environments:compare?from=staging&to=production" \
  -H "Authorization: Bearer $DUCTOR_TOKEN" \
  -H "X-Tenant-ID: $DUCTOR_TENANT_ID"
```

## Bindings [#bindings]

An environment is bound to the artifacts and providers it runs.

* **Artifact bindings** — `ListEnvironmentArtifactBindings`
  (`GET /api/v2/environments/{environment}/artifact-bindings`, `environment:read`)
  — list the redacted artifacts pinned to the environment: workflow definitions,
  connector deployments and connections, **runtime-config refs**, policy packs,
  promotion candidates, schedule triggers, and more. The runtime-config ref kind
  is how a resolved [runtime configuration](/docs/management/configuration) is
  attached to an environment. Filter by `artifact_kind` and `artifact_id`.
* **Provider bindings** — `ListProviderEnvironmentBindings`
  (`GET /api/v2/environments/{environment}/provider-bindings`, `environment:read`)
  — list explicit provider bindings for sandbox, staging, and production
  environments. Provider credentials and tokens are never returned. Filter by
  `canonical_provider_key`, `purpose`, and `status` (`available`, `needs_review`,
  or `unsupported`).

<Callout type="warn">
  Provider-binding validation enforces environment hygiene: a production binding
  may not execute a sandbox or staging provider key, and sandbox/staging bindings
  must record a distinct provider key when marked `available`. These guards keep a
  "production" environment from quietly pointing at non-production infrastructure.
</Callout>

## Operations at a glance [#operations-at-a-glance]

| Operation              | Method & path                                                | Scope               |
| ---------------------- | ------------------------------------------------------------ | ------------------- |
| List environments      | `GET /api/v2/environments`                                   | `environment:read`  |
| Create environment     | `POST /api/v2/environments`                                  | `environment:write` |
| Get environment        | `GET /api/v2/environments/{environment}`                     | `environment:read`  |
| Update environment     | `PATCH /api/v2/environments/{environment}`                   | `environment:write` |
| Set default            | `POST /api/v2/environments/{environment}:set-default`        | `environment:write` |
| Set production flag    | `POST /api/v2/environments/{environment}:set-production`     | `environment:admin` |
| Archive                | `POST /api/v2/environments/{environment}:archive`            | `environment:write` |
| Validate readiness     | `POST /api/v2/environments/{environment}:validate-readiness` | `environment:read`  |
| Compare                | `GET /api/v2/environments:compare`                           | `environment:read`  |
| List artifact bindings | `GET /api/v2/environments/{environment}/artifact-bindings`   | `environment:read`  |
| List provider bindings | `GET /api/v2/environments/{environment}/provider-bindings`   | `environment:read`  |

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Tenancy" href="/docs/concepts/tenancy">
    The isolation boundary environments partition.
  </Card>

  <Card title="Configuration" href="/docs/management/configuration">
    Runtime config, resolved views, and the config refs bound to an environment.
  </Card>

  <Card title="Secrets & variables" href="/docs/management/secrets-and-variables">
    Environment-partitioned credential and config storage.
  </Card>
</Cards>
