# Organize projects and environments

Source: https://docs.imprezahost.com/project-environments.html

Use the portal, REST API or hosted MCP. Local MCP tools require MCP 0.36.0 or later.

A project groups environments such as staging and production. Each environment contains named components linked to existing catalog or custom applications. Linking does not deploy, change networking, copy variables or share credentials. Existing applications retain their IDs, history and runtime configuration.

## Before you start

Use an account credential with `read` scope for inventory and `deploy` scope for changes. Named hosted MCP tools require unrestricted account credentials. Portal viewers can read the inventory; managing services requires the corresponding team permission and session CSRF token.

Applications must currently belong to your account, have a non-revoked agent and valid current server ownership. Attach only running or failed applications. Use lowercase names beginning with a letter, at most 48 characters, containing letters, numbers and internal hyphens.

## Portal workflow

In **My Apps → Projects and environments** (below the App catalog, in both the Simple and Advanced views), create a project, create its environments and open the project to inspect its services. Select an environment and existing app, give the component a name and choose a role: web, worker, database or cache. Roles are organizational labels; selecting database does not validate an engine or provision a database.

Select a project name to view its environments, linked apps, recorded deployment results, active previews, primary domains, backup policy and team permissions. Variable groups show names only. Team permissions apply to the account. Certificate status is shown only when reported by the metadata; the current portal does not report certificate telemetry. Use **Promote configuration**, **Deploy environment in order**, **Create preview** or **App settings** to reach the existing controls. Opening the page does not deploy an app or create a backup.

Use the same component name and role in staging and production for the corresponding app. One app belongs to at most one environment, and a component name is unique within its environment. A conflicting association is refused. **Detach** requires confirmation and removes only the association; the application and its data remain.

The same panel edits [variable groups](#variable-groups), reviews and applies a [configuration promotion](#config-promotions) and starts an [ordered environment deploy](#environment-deploys). Click **Open environments** on a project first, so the environment lists are filled.

## MCP workflow

1. `impreza_create_project` with `name: "store"` returns `project_id`.
2. `impreza_create_environment` with that `project_id` and `name: "staging"` returns `environment_id`. Repeat for production.
3. `impreza_attach_environment_service` with `environment_id`, `deployment_id`, `component: "web"` and `role: "web"` links an existing application.
4. `impreza_list_projects` without arguments returns project names and IDs. Pass `project_id` to load environments and authorized service metadata. Variable values are never returned.
5. To move an app, first call `impreza_detach_environment_service` with `environment_id`, `deployment_id` and `confirm: true`, then attach it to the intended environment.

These projects organize running applications. [Saved deployment plans](project-plans.html) describe reusable deploy inputs and remain a separate feature.

## REST requests

Use the standard authenticated API headers. IDs in these examples are placeholders; use the returned IDs.

```http
POST /v1/platform/projects
Content-Type: application/json

{"name":"store"}
```

```http
POST /v1/platform/projects/prj_RETURNED_ID/environments
Content-Type: application/json

{"name":"staging"}
```

```http
POST /v1/platform/environments/env_RETURNED_ID/services
Content-Type: application/json

{"deployment_id":"dpl_EXISTING_APP","component":"web","role":"web"}
```

`GET /v1/platform/projects` lists projects. `GET /v1/platform/projects/{id}` loads the selected inventory. `POST /v1/platform/environments/{id}/services/detach` accepts only `deployment_id` and boolean `confirm: true`.

## Verify and retry

Read the selected project after a write, especially if the response was lost. Repeating an identical project/environment name or association returns the existing identity. An app cannot be silently moved or relabeled by retrying an attach.

The inventory rechecks current ownership. Transferred, revoked or uninstalled applications are not exposed as active service entries. The owner of the organizational record can detach stale metadata by known ID without touching the workload. Pending application operations block membership changes.

Grouped [image promotions](image-promotions.html) require the same project, component and role across two different environments. A changed association invalidates the prepared promotion. Ungrouped source and destination apps can still use the standalone promotion flow.

## Limits and errors

- Up to 50 projects per account, 20 environments per project and 100 linked services per environment. Load one project at a time for service details.
- Invalid fields or names: `400`. Missing permissions: `403`. Unowned or unavailable resources: `404`. Conflicting association, busy app or stale promotion: `409`. Quota: `429`. Storage failure: `503`; read before retrying.
- No automatic app provisioning. Inherited variable groups, dependency wiring, deployment ordering and rename/delete arrived in a later increment — see [environment automation](#f44).
- Linking a database/cache does not yet create a connection binding or grant network access. Image promotion can interrupt traffic and does not migrate or roll back database contents.

[PostgreSQL connections](service-bindings.html) use these associations and require agent 0.6.13 or later.


## Environment comparison and traffic switch

Available through the portal, REST API and hosted MCP. Local MCP requires **0.38.0+**. Traffic switching requires agent **0.6.16+**; update existing agents explicitly before using it.

Comparison pairs services by component within one project. `GET /v1/platform/environments/{id}/compare?other=env_OTHER_ID` requires `read` scope and unrestricted account credentials. The portal's comparison view and hosted MCP `impreza_compare_environments` return variable **names** grouped as equal, different or present on one side, source descriptors and connection state. Values and credential digests are never returned. A revoked server or lost current ownership hides its services.

A traffic switch moves one hostname between two running custom apps on the **same agent**. The destination must have no hostname; previews cannot participate. Use the portal review, or `impreza_prepare_traffic_switch` with `deployment_id` and `target_deployment_id`, then `impreza_get_traffic_switch` and `impreza_apply_traffic_switch`. Applying requires `deploy` scope, unrestricted account credentials, `switch_id`, the exact `review_digest` and `confirm: true`. REST equivalents are `POST /v1/platform/deployments/custom/{id}/prepare-traffic-switch`, `GET /v1/platform/traffic-switches/{id}` and `POST /v1/platform/traffic-switches/{id}/apply`.

The agent must announce `traffic-switch-v1` and controlled-deployment support. The target must be running and any configured Docker healthcheck must be healthy. A local HTTPS route probe rejects connection errors and 5xx responses; it does not certify application correctness or public DNS/TLS. The source stays running. Brief interruption remains possible.

Both apps are reserved against conflicting changes. Ownership and the reviewed configuration are checked again; stale reviews return `409`, unowned resources `404`. A verified receipt updates hostname and saved routing variables together. On an ordinary failure the agent attempts rollback. If recovery cannot be verified, or the agent crashes during the move, **do not retry blindly**: routing must be reconciled before further app changes. An interrupted local proxy transaction retains private recovery evidence and blocks unrelated proxy changes as well; do not remove that evidence to unblock the queue. There is no canary split, cross-host traffic switch or automatic recovery guarantee after a host crash.


## Variable groups, wiring and ordered deploys

**Status: available through the REST API and hosted MCP since 25 September 2026, and local `impreza-mcp` 0.46.0 or later.** The portal has covered variable groups, configuration promotion and the ordered environment deploy since 28 September 2026, and since 29 September 2026 it saves a group only against the revision it loaded, keeps a stored value you leave blank, and lists the environment's recent deploys. Environment wiring, rename and delete remain REST and MCP operations.

### Variable groups

A variable group is one object of variables on a **project** or on one **environment**, replacing the twenty copied lines every app used to keep in sync by hand. `GET /v1/platform/projects/{id}/vars` and `GET /v1/platform/environments/{id}/vars` return every variable name, which ones are secret, and the non-secret values (sensitive-looking ones masked, the house rule for variables). Secret values are never returned. Each read also carries the group's `revision`, a counter that goes up on every write.

`PUT` on the same paths takes `{"vars": {...}, "secrets": {...}}` and **replaces the whole group** — a partial edit would be a read-modify-write race, so the honest unit is the group. `vars` are stored readable; `secrets` are sealed (AES-GCM, bound to the group) and come back as names only. Each name goes in exactly one of the two maps. Platform-managed names — `DOMAIN`, `HOST_PORT`, `DATABASE_URL` and the other routing/resource keys — are refused, as are duplicates. Limits: 100 names per group, 32 KiB per value, 128 KiB total. Sending empty `vars` and `secrets` deletes the group: an empty scope is the honest "no group".

Values follow the same rules as an app's own variables: one line of valid UTF-8 text, at most 32 KiB, with no control characters, and no `$` or ` #` — the environment file an app starts from is read verbatim, where `$` triggers variable substitution and ` #` starts a comment. Names starting with `COMPOSE_` or `DOCKER_` are refused, because they steer the container engine itself. A value kept with `__IMPREZA_KEEP__` answers to the same rules.

Send `base_revision` with the `revision` you read to refuse overwriting a change you have not seen: if the group was written since, the request fails with `409 REVISION_CONFLICT` and nothing is written. Without `base_revision` the write replaces the group as before. To keep a stored value without typing it again, send the exact value `__IMPREZA_KEEP__` for that name, in the map it is stored in (`secrets` for a secret, `vars` for a masked plain value): the stored value is put back, and a secret stays sealed and is never returned. A marker for a name that has no stored value, or in the other map, fails with `409 KEEP_TARGET_MISSING`, so the marker itself is never saved as a value. The limits above apply to the values actually stored, the kept ones included: a group that would exceed them is refused with `400`, and nothing changes.

Resolution is **project → environment → app on each app's next deploy**, the narrower level winning per name. It applies to an ordinary redeploy and to every stage of the ordered environment deploy below. Group values merge into the deploy payload only — they are never written into the app's own stored variables — and running containers do not change. With MCP, `impreza_get_variable_group` takes `project_id` plus an optional `environment_id`; `impreza_set_variable_group` takes the same identifiers plus `vars` and `secrets`.

In the portal, choose the scope (project or environment) under **Variable groups** and click **Load group**. Plain values can be edited in place. A saved secret, like a plain value masked by the rule above, is listed by name with an empty field: leave it blank to keep the stored value, type a new value to replace it, or remove the row to delete it. The page never receives a secret value. **Save complete group** replaces the whole group after you type the project or environment ID. The page saves against the revision it loaded: if someone else saved the group in the meantime, the save is refused and nothing changes, so load the group again and redo your edit.

### Environment wiring

`impreza_prepare_service_binding` (and `POST /v1/platform/deployments/custom/{id}/prepare-binding`) accepts an optional `wire: true`. The review names the sibling components it would wire (`wire_components`). On apply, every **other** eligible web/worker component of the same environment receives its **own** [managed connection](service-bindings.html) to the same provider: its own binding reference, its own generated credential, its own deploy job — never a share of the first consumer's credential. Eligibility is rechecked inside the apply transaction: components already bound, busy or with an incompatible shape are skipped and named in the receipt, never a failure.

Wiring also applies on its own inside the ordered deploy below: when a web/worker component runs as a stage and its manifest carries no binding reference, the stage manifest inherits the reference of the one active binding its environment holds for it. The reference only — the credential stays on the agent-only path, exactly like the primary connection. An explicit binding already in the manifest always wins, a retiring binding is never injected, and the provider must live in the same environment with a database or cache role. An ordinary redeploy simply reuses the recorded manifest, so whatever reference a reviewed connection wrote there is what keeps serving.

### Ordered environment deploy

`POST /v1/platform/environments/{id}/deploy` stages every non-preview service of the environment in role order — **database → cache → worker → web** — and returns `202` with a `batch_id` (`edb_...`); `GET /v1/platform/environment-deploys/{id}` reports the stages, the cursor and the terminal state. `GET /v1/platform/environments/{id}/deploys` lists the environment's 20 most recent batches, newest first, with each batch's `batch_id`, `status`, `require_healthy`, `cursor`, `stage_count`, `error`, `created_at` and `finished_at` (read scope). Each stage is an ordinary smart-replace deploy, so variable groups resolve and wiring applies per stage, and each stage sees the state that exists when it runs, not when the batch was created.

Stages are gated on health by default: `require_healthy_start` defaults to `true`, every stage's manifest gains the existing startup-health requirement, and every stage's agent must already announce `startup-health-v1` — checked before anything queues, because refusing early beats a batch that dies at stage two. Pass `require_healthy_start: false` to deploy without the gate. One running batch per environment; every component must be settled (running or failed, no lifecycle operation in flight).

A failed stage fails the batch and the batch names the component that stopped the line. Nothing rolls back implicitly: the stages that already succeeded stay deployed, and the fix is another deploy — the same honesty the single-app operations have. MCP: `impreza_deploy_environment` (`environment_id`, optional `require_healthy_start`) `impreza_get_environment_deploy` (`batch_id`) and `impreza_list_environment_deploys` (`environment_id`).

In the portal, **Deploy environment in order** starts the batch after you type the environment name. **Require healthy start** stays on unless you clear it. The page lists the environment's recent batches and follows each stage of the one in progress until it ends. Reloading the page or closing the section never stops the batch: choose the environment again and the page resumes following it.

### Configuration promotion

Image promotion moves the immutable artifact; a configuration promotion moves the **configuration** around it between two environments of one project. `POST /v1/platform/environments/{id}/config-promotions` with `{"source_environment_id": "env_..."}` prepares a 15-minute review (`cpro_...`) of what the target environment would inherit: the environment variable group and each paired component's user variables, components paired by name exactly like image promotion. `GET /v1/platform/config-promotions/{id}` reads the saved review; `POST /v1/platform/config-promotions/{id}/apply` with the exact `review_digest` and `confirm: true` applies it.

The same machinery as every other reviewed mutation: sealed payload, digest over the ciphertext, one receipt, replay returns the receipt, at most 20 pending reviews per account. The diff buckets **names only** — same, different, only in source, only in target — never a value. Secret group values travel inside the sealed review and are re-sealed under the **target** group on apply; the source group is never rewritten.

Applying writes the target environment's variable group and replaces each paired component's **user** variables with the source's (platform-managed keys are preserved). It does **not** restart or redeploy anything: the configuration takes effect on each app's next deploy, and the ordered environment deploy above is the way to roll it out. MCP: `impreza_prepare_config_promotion` (`target_environment_id`, `source_environment_id`), `impreza_get_config_promotion` (`promotion_id`) and `impreza_apply_config_promotion` (`promotion_id`, `review_digest`, `confirm: true`).

In the portal, **Promote configuration** takes a source and a target environment of the same project and prepares the review. The review shows the name buckets and the paired components, never a value. **Apply reviewed configuration** applies that exact review after you type the target environment name. When both environments already match, the portal says there is nothing to review.

### Rename and delete

`POST /v1/platform/projects/{id}/rename` and `/v1/platform/environments/{id}/rename` take `{"name": "..."}` (deploy scope) and change organization only — nothing about the workload. `POST .../delete` (manage scope) requires `confirm` set to the **current name, verbatim**: organization deletion is not the two-step review a deploy gets, but a stray `confirm: true` must never be enough to remove the label a team's services hang under. A project can be deleted only once every environment inside it is deleted; an environment only once every service is detached. Variable groups under the target go with it, and their sealed secrets die there. No application is stopped or uninstalled — a deletable project or environment is already empty. MCP: `impreza_rename_project`, `impreza_delete_project`, `impreza_rename_environment`, `impreza_delete_environment`.

### Previews in an environment

A dark preview of an attached application joins the same environment automatically, with the role `preview` and a component named after its parent plus a short branch digest. The inventory can then say what is a review artifact, and the machinery above ignores previews by construction: they are never a stage of the ordered deploy, never wired and never paired by a configuration promotion. The membership dies when the preview is retired; the application and its data are otherwise untouched.

### Topology export

`GET /v1/platform/projects/{id}/export` (read scope) returns a **deterministic,
read-only export of one project's topology**: every component that exists in
each environment with its role, kind (catalog or custom) and status, the
active service bindings as dependencies, the deploy policy in force, and the
**variable names** at project and environment level. Secret variables appear
as **references only** — their names, listed under `secret_refs` — never a value; nothing that
could run or re-create the project is exported. The export is canonical
(recursive key order and sorted lists), so the same state always exports the
same bytes, and the `digest` field (`sha256:` of those bytes) makes two exports comparable
without timestamps: export twice and diff the digests to prove nothing drifted.

MCP: `impreza_export_project_topology` (`project_id`) — same output, read
scope, applies and mutates nothing. A foreign or missing project answers a
plain 404.
