account takedown runbook #
This runbook is for operator action against accounts hosted on this ZDS instance. It covers test-account cleanup, emergency suspension, and long-term host takedown. It is not a global identity takedown mechanism.
authority boundary #
A PDS operator controls local hosting for accounts on that PDS:
- whether this PDS reports a hosted account as active
- whether this PDS serves the account's repo, records, and blobs
- whether this PDS accepts authenticated writes for the account
- whether this PDS emits account-status events on its local firehose
- whether this PDS retains or purges local account data according to policy
A PDS operator does not, by this action alone, erase all downstream copies, control every relay or appview, or tombstone a PLC identity. Downstream services are expected to respect account-status changes, but propagation is hop-by-hop and can lag or diverge.
References:
- Account lifecycle guide: https://atproto.com/guides/account-lifecycle
- Account hosting status spec: https://atproto.com/specs/account
- Sync and
#accountevents: https://atproto.com/specs/sync
status vocabulary #
Atproto distinguishes several inactive hosting states:
deactivated: user or host temporarily pauses the account. Content should not be displayed or redistributed, but infrastructure does not need to delete local copies.suspended: host temporarily pauses the account.takendown: host or service removes the repository long-term for policy or terms reasons.deleted: account deletion. Downstream services should stop serving content and may delete data according to policy.
ZDS stores account hosting state in accounts.account_status. The currently
implemented operator states are:
activedeactivatedtakendown
suspended and deleted are reserved vocabulary from the protocol, but ZDS
does not yet expose operator workflows for them.
Current behavior:
com.atproto.server.deactivateAccountsetsaccount_status: "deactivated".com.atproto.server.activateAccountreturns the account toactiveonly fromdeactivated.com.atproto.admin.updateSubjectStatuscan apply or cleartakendownfor a repo subject.- applying
takendownrevokes local session tokens and OAuth token families. - ZDS emits a
#accountevent withactive: falseand the precise inactive status. com.atproto.sync.getRepoStatusreportsactive: falseand the precise inactive status, and omitsrevwhile inactive.- public repo/blob/read XRPCs and authenticated repo writes are denied while the account is inactive.
response levels #
test account cleanup #
Use this when the operator owns the test account and wants to remove clutter.
Default action:
- Verify the account DID and handle.
- Export or snapshot enough metadata to undo mistakes.
- Deactivate first.
- Leave physical purge as a separate, reviewed step.
temporary safety pause #
Use this when an account may be compromised, abusive, or causing operational harm, but the final policy decision is not ready.
Default action:
- Capture evidence and timestamps.
- Deactivate the account.
- Revoke active local sessions and OAuth grants if the compromise involves credentials.
- Verify
getRepoStatusreports inactive. - Watch relay/appview behavior and logs.
- Decide later whether to reactivate, keep suspended, or perform long-term takedown.
long-term host takedown #
Use this when the PDS will no longer host the account.
ZDS has a first-class takendown state for hosted repo subjects. It stops local
hosting without deleting rows or blob bytes. Treat takedown as the normal
long-term operator action; treat physical purge as a separate retention-policy
decision.
Behavior:
- durable status distinct from user deactivation
#accountevent withactive: falseandstatus: "takendown"getRepoStatusreturnsactive: falseandstatus: "takendown"- writes, repo exports, record reads, and blob reads are denied
- sessions and OAuth grants are revoked
Remaining gap:
- ZDS should add a richer admin audit row for operator status changes. Until then, preserve external notes with actor, reason, timestamps, and affected DID.
investigation checklist #
Before changing account state:
fly ssh console -a zds-pds -C "sqlite3 /data/zds.sqlite3 \
\"select handle,did,account_status,datetime(created_at,'unixepoch'),\
datetime(activated_at,'unixepoch'),email \
from accounts order by created_at;\""
For a candidate DID, inspect footprint before deciding:
did='did:plc:example'
fly ssh console -a zds-pds -C "sqlite3 /data/zds.sqlite3 \"
select 'records', count(*) from records where did='$did'
union all select 'repo_blocks', count(*) from repo_blocks where did='$did'
union all select 'commits', count(*) from commits where did='$did'
union all select 'blobs', count(*) from blobs where did='$did'
union all select 'spaces_owned', count(*) from permissioned_spaces where authority_did='$did'
union all select 'space_actor_state', count(*) from permissioned_space_actor_state where actor_did='$did'
union all select 'space_records', count(*) from permissioned_space_records where repo_did='$did'
union all select 'space_repos', count(*) from permissioned_space_repos where repo_did='$did'
union all select 'session_tokens', count(*) from session_tokens where did='$did'
union all select 'oauth_tokens', count(*) from oauth_tokens where did='$did';
\""
Record:
- handle and DID
- who requested action
- why action is justified
- whether the account is operator-owned test data or another user's account
- current record/blob/space footprint
- whether a backup is needed before action
- planned status and retention policy
current ZDS actions #
For user-initiated deactivation, use the existing XRPC with the user's own bearer token.
For operator-driven takedown, use the admin XRPC with ZDS_ADMIN_TOKEN:
curl -fsS -X POST "https://pds.zat.dev/xrpc/com.atproto.admin.updateSubjectStatus" \
-H "authorization: Bearer $ZDS_ADMIN_TOKEN" \
-H "content-type: application/json" \
--data-binary @- <<JSON | jq .
{
"subject": {
"$type": "com.atproto.admin.defs#repoRef",
"did": "$did"
},
"takedown": {
"applied": true,
"ref": "operator-note-or-ticket"
}
}
JSON
To clear an operator takedown:
curl -fsS -X POST "https://pds.zat.dev/xrpc/com.atproto.admin.updateSubjectStatus" \
-H "authorization: Bearer $ZDS_ADMIN_TOKEN" \
-H "content-type: application/json" \
--data-binary @- <<JSON | jq .
{
"subject": {
"$type": "com.atproto.admin.defs#repoRef",
"did": "$did"
},
"takedown": {
"applied": false
}
}
JSON
If direct SQL is unavoidable in an emergency, the minimum semantic behavior is:
- Set
account_status. - Insert a
#accountevent withactive: falseand the precise status. - Notify crawlers.
- Revoke tokens if credential compromise is part of the incident.
Do not perform only step 1 and call it done; downstream services learn about status through repo-status reads and firehose account events.
verification #
After deactivation or takedown:
curl -fsS "https://pds.zat.dev/xrpc/com.atproto.sync.getRepoStatus?did=$did" | jq .
Expected for ZDS takedown:
{
"did": "did:plc:...",
"active": false,
"status": "takendown"
}
Also verify:
- the account does not appear as active on the landing page
- repo export, record reads, and blob reads do not continue to make unavailable content public
- the firehose emitted a recent
#accountevent - relays/appviews eventually stop serving or redistributing the account's content
physical purge #
Physical purge is different from account status.
Before purging rows or blob bytes:
- confirm this is operator-owned test data, or that policy/terms require purge
- take a database backup or Fly volume snapshot if rollback matters
- understand foreign-key cascades for the affected tables
- decide whether global blob/block tables may contain shared data
- record what was deleted
For non-test accounts, prefer a two-step process:
- deactivate or takedown now
- purge after a retention window
This keeps the emergency action reversible while still stopping local hosting.
remaining manual boundary #
ZDS has first-class user deactivation and operator takedown, including token
revocation, repo-status responses, and #account events. It does not expose a
one-step physical purge command. Purge remains a separately reviewed retention
operation because public repo state, permissioned repos, blobs, credentials,
audit state, and downstream copies have different ownership and recovery
semantics.
Do not keep live account identifiers or database footprints in this runbook. Capture incident-specific evidence in the operator's private record, then use the investigation and verification steps above.