Docs / Guides

Stable

Add Continue with Atmosphere

Use the hosted picker to let people choose an Atmosphere account, then complete your own AT Protocol OAuth flow with that account.

The picker remembers browser-level Atmosphere accounts.The app receives a signed selection_token with DID, handle, state, audience, and optional PDS URL.OAuth tokens stay between the app and the user’s account host.

Install

Load the browser SDK

HTML
<script src="https://login.atmosphereaccount.com/atmosphere-login.js" defer></script>

<button
  data-atmosphere-login
  data-client-id="https://app.example.com/oauth/client-metadata.json"
  data-return-uri="https://app.example.com/auth/atmosphere/selected"
  data-app-name="Example App"
></button>
01

Open the picker

The SDK builds /login/select with client_id, return_uri, state, and optional scope.

02

Verify the selection

Your callback receives selection_token. Verify the ES256 signature and bind iss, aud, state, and return_uri.

03

Start AT Protocol OAuth

Use the selected handle or DID as the login hint. Your app still completes the AT Protocol OAuth profile with the user’s PDS or entryway.

Start with the working example

The reference app is a complete relying-app loop: redirect and popup buttons, picker handoff, callback verifier, app-owned AT Protocol OAuth start, app OAuth callback, and final signed-in app state.

GET/examples/atmosphere-login/app

A copy-paste example app that uses the static SDK, returns to a verifier callback, starts app-owned AT Protocol OAuth, then shows the final signed-in app state. It includes redirect mode and popup mode so you can compare the browser behavior.

01

Click Continue with Atmosphere

The SDK stores a one-time state value, builds the picker URL, and sends the user to /login/select. Redirect mode navigates the page; popup mode keeps the app page open.

02

Choose an account

The picker returns selection_token, client_id, state, selected DID, handle, and issuer as query parameters.

03

Verify before doing anything else

The callback verifies signature, expiry, issuer, audience, state, return URI, and replay key before redirecting to the app OAuth start route. In popup mode, the popup completion page notifies the opener, and the opener still continues through that server-verified callback.

04

Finish as the app

The example app completes AT Protocol OAuth on its own callback and creates its own app session. Atmosphere never receives the app OAuth token.

Complete the ATProto OAuth handoff

Atmosphere Login only selects an account. After the selection token passes, your app should begin normal AT Protocol OAuth with the selected handle or DID as the login hint.

GET/examples/atmosphere-login/oauth/start

Starts app-owned AT Protocol OAuth using the selected handle or DID from the verified selection token.

GET/examples/atmosphere-login/oauth/callback

Exchanges the authorization code on the example app callback and mints an example-app session. Production apps should store their OAuth session in their own token store.

Selection callback to app-owned OAuth
const verified = await verifyAtmosphereLoginCallback({
  url: request.url,
  publicJwk,
  expectedIssuer: "https://login.atmosphereaccount.com",
  expectedClientId,
  expectedReturnUri,
  expectedState,
  replayStore,
});

if (!verified.ok) throw new Error(verified.error);

return redirect("/oauth/start?" + new URLSearchParams({
  login_hint: verified.claims.handle || verified.claims.sub,
}));
Reference replay storage

The example app consumes replay keys in the app database so the final callback rejects a reused selection token across app instances. Production apps should use their own durable store and keep each key until the token expires.

Keep the token boundary clear

Do not send AT Protocol OAuth access tokens to Atmosphere. The relying app owns its OAuth session, refresh behavior, scopes, and logout semantics.

Account management stays with the host

Atmosphere Login gives apps a consistent account picker. It is intentionally not a replacement for the user's PDS account page.

Thin architecture

Atmosphere can show picker history and apps that used the Atmosphere picker. The user's PDS host manages OAuth grants, connected apps, devices, sessions, passwords, keys, recovery, backups, deletion, and migration.

NeedWhere it belongs
Choose which account to useAtmosphere hosted picker and signed selection token.
Grant an app repository accessThe app's normal AT Protocol OAuth flow with the user's PDS or entryway.
Review or revoke all connected appsThe user's PDS-owned account page, usually exposed by the host.
Manage devices, passwords, recovery, backups, or migrationThe user's account host. Atmosphere only routes to that destination when known.

Try it locally

Generate a picker URL from the reference app or your registered app metadata, inspect redirect and popup button snippets, and paste a token to check pass/fail verification.

Seed the local redirect picker

Open /dev/login-picker during local development to seed four fictional saved accounts with profile portraits and enter the normal same-tab redirect picker. The route and avatar mapping are unavailable in hosted production environments.

Add the button

The default button uses the Atmosphere icon and can run as a full-page redirect or a popup. Full-page redirect is the most reliable default; popup mode has an origin-checked completion event for apps that need it.

HTML
<button
  data-atmosphere-login
  data-client-id="https://app.example.com/oauth/client-metadata.json"
  data-return-uri="https://app.example.com/auth/atmosphere/selected"
  data-scope="atproto"
  data-app-name="Example App"
  data-app-homepage="https://app.example.com"
  data-mode="redirect"
></button>
AttributePurpose
data-client-idYour stable app client ID. Production IDs should be HTTPS URLs.
data-return-uriThe exact callback URI registered for receiving the selection token.
data-moderedirect by default, or popup when your app can handle the extra browser constraints. Popup callbacks can call consumeSelection() to notify the opener.
data-app-nameLocal button label and accessibility context; registered metadata is still the picker authority.
Popup mode completion

In popup mode, the callback page should load the browser SDK and call AtmosphereLogin.consumeSelection({ clientId }). The SDK opens a centered window that adapts to the available screen, posts the selection back to the opener, and accepts it only when the return URI origin, client ID, and state all match. Browsers can still promote the flow to a tab or block it unless it starts from a direct user action.

Native mobile apps use the system browser

Open the picker URL with ASWebAuthenticationSession on Apple platforms or an Android Custom Tab, then return through an app/universal link. Mobile websites should use the normal same-tab redirect. Do not depend on the JavaScript popup handoff from a native webview.

Register your app

Registering gives the picker a clear app identity and a return URI allow-list tied to your signed-in Atmosphere account.

GET/account/developer/apps

Signed-in developer page for registering an app name, client ID, logo URL, homepage, allowed return URIs, and an optional verified preferred account host.

01

Sign in with the owner account

The current Atmosphere account becomes the owner of the app registration. That owner can update the name, logo, homepage, and return URI allow-list.

02

Use a stable client ID

For production, the client ID should be an HTTPS URL controlled by your app, usually your OAuth client metadata URL.

03

Add exact return URIs

Every production callback that receives selection_token must be listed exactly. Atmosphere strips URL fragments before matching.

04

Recommend a host you operate (optional)

If the same owner account has claimed a joinable account host, select it as the preferred host. The picker pins it first and labels it as recommended by the app, while keeping every other eligible host available.

Preferred hosts are registration data, not request input

Atmosphere verifies the host claim when the registration is saved and again when the picker opens. Apps cannot nominate an arbitrary host in the picker URL, and revoked, transferred, closed, or unconfigured hosts stop being recommended.

StatePicker labelMeaning
developmentDevelopment appLocal-only development app. Use the ATProto http://localhost/ client ID shortcut with loopback IP return URIs while building.
unverifiedUnverified appSelf-registered production app. The picker warns users before continuing.
trustedTrustedAtmosphere-reviewed app identity with a verified domain manifest and production return URI allow-list.
blockedBlockedThis app cannot use the hosted picker.
Trust information stays compact

Trusted and local development apps show their state in the app card's status pill. Unverified apps receive an expanded warning before account selection, while blocked apps cannot open the picker.

Verify your app domain

Before an app can request Trusted status, its homepage origin must publish a small Atmosphere Login manifest. This proves the registered app identity is controlled by the same domain users see in the picker.

/.well-known/atmosphere-login.json
{
  "version": "atmosphere.login.v0.1",
  "apps": [
    {
      "client_id": "https://app.example.com/oauth/client-metadata.json",
      "app_name": "Example App",
      "homepage": "https://app.example.com",
      "logo_uri": "https://app.example.com/icon.png",
      "allowed_return_uris": [
        "https://app.example.com/auth/atmosphere/selected"
      ]
    }
  ]
}
  • Host this file at the HTTPS origin of your registered homepage.
  • Use an apps array when one domain hosts more than one relying app.
  • client_id, app_name, homepage, logo_uri, and every registered allowed_return_uris entry must match the Atmosphere registration.
  • The developer app detail page fetches this file during Run checks and blocks Trusted review until it passes.

Allowed return URI rules

The return URI is where Atmosphere sends the short-lived account-selection token. Treat it like an OAuth redirect URI: exactness matters.

CaseRule
Registered production appsThe return_uri must exactly match one of the registered allowed return URIs, including scheme, host, port, path, and query.
URL fragmentsFragments are removed before matching and are never used as the delivery location for a selection token.
HTTPSProduction client IDs, homepages, logos, and return URIs must use HTTPS.
Local developmentIn development, the special http://localhost/ client ID can use loopback IP return URIs such as http://127.0.0.1:5173/callback for quick testing.
Avoid broad origins in production

Registered production apps use exact allowed return URIs. Atmosphere does not accept a whole origin such as https://app.example.com as a wildcard for every callback path.

Production checks

Registered apps get a health check panel before they ask users to trust the picker handoff.

GET/account/developer/apps/{clientId}

Developer app detail page with production readiness, picker test URL generation, expected callback shape, and selection token verification.

CheckWhat Atmosphere looks for
Client IDAbsolute URL, HTTPS in production, or the ATProto http://localhost/ shortcut in local development.
HomepageA valid HTTPS homepage so people can identify the app.
LogoA recognizable app mark, preferably served over HTTPS.
Return URIsExact callback URLs; no wildcard origins or fragment delivery.
Loopback/dev URLsAny localhost, 127.0.0.1, or [::1] URL keeps the app in local-development readiness.
Domain alignmentHomepage and client ID domains should clearly belong to the same app identity.
Domain manifestThe app homepage origin serves /.well-known/atmosphere-login.json confirming the registered client ID, identity, and return URI allow-list.
Review statusWhether the app is development, unverified, requested, trusted, or blocked.
  • Local development only means loopback URLs are present.
  • Needs production fixes means one or more production checks failed.
  • Ready to request trusted review means production checks pass and the app can be submitted.
  • Trusted means the app identity and return URI allow-list have been approved.
  • Blocked means the picker will not continue for this app.

Trusted review requirements

Trusted review is about reducing ambiguity in the picker. It is not a security audit of the app's own OAuth implementation.

01

Finalize app identity

Use the production app name, homepage, logo, and client ID that users will recognize.

02

Remove local URLs

Replace the local client ID shortcut and every local homepage, logo, or return URI with HTTPS production or staging URLs.

03

Run the picker test

Generate a test picker URL on the app detail page, complete the handoff, and verify the returned callback URL or token.

04

Publish the domain manifest

Host /.well-known/atmosphere-login.json on the app homepage origin and make sure Run checks shows Domain manifest as passing.

05

Submit context

Explain what the app does, who maintains it, and why the registered domains should be shown as trusted.

Trust can reset

Changing a trusted app's name, homepage, logo, or return URIs returns it to review so the picker does not imply stale trust.

How to move from local dev to production

01

Start with local dev

Use the docs console or your local app with the ATProto http://localhost/ client ID shortcut and a loopback IP return URI while developing.

02

Publish your app identity

Move to an HTTPS client ID, homepage, and logo URL on a domain your app controls.

03

Register production callbacks

Add every production selection callback to /account/developer/apps. Keep staging and production URLs separate.

04

Publish the domain manifest

Serve /.well-known/atmosphere-login.json from the homepage origin so Atmosphere can verify the app identity and callback allow-list.

05

Verify before OAuth

After the picker returns, verify the signed selection token and then start your own AT Protocol OAuth flow with the selected DID or handle as a hint.

TXT
Local:
client_id=http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1%3A5174%2Fauth%2Fatmosphere%2Fselected
return_uri=http://127.0.0.1:5174/auth/atmosphere/selected

Production:
client_id=https://app.example.com/oauth/client-metadata.json
return_uri=https://app.example.com/auth/atmosphere/selected

Verify the selection token

The token is an account-selection handoff, not an OAuth credential.

TS
import {
  fetchAtmosphereLoginPublicJwkForToken,
  verifyAtmosphereLoginCallback,
} from "https://login.atmosphereaccount.com/atmosphere-login-server.js";

const callbackUrl = new URL(request.url);
const selectionToken = callbackUrl.searchParams.get("selection_token");
if (!selectionToken) throw new Error("Missing selection token");

const publicJwk = await fetchAtmosphereLoginPublicJwkForToken(
  selectionToken,
  "https://login.atmosphereaccount.com",
);

const result = await verifyAtmosphereLoginCallback({
  url: callbackUrl,
  publicJwk,
  expectedIssuer: "https://login.atmosphereaccount.com",
  expectedClientId: "https://app.example.com/oauth/client-metadata.json",
  expectedState: stateFromSession,
  expectedReturnUri: "https://app.example.com/auth/atmosphere/selected",
  replayStore,
});

if (!result.ok) throw new Error(result.error);
const { sub: did, handle, pds_url } = result.claims;
ClaimMeaningRequired check
issAtmosphere Account originMust match the expected deployment.
audRequesting app client IDMust equal your client_id.
subSelected account DIDUse as the durable account id.
handleSelected account handleGood for display and login hint.
pds_urlKnown account host URLOptional hint; still verify via OAuth.
stateApp nonceMust match the state you created.
return_uriSelection callbackMust match the route receiving the token.
jtiUnique token idStore until expiry and reject repeat use.

Start ATProto OAuth

Once the selection token passes, use the verified account as a login hint for your own AT Protocol OAuth flow.

TS
if (!result.ok) throw new Error(result.error);

const loginHint = result.claims.handle || result.claims.sub;
const oauthUrl = await yourAtprotoOAuthClient.authorizeUrl({
  loginHint,
  scope: "atproto",
});

return Response.redirect(oauthUrl);
Selection is not authorization

Atmosphere tells your app which account the user picked. Repository reads, writes, blobs, and app sessions still require your normal AT Protocol OAuth grant.

Security model

Do not treat selection as authorization

The selected DID/handle tells your app which account the user chose. It does not grant repository reads, writes, blobs, preferences, or account-management powers.

  • Use a fresh state value for every picker request.
  • Verify the JWT signature with /oauth/jwks.json.
  • Require exact aud, iss, state, and return_uri matches.
  • Reject replayed jti values until the token expires.
  • Start your own AT Protocol OAuth flow after selection.
  • Store OAuth refresh tokens server-side or according to the AT Protocol OAuth profile for your client type.

Production checklist

  • Register the app at /account/developer/apps with a stable HTTPS client ID.
  • Use exact allowed return URIs for production and staging callbacks.
  • Show a recognizable app name, homepage, and HTTPS logo.
  • Verify iss, aud, state, return_uri, expiry, signature, and replay key server-side.
  • Start your own AT Protocol OAuth flow after selection; do not treat the selection token as an app session.
  • Request trusted review when the app identity and production callbacks are ready.
On this page

Next steps

Keep building