# The account lifecycle `didbot_pds::AccountState` is whether an account's data exists, and whether it is being created or destroyed right now. It answers nothing about whether the account *serves*: that is a set of locks, `didbot_pds::Locks`, hung on the account by named parties, and the account serves when the hasp is empty. `didbot_pds::ServerState` (see [server-lifecycle.md](server-lifecycle.md)) is the deployment's own standing, which is a third thing. Everything below the marker is generated from the types by `didbot_pds::account_lifecycle::generated_docs` and checked against them by `the_committed_diagram_matches_the_types`. Regenerate it with: ``` UPDATE_DOCS=1 cargo test -p didbot-pds account_lifecycle ``` ```mermaid stateDiagram-v2 direction LR [*] --> reserved reserved --> provisioning: hostname published provisioning --> active: records landed, announced active --> decommissioning: erase decommissioning --> decommissioned: erased decommissioned --> [*]: hard delete ``` | state | serves the document | serves the verification method | serves the repository | accepts external writes | on `#account` | | --- | --- | --- | --- | --- | --- | | `reserved` | no | no | no | no | nothing announced | | `provisioning` | yes | yes | no | no | nothing announced | | `active` | yes | yes | yes | yes | `active: true` | | `decommissioning` | yes | yes | no | no | `active: false`, `status: deleted` | | `decommissioned` | yes | yes | no | no | `active: false`, `status: deleted` | | lock | removes reads | removes writes | on `#account` while `active` | | --- | --- | --- | --- | | `frozen` | no | yes | `active: true` | | `quarantined` | no | yes | `active: true` | | `suspended` | yes | yes | `active: false`, `status: suspended` | | `deactivated` | yes | yes | `active: false`, `status: deactivated` | | hold | refuses | | --- | --- | | `prevent-data-deletion` | `active --> decommissioning`, whoever asks | ## The state is data existence, and it moves one way Data can always be served again, and can never be un-erased. That is why the axis is a state machine with no edge backwards, and why a reason to stop serving is never a state: `frozen` as a state would have to be left again, and the state that answers "does the data exist" cannot be the state that answers "who says it may not serve". An `-ing` state is work that is running, so each one names what advances it and what finishes it when nothing else does. `Registry::provision` moves an account from `reserved` through `provisioning` to `active` in one call; `Registry::delete` moves it from `active` through `decommissioning` to `decommissioned`. A process that dies between two durable steps leaves the `-ing` state behind, and `Provisioner::sweep_stale` is what acts on it: a `reserved` or `provisioning` row is reaped, and a `decommissioning` one is resumed and finished. `tests/erasure.rs` kills the process at each step. `reserved` is the first durable step of provisioning and comes *before* the hostname is published, so the zone never holds a record no row names — `tests/route53_seam.rs` proves it against a Route53 double. `decommissioned` keeps the DNS record, the zone, the certificate and `did.json`: a `did:web` identity cannot leave the domain that hosts it without breaking every signature it ever made. The row leaves on a hard delete, which is `Registry::hard_delete` with the DID confirmed by an operator, or the retention sweep once the account's retention has run out. ## Locks are lockout/tagout Any party may hang a lock; each tag is lifted only by the party that hung it; the account serves when nothing hangs. Two parties hanging the same kind of lock are two tags, so lifting one never lifts the other. The parties are `didbot_pds::Party`, and `Registry::lock`/`unlock` take an `Actor` that names which one is acting and, for an operator, who. One recorded exception: an operator lifts a policy's `quarantined`, and the ledger says which operator overrode it. `plan/policy.md`'s rule that an evaluator can never unfreeze holds — the policy's tag comes off by an operator's hand, never by a later verdict. Subtraction always wins. A lock that removes reads removes writes too, and `AgentAccount::policy()` — the one call an enforcement point makes — is the state's baseline with every tag subtracted. No lock touches the document. Over HTTP the account's own credential is the only one this server authenticates, so `bot.did.freezeAgent`/`unfreezeAgent` and `bot.did.deactivateAgent`/`activateAgent` hang and lift the account's *own* tags. An operator's tags, the policy's tag, and every hold are in-process calls on `Registry`, the way `Registry::hard_delete` is. **Self-service erasure respects every lock.** `Registry::delete` is refused while anything hangs: an account that is frozen, quarantined or suspended does not get to destroy what the lock was hung to keep. An operator's `Registry::hard_delete` ignores the locks. ## A hold refuses a destructive edge `PreventDataDeletion` is set and cleared by an operator, both recorded in the account's ledger, and every eraser — the account's own, an operator's confirmed one, the retention sweep — is refused while it is set. The store refuses the row's removal again on its own, so a caller that skipped the provisioner's check still cannot get past it. It adjusts nothing about service: stopping an account is a lock. A pin is not a hold. It is `Retention::Forever`, a value on the retention axis, and it keeps the *row* — the sweep passes a pinned account by and a hard delete is refused — while the account's own erasure goes through it. ## What a relay sees `#account` on `com.atproto.sync.subscribeRepos` is this host's answer to *can you fetch this repository from me, and if not, why*. `active` is fetchability; `status` is the reason when it is false. The generated tables above are the mapping, computed by `AccountState::sync_status` over the state and the locks, and the same function decides whether a lock change is announced at all: a tag that changes the answer is, one that does not is not. The write axis has no wire representation, so a consumer of `#account` alone cannot learn that an account is frozen or quarantined; `bot.did.listAgents` carries every tag with the party that hung it. `takendown`, `desynchronized` and `throttled` are values this deployment has no position for. See [conformance.md](conformance.md).