Skip to main content

Environments

Each project holds many environments. Each owns its own content (entries, tree, routes); schema, media, sites, API keys, webhooks, roles, and users remain project-level.

Default environments

A new project auto-creates two:

  • Productionslug: production, isDefault: true. The live destination served by the public delivery API.
  • Draftslug: draft. A staging area authors can edit without affecting production.

Schema

model Environment {
id String
projectId String
name String
slug String
description String?
isDefault Boolean @default(false) // exactly one env is default per project, set explicitly on create
isLocked Boolean @default(false)
promotionSourceId String? // self-ref: where this was cloned from
lastPromotedAt DateTime?
}

Switching environments in the admin UI

The sidebar has an environment switcher under the project switcher. Selecting an environment writes a krios-env-{projectSlug} cookie; server loaders read it and scope every per-env query.

Environment context in API requests

Three precedence levels:

  1. API key scope — when an API key has environmentId set, every request through it is locked to that environment.
  2. Explicit hint?environment=draft query param or X-Krios-Environment header.
  3. Project default — falls back to the isDefault: true environment.

A hint that conflicts with an API key's scope returns 403 environment_scope_mismatch.

Promotion

Two modes — full replaces the target's entries entirely, cherry-pick copies only the listed entries:

POST /api/v1/projects/demo/environments/draft/promote
{
"targetEnvironmentSlug": "production",
"mode": "full"
}
POST /api/v1/projects/demo/environments/draft/promote
{
"targetEnvironmentSlug": "production",
"mode": "cherry-pick",
"entryIds": ["ckl_a", "ckl_b"]
}

What gets copied:

  • Content entries (with originalEntryId linking back to the source)
  • Field values
  • Locale states
  • Version history
  • References (with target IDs remapped)
  • Tree nodes (full mode only — cherry-pick leaves tree placement to authors)
  • Route index (full mode only)

Promotion is recorded in the audit log with the source / target slugs, mode, and affected entry IDs.

Locking

isLocked = true on an environment blocks creating new entries and tree nodes (enforced on entry-create and tree-node-create only — updating or deleting an existing entry in a locked environment is not blocked):

{
"error": "environment_locked",
"message": "Environment \"production\" is locked; promote into it instead of editing directly."
}

Use this on production environments where every change should flow through promotion from Draft / Staging.

API

GET /api/v1/projects/{slug}/environments # list
POST /api/v1/projects/{slug}/environments # create
PUT /api/v1/projects/{slug}/environments/{slug} # update
DELETE /api/v1/projects/{slug}/environments/{slug} # delete (default protected)
POST /api/v1/projects/{slug}/environments/{slug}/promote
POST /api/v1/projects/{slug}/environments/select # set the cookie

Admin UI

Settings → Environments tab. List with entry counts, create with optional clone-from, edit name / description / lock, delete (default protected, confirmation with entry-count warning), promote (full or cherry-pick) modal.

Worked example — staging → production

# 1. Create a Staging environment cloned from Production.
curl -X POST .../environments \
-d '{ "name": "Staging", "slug": "staging", "cloneFromSlug": "production" }'

# 2. Edit content in Staging (admin UI's environment switcher selects it).

# 3. Promote everything to Production.
curl -X POST .../environments/staging/promote \
-d '{ "targetEnvironmentSlug": "production", "mode": "full" }'

Full-mode promotion wipes the target's content (wipeEnvironmentContent) and then clones from the source (cloneEnvironmentContent) sequentially. This is not currently wrapped in a transaction, so a failure after the wipe can leave the target emptied.