Atmosphere Login
The @emdash-cms/auth-atproto package adds an Atmosphere account login option to EmDash. An Atmosphere account is a portable, user-owned identity used across Bluesky and other apps in the AT Protocol network. Users sign in with their handle (e.g. alice.bsky.social) and authenticate at their own provider — EmDash never sees a password.
This is a good fit when:
- Your contributors already have an Atmosphere account.
- You want to gate an org-controlled domain (
*.yourcompany.com) without managing OAuth apps or invites. - You’re building something that’s part of the wider Atmosphere and want consistent identity with the rest of your stack.
Install
Section titled “Install”Install the provider package:
pnpm add @emdash-cms/auth-atprotoAdd the provider to the EmDash integration:
import { defineConfig } from "astro/config";import emdash from "emdash/astro";import { atproto } from "@emdash-cms/auth-atproto";
export default defineConfig({ server: { host: "127.0.0.1", // required for local development; see below }, integrations: [ emdash({ authProviders: [atproto()], }), ],});That’s enough to put Sign in with Atmosphere on the login page and the setup wizard. With no allowlist configured, the first user becomes Admin and self-signup is closed for everyone after that — see allowlists to open it up.
The provider is a public OAuth client and serves its own metadata document at /.well-known/atproto-client-metadata.json, so it works with the configuration above alone — no environment variables, client secret, or OAuth-app registration to set up.
Configure access
Section titled “Configure access”The atproto() provider accepts an allowlist and a default role:
atproto({ allowedDIDs: ["did:plc:abc123..."], allowedHandles: ["*.example.com", "alice.bsky.social"], defaultRole: 30, // Author});| Option | Type | Default | Description |
|---|---|---|---|
allowedDIDs | string[] | none | Exact DID allowlist. |
allowedHandles | string[] | none | Handle allowlist. Supports leading wildcards (*.example.com). |
defaultRole | number | 10 (Subscriber) | Role assigned to allowed users after the first. First user is always Admin. |
The full role ladder is documented in the main authentication guide.
Allowlists
Section titled “Allowlists”If neither allowedDIDs nor allowedHandles is set, only the first user can sign up. Accounts already linked to an EmDash user can continue to sign in, while a new account is rejected with signup_not_allowed.
When at least one allowlist is configured, every login must match it, including logins for existing users. Removing an existing user’s DID and handle from the configured lists prevents that account from signing in. A user is admitted if either list matches:
- DID match. The user’s stable account identifier exactly matches a value in
allowedDIDs. - Handle match. The user’s handle matches an entry in
allowedHandles, exactly or via a leading-wildcard pattern (*.example.commatchesalice.example.comandbob.team.example.com).
Handle allowlists are safe even though handles are mutable. Before admitting a user via a handle match, EmDash independently resolves the handle’s DNS/HTTP record and verifies that it points at the same DID the provider claims. A misbehaving provider cannot simply assert that it owns you.yourcompany.com.
Default role
Section titled “Default role”Allowed users land on the role you set in defaultRole. Only the first user — the one who completes setup — is forced to Admin. There’s no group/role mapping for Atmosphere accounts; if you need finer-grained roles, change the user’s role from Settings → Users after they’ve logged in once.
Set up the first user
Section titled “Set up the first user”When you start a fresh site with the Atmosphere provider configured, the setup wizard offers it as an option for creating the initial admin account.
-
Visit
/_emdash/admin. On Set up your site, enter the site title and optional tagline, then continue. -
On Create your account, enter the email address and optional name to store on the EmDash user.
-
On Secure your account, choose Atmosphere, enter your handle (for example,
alice.bsky.social), and continue. -
Your account provider opens its authorization page. Sign in using the method that provider supports and approve the request.
-
The provider redirects you to EmDash. EmDash creates the first user as Admin, stores the email from step 2, establishes an EmDash session, and opens the dashboard.
Later logins begin with the handle, continue at the account provider, and return with an EmDash session. The provider’s OAuth state and tokens are stored separately from that EmDash session so the OAuth callback can complete and the provider can refresh its own session.
Local development
Section titled “Local development”The AT Protocol OAuth profile requires loopback redirect URIs to use an IP literal (127.0.0.1 or [::1]), not localhost. EmDash transparently rewrites ://localhost to ://127.0.0.1 when generating the redirect URI, but that means your dev session needs to start on 127.0.0.1 too — otherwise the session cookie set on localhost won’t be visible after the redirect lands you on 127.0.0.1.
Astro’s dev server uses Vite, which binds to localhost by default. Set Astro’s top-level server.host option to the loopback IP:
export default defineConfig({ server: { host: "127.0.0.1", }, // ...});Then open http://127.0.0.1:4321/_emdash/admin for the whole flow.
Production
Section titled “Production”The same configuration works in production. The provider serves its own client metadata at:
https://your-site.example.com/.well-known/atproto-client-metadata.jsonAuthorization servers fetch this URL during login to verify the client’s redirect URI. Make sure your deployment’s site URL is reachable on the public internet over HTTPS — internal-only deployments behind a VPN won’t be able to complete a login because the user’s authorization server can’t fetch the metadata document.
If you run EmDash behind a TLS-terminating reverse proxy, set siteUrl so EmDash builds the right redirect URI. Without it, requests look like http://internal-host:4321 and the metadata won’t match what the auth server sees.
Troubleshooting
Section titled “Troubleshooting””Account is not in the allowlist”
Section titled “”Account is not in the allowlist””The handle or DID you signed in with isn’t in allowedDIDs / allowedHandles. Check the wildcard pattern (it must start with *.) and remember the handle match is verified against DNS/HTTP — if the handle’s DID record doesn’t currently resolve to the same DID the provider returned, the match is rejected.
”Self-signup is not allowed”
Section titled “”Self-signup is not allowed””You reached the callback successfully, but no allowlist is configured and you are not the first user. Add the account’s DID to allowedDIDs or its verified handle to allowedHandles. An email invite does not link an Atmosphere DID to an EmDash user.
Login redirects to the login page with no error
Section titled “Login redirects to the login page with no error”This is almost always the loopback-cookie issue described in Local development. Open the admin at http://127.0.0.1:4321 (after setting server.host: "127.0.0.1") and try again.
Handle resolution fails for a self-hosted handle
Section titled “Handle resolution fails for a self-hosted handle”The provider verifies handles by racing DNS-over-HTTPS (Cloudflare’s DoH endpoint) and an HTTP /.well-known/atproto-did lookup. Self-hosted handles need at least one of:
- A
_atproto.<handle>DNS TXT record containingdid=<your-did>, or - An
https://<handle>/.well-known/atproto-didfile containing the DID.
If both methods fail, the handle match is rejected even when the underlying account is valid. DIDs in allowedDIDs aren’t affected — they’re matched directly.