
This guide covers org offboarding: the two-step erasure lifecycle, what each
step destroys, and what it leaves alone. Read
[Data residency](/sovereignty/data-residency/) first for why the keys are
per-org in the first place.

## What crypto-shredding destroys

Every controlplane value that an org owns is encrypted at rest under that
org's own
key, named `org-<id>-secrets` in the operator KMS
(`internal/kms/crypter.go`, `OrgKeyName`). The key encrypts only key-wrapping
material: each stored value carries its own data encryption key, and the org
key wraps that. Destroy the org key and the wrapped keys can no longer be
unwrapped. Every value under the key becomes permanently unreadable, and database
access cannot recover it.

`CompleteErasure` destroys the key first and sweeps the dead rows second
(`internal/controlplane/offboarding/offboarding.go`). If the process crashes in
between, the leftover rows are unreadable ciphertext. They are not readable
orphans.

The sweep deletes four row families from the controlplane database:

- policy settings for the org
- policy agent overrides for the org
- config values for the org
- config agent overrides for the org

After the sweep the org's status becomes `erased`. The shred is complete the
instant the key is gone; the rows are only tombstones that the sweep drops.

## The two steps

Offboarding is a two-step lifecycle, so an accidental trigger can be caught
before anything is destroyed:

```
active --begin--> destroying --complete--> erased
```

The first step opens the grace window. It marks the org `destroying` and
destroys nothing:

{{< terminal title="offboard, step 1" prompt="$" >}}
$ curl -X POST -H "Authorization: Bearer $TOKEN" \
    https://<controlplane>/api/v1/orgs/<org>/erasure
{"org_id":"<org>","status":"destroying"}
{{< /terminal >}}

A `202` response confirms the request. The status code is not `201` or `200`
because the destructive half has not run. There is no timer on the grace
window; it stays open until you complete it. There is also no documented cancel
route that closes the window and returns the org to `active`: no erasure
endpoint undoes the begin step. The only path that sets an org back to
`active` is a side effect of the org upsert: `POST /api/v1/orgs` with the
same `id` and an empty `status` writes the org row with status `active`
(`internal/controlplane/iam/api.go`, `createOrg`; `internal/controlplane/iam/store.go`,
`UpsertOrg`). That route exists for creating orgs, not for canceling erasure,
so treat it as an undocumented recovery path and not a supported cancel.

The second step is the irreversible one. It destroys the org key, sweeps the
rows, and marks the org `erased`:

{{< terminal title="offboard, step 2" prompt="$" >}}
$ curl -X POST -H "Authorization: Bearer $TOKEN" \
    https://<controlplane>/api/v1/orgs/<org>/erasure/complete
{
  "org_id": "<org>",
  "status": "erased",
  "key_shredded": true,
  "rows_swept": 12
}
{{< /terminal >}}

Complete the erasure only from the grace window. Calling it on an org in any
other state is a `409` conflict, so a single call can never destroy an org.

The `key_shredded` value reports the outcome accurately. It is `false` when no
operator KMS is configured, or
when the KMS provider cannot delete keys (the provider reports
`ErrUnsupportedOperation`). In those cases the rows are still swept, but the
ciphertext left behind was not key-shredded, and the response reports that
with `key_shredded: false`. A KMS
error during the destroy aborts the erasure before any row is swept.

## Who can run it, and what is refused

Both routes require the `platform:orgs` capability at platform scope over the
bearer-authenticated operator API. See
[Operator API](/reference/operator-api/) for the authentication model. An
agent-operator caller is refused at the actor-kind check before grants are even
loaded. The platform org itself cannot be erased: the shred would destroy the
operator's own key material, so the route returns `403` before touching
anything. An unknown org is `404`.

Every step is audited at authority tier. The `controlplane.org.erasure.begin`
event is durably captured before the status commit, so a destruction is never
unrecorded. `controlplane.org.erasure.complete` reports `key_shredded`,
`rows_swept`, and the final status. See
[Audit events](/reference/audit-events/) for the field lists.

## What the erasure does not destroy

Erasure is narrower than it looks. Know these four boundaries before you run
it.

**The org row survives.** The status becomes `erased`; the org still exists.
Deleting the org row is a separate, idempotent step:

{{< terminal title="remove the erased org" prompt="$" >}}
$ curl -X DELETE -H "Authorization: Bearer $TOKEN" \
    https://<controlplane>/api/v1/orgs/<org>
{{< /terminal >}}

The platform org is undeletable as well as unerasable. A `DELETE` on an org
that does not exist is a `204` either way.

**Users, groups, grants, and service accounts are not swept.** The erasure
sweep covers policy and config rows only. Those identity rows keep existing
until you delete the org or remove them by their own routes.

**The enclave is never dialed.** The controlplane holds no credential for the
enclave's KMS and performs no operation against it. The org's sovereign
secrets and sealed inference bodies live in the enclave's own database under
the enclave KMS's per-org key. The enclave-side store implements the same
crypto-shred (`internal/enclave/sovereign/store.go`, `CryptoShredOrg`), but no
API exposes it yet. Today, destroying an enclave's recoverable data means
tearing down the enclave and destroying its KMS keys, as described in
[Data residency](/sovereignty/data-residency/).

**A deployment with no operator KMS has no shred to report.** Policy and
config values are then stored in plaintext. Erasure still sweeps the rows, and
it still destroys nothing else, but `key_shredded` reports `false` because no
key existed to destroy.
