Reference file: migration-lifecycle.md
Migration and Account Lifecycle
Account migration lets a DID move between PDS hosts without changing the account identifier. Tempest keeps this flow explicit: an imported account is present in local storage, but it is not publicly served until activation proves the account belongs on this PDS.
Concepts
Atproto account identity is the DID. Handles may change, but clients and relays
follow the DID document's #atproto_pds service to find the authoritative PDS.
A migration therefore has two parts:
- Move repository and blob data to the new PDS.
- Update identity so the DID document points at the new PDS.
active=false means Tempest must not redistribute repo content. Sync repo reads,
record reads, blob reads, and commit events are suppressed while the account is
inactive. Account status and identity metadata may still be visible.
XRPC Methods
Account lifecycle:
com.atproto.server.checkAccountStatuscom.atproto.server.getServiceAuthcom.atproto.server.reserveSigningKeycom.atproto.server.activateAccountcom.atproto.server.deactivateAccountcom.atproto.server.requestAccountDeletecom.atproto.server.deleteAccount
Migration data:
com.atproto.repo.importRepocom.atproto.repo.listMissingBlobs
createAccount also accepts an existing did when accompanied by a valid
service-auth proof for com.atproto.server.createAccount.
Create and import flow
When createAccount receives an existing DID, Tempest validates the service-auth
proof and creates the account as:
active=false
status=deactivated
The account receives stable signing-key material and an empty repo, but public
sync reads remain disabled. importRepo then replaces the repo with the imported
CAR after verification.
The import boundary checks:
- valid CAR container and commit root
- commit block is present and matches its CID
- commit DID matches the authenticated account
- complete MST graph and all referenced record blocks are present
- commit signature verifies against the DID document
- replacement is atomic, so failed imports leave the prior repo intact
Post-import writes use the normal repo write path. Revisions are monotonic: if the clock would produce a lower or equal TID, Tempest increments from the imported current revision.
Blob readiness
listMissingBlobs compares blob references indexed from current records with
local blob metadata. It returns referenced CIDs that still need to be uploaded.
checkAccountStatus reports repo, record, public blob, and missing blob counts,
plus migrationReady. Missing blobs keep migrationReady=false.
Activation and lifecycle events
activateAccount verifies local DID-document consistency before changing state.
The DID document must match the account and expose #atproto_pds for this
Tempest public URL.
Activation emits events in migration-safe order:
#identity
#account active=true
#commit repo.activate
The commit event contains a CAR slice for the latest imported state. After this, public sync reads and future commit events are enabled.
deactivateAccount sets active=false, status=deactivated, and emits an
account event before public content can be redistributed again. deleteAccount
sets status=deleted, revokes active sessions, and emits #account active=false.
Recovery notes
A missing or bad service-auth proof fails closed before account creation for an existing DID. This covers the unavailable-old-PDS path until fuller PLC and external DID resolution flows are implemented.
Self-controlled did:web accounts can be created with service auth and activated
when their local DID document points at this PDS. This is the current tested path
for bring-your-own identity migration.
Verification
hurl --test --jobs 1 \
--variable base_url=http://localhost:4000 \
test/smoke/migration-lifecycle.hurl
The smoke test covers status, service auth, exported-CAR fixture reads, bad CAR atomicity, missing blobs, deactivation suppression, activation, and deletion.