PDS MOOver web ui #
A SvelteKit app that moves an Atmosphere account from one PDS to another. 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. The migration logic itself lives in
@pds-moover/moover.
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 theaccount:status?action=manageOAuth 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:* account:status?action=manage |
| Missing blobs | atproto blob:*/* |
| Rotation key | atproto identity:* |
| Backups | atproto identity:* rpc:com.pdsmoover.backup.getRepoStatus?aud=did:web:xrpc.pdsmoover.com#repo_backup rpc:com.pdsmoover.backup.signUp?aud=did:web:xrpc.pdsmoover.com#repo_backup rpc:com.pdsmoover.backup.requestBackup?aud=did:web:xrpc.pdsmoover.com#repo_backup rpc:com.pdsmoover.backup.removeRepo?aud=did:web:xrpc.pdsmoover.com#repo_backup |
| Admin | atproto rpc:com.pdsmoover.pds.checkAdminAuth?aud=did:web:xrpc.pdsmoover.com#repo_backup rpc:com.pdsmoover.pds.listPds?aud=did:web:xrpc.pdsmoover.com#repo_backup rpc:com.pdsmoover.pds.searchPds?aud=did:web:xrpc.pdsmoover.com#repo_backup rpc:com.pdsmoover.pds.updatePds?aud=did:web:xrpc.pdsmoover.com#repo_backup |
See src/lib/oauth/scopes.ts for what each permission unlocks.
Development #
This is one member of the repo's pnpm workspace, so install from the repo root. The commands below work from the root (all packages) or from this directory (just the web ui).
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 test:e2e # Playwright, see below
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.
End-to-end tests #
e2e/ runs the app in Chromium against @pds-moover/mock-atmosphere:
two mock PDSs (each its own OAuth server), plc.directory, slingshot and the PDS MOOver directory
API, all in memory. Specs sign in through the real OAuth client; nothing reaches the internet.
pnpm --filter web-ui exec playwright install chromium # once
pnpm test:e2e # from the repo root: builds the mock, then runs the specs
The suite starts its own dev server on 127.0.0.1:5183, so it does not disturb one you have
running on 5173. E2E_DEBUG=1 logs the browser console and every proxied request.
Deploying (Docker) #
The ui ships as a Docker image built from the repo root (it needs the workspace packages):
just test-build-ui # builds moover_ui:test for this machine, nothing is pushed
just run-test-ui # runs it on http://127.0.0.1:3001, where OAuth sign-in works without a domain
just release # multi-arch build and push of every image, the ui included
The image runs node build (@sveltejs/adapter-node) and reads its settings at start-up, so one
image works for any domain:
| Variable | Value |
|---|---|
PORT |
port to listen on (the composes use 3001) |
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.
After deploying, https://<your domain>/oauth-client-metadata.json must be reachable from the
internet: that URL is the OAuth client_id, and every PDS a user signs in through fetches it. Without
PUBLIC_APP_URL the ui falls back to a loopback OAuth client, which only works when it is opened
on http://127.0.0.1:<port>.
License #
MIT, see the LICENSE.md at the repo root.