Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow

Files
README.md

PDS MOOver (OAuth beta) #

A SvelteKit app that moves an Atmosphere account from one PDS to another. This branch is the OAuth beta: the user's current account is signed in through their own PDS with atproto OAuth and granular permission scopes instead of a password. Everything runs in the browser; the server only serves pages and the OAuth client metadata.

What it does #

  • /moover – migrate an account (create the account on the new PDS with a service-auth token, import the repo, copy blobs and preferences, sign the PLC operation, activate, deactivate the old account). The user picks a password for the new account. Deactivating the old account needs the account:status?action=manage OAuth permission; PDSs that don't allow it get the UI explaining how to do it by hand.
  • /missing-blobs – copy blobs a previous migration missed from the old PDS to the current one.
  • /rotation-key – add a rotation key the user controls to their did:plc identity.

OAuth scopes #

Each page requests only what it needs; /oauth-client-metadata.json advertises the union.

Page Scope
Migration atproto account:email rpc:com.atproto.server.createAccount?aud=* rpc:app.bsky.actor.getPreferences?aud=did:web:api.bsky.app#bsky_appview identity:*
Missing blobs atproto blob:*/*
Rotation key atproto identity:*

See src/lib/oauth/scopes.ts for what each permission unlocks.

Development #

pnpm install
pnpm dev            # http://127.0.0.1:5173 (must be 127.0.0.1, see below)
pnpm check          # svelte-check
pnpm lint
pnpm test           # vitest
pnpm lex:generate   # regenerate src/lib/lexicons from ./lexicons

Locally the app uses an atproto loopback OAuth client: the redirect URI and scope are encoded in the client_id, and authorization servers only accept 127.0.0.1 as the redirect host. Vite is pinned to 127.0.0.1:5173 for that reason; opening localhost will not sign in.

Deploying (Railway) #

railway.json uses Railpack (pnpm, pnpm build, node build). Set these variables:

Variable Value
PUBLIC_APP_URL https://<your domain> – builds the client_id and redirect URI
ORIGIN same URL, required by @sveltejs/adapter-node
PUBLIC_XRPC_BASE host (no scheme) of the PDS MOOver XRPC API for backups/restore/stats; defaults to xrpc.pdsmoover.com
PUBLIC_HUBBLE_URL base URL of the Hubble mirror used as an alternate restore source; defaults to the test instance

Destination PDSs are listed by the public com.pdsmoover.pds.listPds/searchPds endpoints on the same PUBLIC_XRPC_BASE API.

PORT is injected by Railway. After deploying, https://<your domain>/oauth-client-metadata.jsonn must be reachable; that URL is the OAuth client_id.

License #

MIT, see LICENSE.md.