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:
- Production —
slug: production,isDefault: true. The live destination served by the public delivery API. - Draft —
slug: 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:
- API key scope — when an API key has
environmentIdset, every request through it is locked to that environment. - Explicit hint —
?environment=draftquery param orX-Krios-Environmentheader. - Project default — falls back to the
isDefault: trueenvironment.
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
originalEntryIdlinking 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.