# Keep and purge deleted data

> How long Kindgi keeps what you delete, the retention policies that purge it, sweeps, keeping records for good, and how long the audit trail is kept.

This page shows how to choose how long Kindgi keeps what you delete, purge it when that time is up, and keep the audit trail as long as you need.

Most deletes in Kindgi are soft. A deleted record drops out of lists and reads at once. It stays in the database, marked deleted, until a **retention policy** says to purge it and a **sweep** does. Without a policy, it stays for good.

## Before you start

- **`admin` on the tenant.** Publishing a policy and sweeping both take it.
- **A client:** the TypeScript SDK (`@kindgi/sdk`), the Python SDK (`kindgi`), or the [Java client](https://docs.kindgi.com/v0.1.9/start/java-app.md) (`com.kindgi:kindgi-client`, preview), which Scala uses too, with `KINDGI_API_URL` and `KINDGI_API_TOKEN` set for your runtime. Under `kindgi dev`, the client finds the running runtime by itself.

## What a policy covers

A retention policy names one **domain**: a kind of record. The console calls each one by the name in the second column.

| Domain            | In the console             | What it purges                                                                                                   |
| ----------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `agent`           | Deleted agents             | unregistered agent versions                                                                                      |
| `flow`            | Deleted flows              | unregistered flow versions                                                                                       |
| `tool`            | Deleted tools              | unregistered tool versions                                                                                       |
| `eval_suite`      | Deleted eval suites        | unregistered versions of test sets and other eval suites                                                         |
| `guardrail`       | Deleted guardrails         | unregistered guardrails                                                                                          |
| `mcp_endpoint`    | Removed MCP servers        | unregistered MCP endpoints                                                                                       |
| `env`             | Deleted environment values | deleted environment values                                                                                       |
| `secret`          | Deleted secrets            | revoked secrets                                                                                                  |
| `run`             | Deleted runs               | **finished runs**, counted from when they finished                                                               |
| `policy`          | Retired policy versions    | unregistered policy versions                                                                                     |
| `judgment`        | Removed judgments          | removed judgments, and the judging queue's history: closed items and skipped runs                                |
| `judge_class`     | Retired judge classes      | retired judge classes                                                                                            |
| `org`             | Deleted orgs               | deleted orgs, once their environments' and MCP endpoints' records are purged                                     |
| `provider`        | Removed model providers    | unregistered model providers                                                                                     |
| `artifact`        | Deleted artifacts          | deleted artifacts, with their files                                                                              |
| `api_key`         | Revoked API keys           | revoked and expired API keys                                                                                     |
| `service_account` | Removed service accounts   | unregistered service accounts                                                                                    |
| `memory`          | Ended memory facts         | every revision of a fact whose life ended (deleted, or past its expiry); a fact under legal hold is never purged |
| `conversation`    | Deleted conversations      | unregistered conversations, with their messages and what recall indexed                                          |
| `*`               | All deleted records        | every domain that has no policy of its own, except `memory` and `conversation`                                   |

`memory` and `conversation` hold people's words, so their retention is opt-in. A `*` policy doesn't reach them. Only a policy that names them does.

> **Caution: run and \* purge finished runs**
>
> Runs aren't deleted first. A policy on `run`, or a `*` policy, purges every finished run once its grace has passed since the run finished. To keep runs while purging the rest, give `run` a policy of its own.

### What outlives a run

A purged run takes its journal, provenance and snapshot with it. These stay, under their own setting:

- **Judgments,** with the copy of the run's input and output each judged run keeps. A judgment that isn't removed stays for good. A removed one goes under `judgment`.
- **The judging queue's history:** items that closed (judged, dismissed, or `erased`) and runs a rule skipped at its cap. They hold ids, versions, statuses and times only. They go under `judgment`, counted from when they closed or were skipped. A purged run's waiting item closes as `erased`.
- **Model-call costs,** which are billing history and stay.

### Webhook deliveries

A [webhook trigger](https://docs.kindgi.com/v0.1.9/guides/webhooks/start-runs-from-webhooks.md)'s deliveries, its fires, are kept for 30 days, whatever the policies say. They're what a repeated delivery is recognized by. A fire keeps the delivery's event only until its run starts, at most about 10 minutes after a crash. Then only the run has it, under `run` retention like any run's input. A skipped, refused or repeated delivery's body is never kept.

- **The people in an event aren't Kindgi identities.** A WooCommerce order names your customer, but erasing a person in Kindgi can't find them in a run's input, unless your pack maps senders to people. Otherwise the run's input goes with the run: under `run` retention, or when the run is deleted.
- **Where a delivery came from is personal data too.** A refused delivery's address is in the [audit](https://docs.kindgi.com/v0.1.9/deploy/retention.md#audit-events), as `evidence.clientAddress`, under the audit's retention. A request to an unknown trigger is in the server's log, under your log retention.

## 1. Set a policy

A policy says how long one domain's deleted records are kept. It's a policy of kind `retention`, whose `spec` is `{ domain, graceSeconds, mode: "purge" }`:

- `domain` is the kind of record, from [What a policy covers](https://docs.kindgi.com/v0.1.9/deploy/retention.md#what-a-policy-covers).
- `graceSeconds` counts from the delete: `0` purges at the next sweep, and `-1` keeps records for good.
- `mode` is `purge`.

Publish it with your client. Each example prints the policy it published.

- **TypeScript**

  `retention-policy.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';
  import type { PolicyId } from '@kindgi/sdk/types';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  // Removed model providers are kept 7 days, then the next sweep purges them.
  const published = await kindgi.policies.publish({
    id: 'acme.keep-providers' as PolicyId,
    version: '1.0.0',
    kind: 'retention',
    spec: { domain: 'provider', graceSeconds: 7 * 86_400, mode: 'purge' },
  });
  console.log(published);
  ```

  ```text
  { policyId: 'acme.keep-providers', version: '1.0.0' }
  ```

- **Python**

  `retention_policy.py`

  ```python
  from kindgi.client import Kindgi

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  # Removed model providers are purged at the next sweep: no grace.
  published = kindgi.policies.publish(
      id="acme.keep-providers",
      version="1.0.1",
      kind="retention",
      spec={"domain": "provider", "graceSeconds": 0, "mode": "purge"},
  )
  print(published)
  ```

  ```text
  policy_id='acme.keep-providers' version='1.0.1'
  ```

- **Java**

  `src/main/java/com/acme/app/RetentionPolicy.java`

  ```java
  package com.acme.app;

  import com.kindgi.client.Kindgi;
  import com.kindgi.client.models.PolicyKind;
  import com.kindgi.client.models.PublishPolicyBody;
  import com.kindgi.client.models.PublishPolicyResult;
  import java.util.Map;

  public final class RetentionPolicy {
    public static void main(String[] args) {
      Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      // Removed model providers are kept 7 days, then the next sweep purges them.
      PublishPolicyResult published = kindgi.policies().publish(PublishPolicyBody.builder()
          .id("acme.keep-providers")
          .version("1.0.0")
          .kind(PolicyKind.RETENTION)
          .spec(Map.of("domain", "provider", "graceSeconds", 7 * 86_400, "mode", "purge"))
          .build());
      System.out.println(published);
    }
  }
  ```

  ```text
  PublishPolicyResult[policyId=acme.keep-providers, version=1.0.0]
  ```

- **Scala**

  `src/main/scala/com/acme/app/RetentionPolicy.scala`

  ```scala
  package com.acme.app

  import com.kindgi.client.Kindgi
  import com.kindgi.client.models.{PolicyKind, PublishPolicyBody}
  import scala.jdk.CollectionConverters._

  object RetentionPolicy {
    def main(args: Array[String]): Unit = {
      val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      // Removed model providers are kept 7 days, then the next sweep purges them.
      val published = kindgi.policies().publish(PublishPolicyBody.builder()
        .id("acme.keep-providers")
        .version("1.0.0")
        .kind(PolicyKind.RETENTION)
        .spec(Map[String, AnyRef]("domain" -> "provider", "graceSeconds" -> Int.box(7 * 86400), "mode" -> "purge").asJava)
        .build())
      println(published)
    }
  }
  ```

  A map with values of several types goes to the Java client as `Map[String, AnyRef]`, a number boxed (`Int.box`).

  ```text
  PublishPolicyResult[policyId=acme.keep-providers, version=1.0.0]
  ```

* **One policy per domain,** plus one for `*`. To change a domain's grace, publish the next version of its policy, as the Python example does. A second policy for a covered domain is refused (`409 policy-scope-taken`):

  ```text
  Retention domain "provider" is already covered by policy "acme.keep-providers": a tenant has one retention policy per domain, and one for "*". Publish a new version of "acme.keep-providers" instead, or unregister it first.
  ```

  A new version can't move a policy to another domain either (`409 policy-scope-changed`): publish a policy for that domain under a new id.

* **A spec that doesn't fit is refused** with `400 validation-failed`, naming the field: `policy.spec/mode can't be "archive" yet: use "purge"`, or `policy.spec/domain must be one of: org, agent, …, provider, * (got "blocks")`.

* **A server can limit how long a domain's records are kept.** A runtime limits runs with `KINDGI_RUN_RETENTION_MAX_DAYS`, in whole days ([the variable](https://docs.kindgi.com/v0.1.9/reference/env-vars.md#kindgi_run_retention_max_days)). Unset, there's no limit. Where there is one, a policy for that domain that keeps records longer, or for good (`-1`), is refused (`400 retention-over-limit`), and so is reinstating a retired version that does:

  ```text
  `graceSeconds` 31536000 (365 days) is over the retention limit for `run` rows, 90 days.
  ```

  A `*` policy isn't refused: in a limited domain, the server keeps records no longer than the limit. A limited domain that no policy covers gets the limit too, and so does a policy stored before the limit was set that keeps records longer or for good.

* **Two policies can still cover one domain** when an older runtime took both. The console warns. Lists, sweeps and the runtime's log say which one applies: unregister the others.

## 2. See what will be purged, and purge it

A sweep purges what's due. The runtime sweeps on its own only when you set `KINDGI_RETENTION_SWEEP_INTERVAL_MS` ([Sweep on a schedule](https://docs.kindgi.com/v0.1.9/deploy/retention.md#sweep-on-a-schedule)). Otherwise, you sweep:

- `retention.scheduled` lists each deleted record a policy will purge, and when.
- `retention.sweep` purges every record past its grace, or one domain's.

Each example lists what a sweep would purge now, then purges it. In the outputs, the policy is at 1.0.1, with no grace, as the Python example publishes it, and one removed provider is waiting.

- **TypeScript**

  `retention-sweep.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  // What a sweep would purge now, then purge it.
  const due = await kindgi.retention.scheduled({ pastGraceOnly: true });
  console.log(
    due.data.map((row) => `${row.domain} ${row.id} (${row.policyId ? `policy ${row.policyId}` : 'the server limit'})`),
  );

  const swept = await kindgi.retention.sweep({ domain: 'provider' });
  console.log(`${swept.totalPurged} purged`);
  ```

  ```text
  [ 'provider 01d0afee-d2c1-4532-aa24-1f85b3da395b (policy acme.keep-providers)' ]
  1 purged
  ```

- **Python**

  `retention_sweep.py`

  ```python
  from kindgi.client import Kindgi

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

  # What a sweep would purge now, then purge it.
  due = kindgi.retention.scheduled(past_grace_only=True)
  print([
      f"{row.domain} {row.id} ({f'policy {row.policy_id}' if row.policy_id else 'the server limit'})"
      for row in due.data
  ])

  swept = kindgi.retention.sweep(domain="provider")
  print(f"{swept.total_purged} purged")
  ```

  ```text
  ['provider 7511b469-9389-49be-b396-348807010bfd (policy acme.keep-providers)']
  1 purged
  ```

- **Java**

  `src/main/java/com/acme/app/RetentionSweep.java`

  ```java
  package com.acme.app;

  import com.kindgi.client.Kindgi;
  import com.kindgi.client.models.RetentionDomain;
  import com.kindgi.client.models.RetentionScheduledPage;
  import com.kindgi.client.models.RetentionSweepBody;
  import com.kindgi.client.models.RetentionSweepResult;
  import com.kindgi.client.resources.RetentionScheduledParams;

  public final class RetentionSweep {
    public static void main(String[] args) {
      Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      // What a sweep would purge now, then purge it.
      RetentionScheduledPage due =
          kindgi.retention().scheduled(RetentionScheduledParams.builder().pastGraceOnly(true).build());
      System.out.println(due.data().stream()
          .map(row -> row.domain() + " " + row.id() + " ("
              + (row.policyId() != null ? "policy " + row.policyId() : "the server limit") + ")")
          .toList());

      RetentionSweepResult swept =
          kindgi.retention().sweep(RetentionSweepBody.builder().domain(RetentionDomain.PROVIDER).build());
      System.out.println(swept.totalPurged() + " purged");
    }
  }
  ```

  ```text
  [provider 76fecb7f-3702-4661-a7d5-bfa5e71255b7 (policy acme.keep-providers)]
  1 purged
  ```

- **Scala**

  `src/main/scala/com/acme/app/RetentionSweep.scala`

  ```scala
  package com.acme.app

  import com.kindgi.client.Kindgi
  import com.kindgi.client.models.{RetentionDomain, RetentionSweepBody}
  import com.kindgi.client.resources.RetentionScheduledParams
  import scala.jdk.CollectionConverters._

  object RetentionSweep {
    def main(args: Array[String]): Unit = {
      val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev

      // What a sweep would purge now, then purge it.
      val due = kindgi.retention().scheduled(RetentionScheduledParams.builder().pastGraceOnly(true).build())
      println(due.data.asScala.map { row =>
        val decidedBy = Option(row.policyId).fold("the server limit")(id => s"policy $id")
        s"${row.domain} ${row.id} ($decidedBy)"
      }.toList)

      val swept = kindgi.retention().sweep(RetentionSweepBody.builder().domain(RetentionDomain.PROVIDER).build())
      println(s"${swept.totalPurged} purged")
    }
  }
  ```

  ```text
  List(provider b981ec8a-7d31-475b-9c59-9e8370f74cec (policy acme.keep-providers))
  1 purged
  ```

Each scheduled record has the policy that decides it and when it will be purged:

```json
{
  "domain": "provider",
  "id": "4b8cfaf3-c18a-4afd-b0e4-1574984fa45f",
  "unregisteredAt": "2026-10-06T11:23:45.358Z",
  "purgeAt": "2026-10-13T11:23:45.358Z",
  "pastGrace": false,
  "policyId": "acme.keep-providers",
  "policyVersion": "1.0.0",
  "graceSeconds": 604800
}
```

- **`domain`** narrows either call to one domain.
- **`pastGraceOnly`**, `past_grace_only` in Python, narrows the list to what a sweep would purge now.
- **`unpolicedDomains`**, in the list's answer, names the domains no policy covers and the server doesn't limit. Their deleted records are kept.
- **`limitApplied: true`**, `limit_applied` in Python, marks a record the server's limit decides: no policy covers its domain, so it has no `policyId`, or its policy keeps records longer than the limit. `graceSeconds` is then the limit.
- **A long list comes in pages.** `limit` is how many records per domain: 25 unless you say, at most 100. `hasMore` says there are more, and `nextCursor`, passed back as `cursor`, gets the next page. In Python, `paginate(kindgi.retention.scheduled, limit=…)`, from `kindgi.client`, walks every page.
- **A purged record is gone for good.** Sweeping again purges nothing new until more records pass their grace. Over HTTP, the calls are `GET /v1/retention/scheduled` and `POST /v1/retention/sweep`, or `/v1/retention/sweep/<domain>` for one domain.
- **A purge that fails is kept and tried again.** Deleting a record can fail when its database errors in the middle of a sweep. The record then stays, counts in that domain's `remaining`, and the next sweep tries it again. Its `retention:purge` audit event says `outcome: failed`, with the error in `evidence.error`.

### Sweep on a schedule

Set `KINDGI_RETENTION_SWEEP_INTERVAL_MS` in `kindgi.env`: how often to sweep, in milliseconds, at least `60000`. `3600000` is hourly. Each round is the same sweep, for every tenant the runtime serves, page by page until nothing due is left. However many replicas you run, one of them runs each round.

- **Rounds keep to the interval.** After the runtime was down longer than one, a round is due at once, and that one round catches up.
- **A round that stops part way is carried on.** It has 30 minutes. One that runs out of time, or meets a tenant whose sweep fails, stays due, and the next check goes on with it. A failed tenant is logged and skipped, never the end of the round.

The startup log says which way it runs, and the limit for runs when there is one:

```text
  Retention: purges deleted rows past their grace every 1 min
  Retention: every 1 h; the limit for runs: 90 days
  Retention: manual (POST /v1/retention/sweep; KINDGI_RETENTION_SWEEP_INTERVAL_MS purges on its own)
```

A pass that purged something logs it, per tenant:

```text
17:41:42.348 INFO  [retention] tenant 5c0a7e11-0000-4000-8000-00000000c0de: purged 1 (provider 1)
```

Rows purged more than a day after they were due mean rounds were missed, such as while the runtime was down. Each round warns about them once:

```text
purged rows more than 24 h past their retention limit (run <count>, the oldest <hours> h past it): retention rounds were missed
```

A value under `60000` stops the runtime at start: `KINDGI_RETENTION_SWEEP_INTERVAL_MS must be at least 60000 milliseconds. Got 59999.`

**Expired rows go on their own,** whatever this setting says. Every hour, the runtime deletes expired idempotency answers, sessions and sign-in state. They aren't records a policy keeps, so no setting turns that on. The retention sweep is for deleted records past their policy's grace. The startup log says so:

```text
  Expired rows: deleted every 1 h (idempotency answers, sessions, sign-in state)
```

## Keep records for good

A policy with `graceSeconds: -1` keeps a domain's deleted records, and sweeps skip it. Not where the server limits the domain: there a hold is refused, and one stored before the limit purges at the limit. With `"domain": "*"`, nothing is purged except in the domains that have a policy of their own, or a limit on the server:

```json
{ "id": "acme.keep-everything", "version": "1.0.0", "kind": "retention",
  "spec": { "domain": "*", "graceSeconds": -1, "mode": "purge" } }
```

Records kept for good aren't listed by `retention.scheduled`, since nothing will purge them.

## Audit events

Retention policies don't touch the audit trail, where the runtime records each access decision, approval decision and sign-in, and changes like those in the table below. Kindgi keeps it for good, unless you start the runtime with `KINDGI_COMPLIANCE_CLASSIFIER` set to:

- `shipped`, the classifier the runtime ships;
- or the absolute path of a classifier JSON of your own.

With `shipped`, the runtime purges audit events by kind, when it starts and then every hour, in the tenants it serves:

| Audit events                                                                                                                                                                                          | Kept for                        |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| Access decisions (`authz-decision`)                                                                                                                                                                   | 90 days; denied ones 365 days   |
| Run outcomes (`run-outcome`), guardrail violations (`guardrail-violation`), changes to [env values](https://docs.kindgi.com/v0.1.9/guides/tools/give-a-tool-env-values.md) (`env-set`, `env-deleted`) | 730 days                        |
| Approval decisions (`hitl-decision`), and changes to secrets, API keys, service accounts and people's access ([On legal hold](https://docs.kindgi.com/v0.1.9/deploy/retention.md#on-legal-hold))      | For good: they're on legal hold |
| Any other kind                                                                                                                                                                                        | For good                        |

Run outcomes and guardrail violations are recorded (runtime 0.1.9 or later):

- **A run's end** (`run-outcome`): one record for each run, however it ends. It's written with the end itself, so no run ends without one; runs that ended before the upgrade have none. Child runs, replays, eval runs and dry runs each have theirs, and the record says which. It says how the run ended, and its only text is the run's failure message.
- **A guardrail violation** (`guardrail-violation`): one record for each guardrail that blocked a step's answer, or let it through with a violation, for each run and step. It holds the check's ids and what it came to, never the check's reason or a judge's answer.

### On legal hold

The changes on legal hold are these kinds:

- **Secrets:** `secret-set`, `secret-rotated`, `secret-rotation`, `secret-rotation-started`, `secret-rotation-failed`, `secret-revoked`, `secret-hard-revoked`;
- **API keys:** `api-key-minted`, `api-key-revoked`;
- **Service accounts:** `service-account-created`, `service-account-granted`, `service-account-ungranted`, `service-account-unregistered`;
- **People:** `person-added`, `person-granted`, `person-ungranted`.

### Check that it's on

The banner says it's on:

```text
  Compliance: on, the shipped classifier: /v1/compliance/*, and audit events purged by kind as it says
```

Without the setting, it says `Compliance: off (KINDGI_COMPLIANCE_CLASSIFIER turns on /v1/compliance/* and purging audit events by kind)`, and no audit event is ever purged. With it on, a pass that purged something logs it, per tenant:

```text
22:19:29.073 INFO  [audit.retention] tenant 5c0a7e11-0000-4000-8000-00000000c0de: purged 1 (authz-decision 1)
```

Any other value stops the runtime at start: ``KINDGI_COMPLIANCE_CLASSIFIER must be `shipped` or the absolute path of a classifier JSON. Got "nonsense".``

### Your own classifier

Your own classifier lists the kinds to purge under `byKind`, each with its `days`, and `onDenyDays` for denied ones; `legalHold: true` keeps a kind for good. A kind it doesn't list is kept.

An entry can name another kind instead of giving a policy. It's then kept, and purged, as that kind is, so the two can't drift apart. The shipped classifier does this for each family above, such as `"secret-revoked": "secret-set"`. This one keeps changes to env values 50 days, removals with them:

```json
{
  "version": 1,
  "default": { "retention": { "days": 90 }, "signed": false, "exportable": false },
  "byKind": {
    "authz-decision": { "retention": { "days": 90, "onDenyDays": 365 }, "signed": false, "exportable": true },
    "env-set": { "retention": { "days": 50 }, "signed": true, "exportable": true },
    "env-deleted": "env-set",
    "hitl-decision": { "retention": { "days": 2555, "legalHold": true }, "signed": true, "exportable": true }
  }
}
```

With `KINDGI_COMPLIANCE_CLASSIFIER=/etc/kindgi/classifier.json`, the banner names the file:

```text
  Compliance: on, the classifier /etc/kindgi/classifier.json: /v1/compliance/*, and audit events purged by kind as it says
```

A pass still counts each kind under its own name: `purged 2 (env-set 1, env-deleted 1)`.

The kind an entry names must have a policy of its own. A name that names another name, or a kind the file doesn't list, stops the runtime at start (exit code 1):

```text
kindgi-runtime: fatal: Error: compliance-classifier.byKind.secret-revoked: names "secret-rotated", which names another kind itself: name the kind with the policy
```

For a kind the file doesn't list, it says `compliance-classifier.byKind.secret-revoked: names "secret-sett", which isn't listed with a policy`.

The setting also turns on `GET /v1/compliance/evidence`, which lists the audit events as compliance evidence. Without it, that route answers `404`.

## In the console

- **Policies → Publish policy → Deleted data** sets a policy: which records, how long to keep them ("Purge right away" to "Keep forever"), and the sentence it will publish, such as "Deleted records are purged for good after 30 days."
- **Deleted data** lists each deleted record with when it was deleted, when it will be purged and the policy that decides it, and has **Purge what's due now**, the same sweep as the API. Where the server's limit decides instead, its grace line says so ("grace 90d, this server's limit"), and a record no policy covers shows **No policy**.

## Next

- [Erase a person's data](https://docs.kindgi.com/v0.1.9/guides/agents/erase-a-persons-data.md): remove what Kindgi holds about one person.
- [Back up and restore the runtime's database](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/back-up-and-restore.md): backups, restores, and replaying erasures after a restore.
- [Logs](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/logs.md): what the runtime logs, and how long your log system keeps it.
