Skip to main content
Group your apps and databases into projects, file projects under organizations, and tag apps with an environment (staging, production, and so on) so they share config and get deployment gates. All of it is optional: add structure when you need it.

The model

Apps and databases are the things that actually run. Projects, organizations and environments are labels on top of them.
  • A project groups apps and databases.
  • An organization groups projects.
  • An environment belongs to one project and is applied to apps (not databases). It carries shared env vars and can be marked protected.
An app or database can skip every level and belong to nothing, and nothing about how it runs depends on where it is filed. Deleting a project, organization or environment never deletes or disrupts what is filed under it. The members just become unlabeled again. Grouping is not an access boundary. Access is controlled by roles and IAM policies, which scope to individual resources and never to a project or organization. See Identity and access. Environments exist for the protected-environment gate, which guards deploy, rollback and promote. Databases have no such action, so only apps can be tagged with an environment.

How the grouping actually works

Projects and organizations are addressed by ID, never by name. Project names are not unique on purpose, so a duplicate name is never rejected (unlike POST /apps).

Moving resources between groups

Moving an app or database to a project, or an app to an environment, is a separate PUT call against the resource, not a field on create/update: To remove a resource, pass an empty string: "project_id": "". All endpoints validate the target ID first, so a mistyped or deleted ID gets a 400, not a silent write. Like PUT /apps/{name}/node for node placement, they need only the write ability, not root, because this is an organizational edit, not an infrastructure change.

Dashboard UX for moving

Moves happen from the resource’s own Overview page, not from the project/environment page:
  • An app’s Overview page has “Move to project” and “Change environment” actions.
  • A database’s Overview page has a “Move to project” action.
  • A project’s detail page has a “Move to organization” action.
Project and environment detail pages are read-only for membership. They list what is filed there and link back to each member’s Overview page to move it.

Moving from the CLI

Moving is the same PUT .../project call as the first assignment. The change is immediate, and nothing about the container, image or config changes.

Shared env var layering

Projects, organizations and environments can each hold shared env vars. These layer automatically into every app’s effective environment. Each scope can hold a mix of plain and encrypted (secret) variables. They stack in a fixed order, lowest to highest. App integrations sit at the same tier as the shared layers, below the app’s own env.

What an app actually sees

An app only sees the tiers that apply to it:
  • Organization tier: only if app has project_id AND that project is filed under an organization.
  • Environment tier: only if app has environment_id.
  • Project tier: only if app has project_id.
An app with no project and no environment gets its own env/secretEnv (plus any storage/database env), unchanged from before.

Secret-marked shared env vars

Any shared env var can be marked as a secret. Secret-marked variables are encrypted at rest using the same envelope-encryption path as per-app secrets (see Security overview). Their values are never returned in plaintext from API endpoints or the dashboard; only the key name is shown. From the CLI:
From the API:
The same pattern applies for organizations and environments: use /organizations/{id}/env/secrets/* and /environments/{id}/env/secrets/* respectively. When an app runs, secret-marked shared env vars are injected at container creation time the same way per-app secrets are (never persisted to container inspect or env listings). Changing a secret’s value rolls apps forward the same way changing a per-app secret does: set the new value, and the app restarts on the next reconciliation pass or manual restart.

Comparing env vars across environments

GET /api/v1/projects/{id}/environments/compare?a={envId}&b={envId} diffs two of a project’s environments: which keys only one side has, and which plain keys both sides have with different values. Each side’s env list is the same organization-then-project-then-environment resolved effective set described above, so the diff reflects what an app tagged with that environment would actually see, not just that environment’s own raw rows. A secret-marked key never shows a value, on either side. One present on only one side is still reported (so you know a secret was added or removed), but one present on both sides is reported with status masked rather than changed or same: this control plane cannot tell whether the two differ without decrypting them, so it never guesses. A plain key present on both sides with the identical value isn’t reported at all, since there’s no drift to surface.
From the CLI:
In the dashboard, the “Compare” action on a project’s Environments panel, or on an environment’s own detail page, opens a picker for two environments and shows the same diff table.

Project-wide pause, resume, and restart

Stop and start

POST /api/v1/projects/{id}/stop and .../start act on every app and database filed under the project:
  • Stop sets each member’s suspended flag to true
  • Start clears it
Same effect as POST /apps/{name}/stop (or database equivalent) applied project-wide. Like single stop/start, this only flips the desired-state flag. Reconcilers actually stop/start containers on their next pass.

Restart (apps only)

POST /api/v1/projects/{id}/restart forces every app under the project to recreate its running container with no image change (a fresh restart_nonce, the same mechanism as POST /apps/{name}/restart on one app). Databases have no bulk restart endpoint at this scope.

Partial success handling

All three endpoints attempt every member independently and never abort on one failure. The response reports successes and failures:
One bad resource does not block the rest.

Dashboard

The project detail page shows:
  • “Stop project” / “Start project” pair (always both buttons, not a toggle)
  • “Restart all” button (disabled when the project has no apps)
Neither requires a confirm dialog. Stopping is not destructive (desired state survives), and restart is non-destructive, the same as restarting a single app.

Protected environments

An environment created with protected: true requires explicit confirm: true on actions that change what a tagged app runs:
  • POST /apps/{name}/deploys (deploy)
  • POST /apps/{name}/promote (promote to another environment)
  • POST /apps/{name}/deploys with an older image tag (rollback; no separate endpoint)
Without confirm: true, the server rejects the request with a 409 and names the environment:
The check fails open: apps with no environment, or tagged with unprotected environments, always pass. Stale or deleted environment_id references are treated as “not protected” rather than blocking.

CLI

All three commands take a --confirm flag:
Leaving out --confirm does not fail. The client catches the 409, prints the server message and prompts on stdin: Type "yes" to proceed:. In a non-interactive script (EOF or anything but “yes”), the original 409 stands.

Dashboard

The deploy/rollback/promote flow shows an amber warning box. Check the checkbox “I understand and want to proceed.” to enable the button (same pattern as Redis’s no-auth warning). Toggle protected on the environment detail page, which calls PATCH /api/v1/environments/{id}. That is the only field the endpoint changes.

Promote and clone dialogs

“Promote to…” on an app page loads GET /api/v1/apps/{name}/promote/preview and shows the plan before you confirm: the image change, replicas, resources and health differences, and the env key names that would be added, removed or differ (values are never shown). Tick “Also apply the env key changes” to send include_env. If the source is unhealthy or its last deploy failed, the blockers are listed and “Promote anyway” sends force. Promotion honors deploy freeze windows: while the target is frozen the dialog requires an override reason (override_freeze and override_reason, also --override-freeze and --override-reason on the CLI), and into a protected environment it still becomes a pending approval. “Clone” on an app page shows what a clone copies and what it leaves behind (GET /api/v1/apps/{name}/clone/preview). Secret values are copied only if you tick the box, re-encrypted for the clone and never displayed (needs read:sensitive); domains are not copied unless you opt in to derived names. A clone never starts a deploy, so freeze windows and approvals apply when you first deploy it. Cloning does not attach or copy databases.

Cloning an environment

POST /api/v1/environments/{id}/clone copies a whole environment: every app tagged with it, plus its own shared env vars, into a brand-new environment in the same project. This is not the same as POST /apps/{name}/promote, which only moves one app’s image tag onto an existing sibling app. Cloning creates new apps and a new environment from scratch and deploys them through the normal reconcile path, the same as creating an app through the API. GET /api/v1/environments/{id}/clone/preview?new_environment_name={name} shows what a clone would create without applying it: each tagged app’s suggested new name, current image, current domains (which will not be copied), declared secret env var names, and scheduled task count, plus the source environment’s own shared env var keys.

What’s copied, regenerated, or dropped

This matches promotion between environments: config crosses freely, and secret plaintext only on explicit opt-in.

Request shape

apps is optional per source app: omit an entry entirely and that app clones under its own auto-suggested name (<source>-<slugified new environment name>) with no domains assigned. Only list apps whose default name or domain assignment needs to change. A name collision with an existing app (auto-suggested or explicit) fails the whole request with 409 Conflict before anything is written; nothing is partially created because of a name clash. A requested domain already claimed by another service (including the source app itself) fails the same way, the same conflict a duplicate domain assignment anywhere else in the platform returns.

CLI

Dashboard

The environment detail page has a “Clone environment…” button next to the protected toggle and delete action, opening a clone dialog. It shows the same preview as the API (suggested names, domains not copied, secret/scheduled-task counts), lets you override a name or assign domains per app, and has an explicit “Also copy real secret values” checkbox that defaults unchecked.

Dashboard pages

File a project into an organization from the project detail page, not from the organizations list.

API reference

CLI

Limits

  • Grouping is not a permission boundary. Roles and IAM policies scope to individual resources, never to a project or organization.
  • There is no bulk move. Move apps and databases between projects or environments one at a time.
  • Databases cannot be tagged with an environment, because the protected-environment gate only guards deploy, rollback and promote.
  • There is no per-project or per-organization audit view. Changes appear in the generic audit log (GET /api/v1/audit-log).
  • There is no server-side membership listing and no GET /api/v1/projects/{id}/apps. The dashboard filters the full /apps, /databases and /projects lists by each row’s project_id or org_id.

Next steps

Deploying apps

The apps being grouped, plus promote and clone.

Managing databases

The databases being grouped.

Project topology graph

A project’s apps, databases and volumes as a diagram.

Identity and access

Roles and per-resource policies.
Last modified on October 12, 2026