When a customer deletes their workspace, Connect purges its own copy of their
data. Your application has its own copy, on your own infrastructure, and
Connect cannot reach it.

The deletion policy is how you tell the customer what happens to that copy —
**before** they confirm. It is a statement made to them, not an internal note,
and it is shown to them verbatim.

<Callout type="warn">
An application with no policy declared is not shown as "nothing is kept". It is
shown as *"this application hasn't told us"*, which is the worst of the
available answers and is what a customer sees for every application that never
filled this in.
</Callout>

## Where it fits in the deletion flow

<Steps>

<Step>

### The customer asks to delete

Connect assembles an impact report from the policy of every application the
workspace subscribes to, and shows it to them. This is the moment your
declaration is read.

</Step>

<Step>

### They confirm, and the grace window opens

Access is denied immediately; the data still exists. Your application receives
`workspace.deletion_scheduled` carrying `purgeAfter` — the instant the data
stops existing on Connect's side — and `policy`, your own declaration frozen
as the customer read it.

The window is the **longest** grace period among the platform default and every
subscribed application's declared one. Your policy can extend it; it can never
shorten it.

</Step>

<Step>

### They may change their mind

`workspace.deletion_cancelled` fires. This is the event integrations forget,
and forgetting it leaves a paying customer locked out of a product that has no
idea anything changed. Handle it.

</Step>

<Step>

### The window closes

Connect purges. `workspace.deleted` fires. You do what your policy said you
would.

</Step>

</Steps>

See [Webhooks](/docs/webhooks) for the delivery contract behind those three
events.

## What you declare

### Grace window

Blank accepts the platform default, which is the right answer for almost
everybody. Set a number only if your own wind-down genuinely needs longer —
and note that it applies to **every** workspace subscribed to your application,
not only ones with a lot of data.

### Who answers for this data

The field most designs of this shape leave out, and the one that decides what a
failed deletion notice *means*.

| Value | Meaning |
| --- | --- |
| `connect_owned` | CosmoOps runs this application. A deletion is an instruction, and a notice that never lands is our incident to chase. |
| `processor` | A third party holds the data under contract to us. Same obligation — a deletion is still an instruction we are answerable for. |
| `independent` | The partner decides its own retention under its own agreement with the customer. We notify; we do not instruct. |

`independent` is the safe default when nobody has confirmed otherwise, and it
is what a new application starts as. Choosing one of the other two is a
statement that somebody has actually taken on the obligation.

### Data classes

One row per kind of data your application holds. Each carries a disposition:

- **Erase** — destroyed when the workspace is purged. Nothing survives.
- **Anonymise** — the records survive with the customer's identifiers stripped.
  Useful where aggregates still have to add up.
- **Retain** — kept intact for a set period after the workspace is gone, in a
  store nothing in the product can read.

`retain` requires a **legal basis**, and this is enforced rather than
encouraged: you cannot save a retained class without naming the clause or rule
that requires it. The customer is shown that string, so it should name a real
clause rather than paraphrase one — *"GST records, six-year statutory
retention"* rather than *"compliance"*.

Leave the retention blank on anything that goes when the workspace does.

### Held identifiers

A different clock from retention, and about the *identifier* rather than the
data: anything allocated to the workspace that has to wait before it can be
given to somebody else. A phone number. A handle. A subdomain.

Quarantining a phone number for 90 days is not retaining the customer's data —
it is refusing to hand their old number to a stranger the day after they leave.

### Export before purge

A promise that your application will let the customer take their data before
the window closes. Leave it off until that export actually exists; it appears in
the impact report as a commitment.

## Versioning

`version` is stamped server-side on every save. Every deletion certificate
written afterwards records the version that was in force, so an operator saving
twice is visible in the record, and a policy that changed after a customer
confirmed cannot be presented as the one they read.

## What your integration owes

The policy is a declaration; these three handlers are the implementation of it.

```ts
export const POST = createConnectWebhookHandler({
  secret: process.env.CONNECT_WEBHOOK_SECRET!,
  on: {
    // Access is already denied on Connect's side. Start your own wind-down on
    // the clock `purgeAfter` gives you.
    "workspace.deletion_scheduled": ({ workspaceId, purgeAfter }) =>
      scheduleWindDown(workspaceId, new Date(purgeAfter)),

    // The one that gets skipped. Without it a customer who changes their mind
    // stays locked out of your product forever.
    "workspace.deletion_cancelled": ({ workspaceId }) => cancelWindDown(workspaceId),

    // Drive this off `policy`, not off a second copy of it in your code.
    "workspace.deleted": async ({ workspaceId, policy }) => {
      for (const dataClass of policy?.dataClasses ?? []) {
        if (dataClass.disposition === "erase") await erase(workspaceId, dataClass.key);
        if (dataClass.disposition === "anonymize") await anonymize(workspaceId, dataClass.key);
        if (dataClass.disposition === "retain") {
          // The number you declared, as Connect promised it to the customer.
          await archive(workspaceId, dataClass.key, dataClass.retainDays);
        }
      }
      for (const release of policy?.resourceReleases ?? []) {
        await holdBeforeReissue(workspaceId, release.key, release.quarantineDays);
      }
    },
  },
});
```

<Callout>
Both deletion events carry `policy` — the same frozen snapshot, including
`version`, every class's `retainDays` and every held identifier's
`quarantineDays`. Read it rather than hardcoding the figures you typed into
Console: the event is the version the customer was actually shown, and your
copy drifts the moment somebody edits the policy. It is `null` only for a
deletion confirmed before policies existed, and an application that declared
nothing gets empty arrays rather than a missing key.
</Callout>

Webhooks are the fast channel and are at-least-once. The readiness endpoint
remains authoritative — reconcile against it rather than trusting arrival
order, and make each handler idempotent, because you will see the same event
twice.