# portable.agency Link your platformed accounts to an Atmosphere account. **→ [link.portable.agency](https://link.portable.agency)** ![portable.agency screenshot](screenshot.png) First trial: linking User & Agents Discord members to their atproto accounts (preserving the "fascinator" role). ## How this works Each linkage is a *pair of records* on atproto PDSes — one under your control, one under portable.agency's. Those records are the only durable state; there's no service database to go stale or lose. 1. **Link a platformed account.** Authorize the external service (e.g. Discord) so we can confirm your membership and any relevant role. Nothing is written yet. 2. **Sign in with your Atmosphere account.** Fine-grained OAuth — we only request permission to write to the `agency.portable.membership` collection. 3. **Two records are written.** - An **attestation** (`agency.portable.attestation`) on portable.agency's PDS — a third-party statement that your DID owns the linked account. - A **claim** (`agency.portable.membership`) on your own PDS — a self-claim naming portable.agency as the attester. Both records carry the same `service` block. Matching them is the proof. **Multiple linkages.** Record keys are deterministic (hash of `did + service.type + community + identifier`), so re-linking the same external account is idempotent; linking a different account (e.g. a second Discord alt) creates a separate record. You can have N linkages per platform. **Where your state lives.** Nothing server-side except a short-lived cookie that holds the in-flight OAuth handshake. After a successful link, your browser stores just your DID in `localStorage`. When you load the page, the browser uses that DID to query your PDS directly and renders the linkages it finds — no server involvement. Clearing browser data only clears the view, not the linkages themselves. **Verification requires both halves.** The two records reference each other by DID, so confirming a linkage means fetching both — your claim from your PDS and portable.agency's attestation from its PDS. If portable.agency's PDS goes away without a repo backup, the attestation half is lost; your self-claim remains but becomes unverifiable on its own. Atproto repos are cryptographically signed, so anyone archiving portable.agency's repo could keep the attestations verifiable independently. **Unlinking.** Unlinking requires signing in with your Atmosphere account to prove ownership of the DID. Clicking Unlink redirects you to atproto OAuth; once you confirm, both records — the attestation on portable.agency's PDS and the matching claim on your own PDS — are deleted, leaving no orphan pointers. ## Run locally ```bash npm install cp .env.example .env # fill in secrets npm run dev ``` Note: atproto OAuth rejects localhost origins, so end-to-end testing needs a deployed HTTPS URL. The rest (record shapes, Discord handshake) can be exercised locally. ## Lexicons - [`agency.portable.membership`](lexicons/agency.portable.membership.json) — user's self-claim - [`agency.portable.attestation`](lexicons/agency.portable.attestation.json) — third-party attestation Served at `/lexicons/:nsid`.