Kaiten

Deployments

Deployments & Lifecycle

The append-only deployment journal — how a deployment is recorded, how the instance linkage resolves what runs where, how history is read, and the lifecycle states the model carries.

Kaiten records a deployment when a deployment zone's current release changes: one entry, appended to a journal that is never edited and never pruned. The journal is the history of every zone and of every release, and the instance linkage turns it into an answer for every instance — this one runs that.

Examples use $KAITEN_API and $KAITEN_TOKEN as set up in the quickstart. Responses are illustrative — ids, actors, and timestamps depend on your stack.

The journal entry

An entry carries exactly what is needed to be a fact about the past:

FieldWhat it holds
idIdentifier of this entry
deploymentZoneIdThe zone the release landed in
releaseIdThe release that landed
createdByWho recorded it — the user or service account behind the call
createdAtWhen

There is no status, no description, and no update. An entry is never "current" in itself: the zone's releaseId says what runs now, and it always equals the newest entry recorded for that zone.

The journal is ordered by timestamp, then by recording sequence. Each entry is stamped with seq, an identity value that grows with every insert, and the journal reads per zone or per release by createdAt, with seq breaking ties. createdAt alone would not do: two entries recorded in the same millisecond carry equal timestamps, and "the newest entry" — the one the zone's releaseId must agree with — would be undefined between them. seq settles it. It is a storage-level fact: the Deployment object the API and the RELEASE_DEPLOYED event carry does not expose it, the data model does. The two questions the journal answers are what has this zone run? and where has this release landed?

Recording a deployment

Two calls append to the journal, both on the zone:

  • POST /deployment-zones with a releaseId — the new zone's first deployment.
  • PUT /deployment-zones/{slug} with a releaseId different from the current one — the zone's current release becomes the new one and one entry is appended. Echoing the current releaseId, or omitting it, leaves the journal untouched.

Both emit RELEASE_DEPLOYED. Both require write:deployment_zones.

The seeded Kaiten Sushi Shop story stops one step short: 2026.8.0 runs in shared-eu and shared-apac, and sakura-dedicated still runs 2026.7.0. Completing the rollout is one PUT. Look up the release's id first, by its version — the seeded release is also addressable by its slug, r-2026-8-0:

export RELEASE_280_ID=$(curl -s "$KAITEN_API/releases" \
  -H "Authorization: Bearer $KAITEN_TOKEN" \
  | jq -r '.items[] | select(.version == "2026.8.0") | .id')

Because PUT replaces the zone's details, resend the fields to keep — name, type, description, metadata — with the new release:

curl -i -X PUT "$KAITEN_API/deployment-zones/sakura-dedicated" \
  -H "Authorization: Bearer $KAITEN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sakura Dedicated",
    "type": "production",
    "description": "Dedicated zone for the Sakura Tokyo group.",
    "metadata": { "region": "ap-northeast-1", "compliance_profile": "pci-dss", "dedicated": true },
    "releaseId": "'"$RELEASE_280_ID"'"
  }'

HTTP/1.1 204 No Content. The journal now holds a sixth entry — 2026.8.0 in sakura-dedicated, now, by you — and the zone's releaseId points at 2026.8.0:

curl "$KAITEN_API/deployment-zones/sakura-dedicated" \
  -H "Authorization: Bearer $KAITEN_TOKEN"
{
  "id": "…",
  "name": "Sakura Dedicated",
  "slug": "sakura-dedicated",
  "type": "production",
  "description": "Dedicated zone for the Sakura Tokyo group.",
  "metadata": { "region": "ap-northeast-1", "compliance_profile": "pci-dss", "dedicated": true },
  "releaseId": "2b6f9c1e-8d3a-4f7b-9e0c-6a5d4b3c2f1e",
  "updatedAt": "2026-09-08T14:00:00Z",
  "…": "…"
}
The Deployment Zones page with the dialog Deploy a release to Sakura Dedicated open: 2026.8.0, Introduces real-time delivery tracking, selected above the Deploy button; behind it, the Sakura Dedicated row is still on 2026.7.0 with one release.The Deployment Zones page with the dialog Deploy a release to Sakura Dedicated open: 2026.8.0, Introduces real-time delivery tracking, selected above the Deploy button; behind it, the Sakura Dedicated row is still on 2026.7.0 with one release.

A rollback is a deployment. Pointing sakura-dedicated back at 2026.7.0 with the same PUT appends another entry — the zone ran 2026.7.0, then 2026.8.0, then 2026.7.0 again, and the journal says so in that order. Nothing is rewritten; a release a zone already ran can land there again.

A releaseId that does not exist in your organization is refused. The journal only ever points at releases that exist, which is also why a release cannot be deleted once it has been deployed: DELETE /releases/{slug} answers 409 DeleteRelease.InUseConflict for as long as an entry names it.

The event

Every entry is published as RELEASE_DEPLOYED, and its payload is the entry itself:

{
  "name": "RELEASE_DEPLOYED",
  "type": "com.kaiten.deployment_zone.v1.release_deployed",
  "data": {
    "id": "c7d8e9f0-a1b2-4c3d-9e4f-5a6b7c8d9e0f",
    "deploymentZoneId": "…",
    "releaseId": "2b6f9c1e-8d3a-4f7b-9e0c-6a5d4b3c2f1e",
    "createdBy": { "id": "…", "name": "…" },
    "createdAt": "2026-09-08T14:00:00Z"
  }
}

This is the hook for the systems that do the shipping: a subscriber that receives RELEASE_DEPLOYED knows which release landed where, and can trigger the runner, notify the customer, or keep its own timeline. Kaiten records the landing; it does not perform it. Like every domain event, RELEASE_DEPLOYED travels the event stream — events and webhooks covers the two delivery paths.

Reading history

What runs now is on the zone: GET /deployment-zones/{slug} returns releaseId. Releases are fetched by slug, not by id, so to expand that id into a version and its component list, match it against id in GET /releases. Before the PUT above, the seeded zones read:

ZonetypededicatedCurrent release
shared-euproductionfalse2026.8.0
shared-apacproductionfalse2026.8.0
sakura-dedicatedproductiontrue2026.7.0

What ran before is the journal. The seeded profile writes five entries — a progressive rollout read top to bottom, oldest first. The seeder dates them relative to the moment it runs, so the intervals between entries are fixed and the calendar dates are those of your own seed:

Recorded before the seed ranZoneRelease
64 days 15 hshared-eu2026.7.0
64 days 14 h 30 minshared-apac2026.7.0
61 days 16 hsakura-dedicated2026.7.0
26 days 15 hshared-eu2026.8.0
25 days 15 hshared-apac2026.8.0

Read per release, the same five rows say that 2026.8.0 reached two of three zones; read per zone, that sakura-dedicated was deployed once and is due a second time.

No REST endpoint lists the journal today, and the console shows the current state rather than the journal: a release's Deployment Zones tab lists the zones that run it now, a zone's page shows its current release, and on the zones list a zone's Releases count opens the releases it has run, with their status.

The Deployment Zones tab of release 2026.8.0: the two zones that run it now, Shared APAC and Shared EU, both of type Production; Sakura Dedicated, still on 2026.7.0, is not listed.The Deployment Zones tab of release 2026.8.0: the two zones that run it now, Shared APAC and Shared EU, both of type Production; Sakura Dedicated, still on 2026.7.0, is not listed.

The dated entries are read in two places. The organization's audit trail keeps each one — the Release deployed to a zone rows of the console's Audit trail page, recorded without an Instance; the audit trail concept page explains what is recorded. The RELEASE_DEPLOYED stream delivers each entry as it is recorded. On a seeded Community Edition stack, the journal holds the five entries above, dated as shown, and the audit trail a RELEASE_DEPLOYED row for each, recorded at the moment the seeder ran. Every deployment recorded from then on is in both, with the same date.

The instance linkage

An instance runs whatever its zone currently runs. Resolving it is three reads with no field to maintain:

  1. GET /instances/sakura-tokyo-prod → deploymentZoneSlug: "sakura-dedicated"
  2. GET /deployment-zones/sakura-dedicated → releaseId
  3. GET /releases, the item whose id matches → version: "2026.7.0" and its four component versions

The result is a fact about the runtime that sits next to the contract without being derived from it. Sakura Tokyo's Premium License grants delivery-tracking, and the flag delivery-tracking-enabled reads that grant and resolves to on for Sakura Tokyo; the zone, meanwhile, ran 2026.7.0 — the release before delivery tracking shipped — until the PUT above. The model does not collapse the two: what a customer is entitled to and what their instance runs are separate records, and keeping them side by side is what lets you see the gap. Placing and moving instances is covered in how instances relate to zones.

Lifecycle states, as the model exposes them

The pillar carries fewer states than a reader might expect, and this page lists exactly what exists.

EntityState fieldValues
Deployment (journal entry)noneAn entry is a past fact. "Current" is not stored on it — it is the zone's releaseId, which always equals the zone's newest entry.
ReleasenoneA release exists or has been deleted. "Deployed" is derived from the journal entries that name it, and governs whether it can be deleted.
Componentnone"Bundled by a release" is derived from Release.components, and governs whether PUT edits in place or versions, and whether DELETE is allowed.
Deployment zonenoneA zone has a releaseId or has not deployed anything yet.
InstancestatusHEALTHY (default), DEGRADED, INCIDENT, MAINTENANCE — the operational state
InstancelifecycleStageFree-form; TRIAL, ACTIVE, AT_RISK, CHURNED are the suggested values — the commercial state

The two instance fields change through PATCH /instances/{slug}, which accepts exactly status and lifecycleStage; each change is its own event, INSTANCE_STATUS_CHANGED or INSTANCE_LIFECYCLE_STAGE_CHANGED. A lifecycleStage once set cannot be cleared back to unset. Both are among the instance facts a targeting rule can read under __kaiten.instance, so a flag can behave differently for an instance under MAINTENANCE — the seeded sakura-tokyo-demo is one — or a customer AT_RISK. The seeder sets no lifecycleStage on any instance. See Customers & Instances.

There is no draft, approval, in-progress, or failed state on a release or a deployment, and no rollout percentage on a zone. A progressive rollout in Kaiten is the sequence of journal entries across zones — the seeded story is one — and a halted rollout is a zone whose newest entry is older than the other zones'.

Events

Event nameEvent typeWhen
RELEASE_DEPLOYEDcom.kaiten.deployment_zone.v1.release_deployedA zone's release is set at creation or changed — one per journal entry
INSTANCE_DEPLOYEDcom.kaiten.instance.v1.deployedAn instance is placed in a zone
INSTANCE_MIGRATEDcom.kaiten.instance.v1.migratedAn instance moves from one zone to another
INSTANCE_STATUS_CHANGEDcom.kaiten.instance.v1.status_changedAn instance's operational status changes
INSTANCE_LIFECYCLE_STAGE_CHANGEDcom.kaiten.instance.v1.lifecycle_stage_changedAn instance's commercial lifecycleStage changes

Where to go next

Driving the PUT from your delivery pipeline so every landing your runner performs is journaled: Deploy with CI/CD; the CLI offers kaiten deployment-zones update for the same call. The one-screen version of this pillar is Releases & Deployments. Exact reference: data model.

On this page