Skip to main content

Bluesky OAuth app setup

Bluesky uses AT Protocol OAuth. There is no central developer app registration portal. Your app is identified by a public client metadata JSON document.

The CLI bundles its own OAuth client at https://simplepost.dev/oauth/client-native-metadata.json and Scheduler bundles a hosted client too, so most users do not need to run their own. Set up your own client only if you are building an app that needs its own brand identity in the consent screen, or if app passwords are not enough.

Client metadata

Host a JSON document at a public HTTPS URL. The document URL is also the client_id.

Browser or public client example:

{
"client_id": "https://yourapp.com/oauth/client-metadata.json",
"application_type": "web",
"client_name": "Your App Name",
"client_uri": "https://yourapp.com",
"dpop_bound_access_tokens": true,
"grant_types": ["authorization_code", "refresh_token"],
"redirect_uris": ["https://yourapp.com/oauth/callback"],
"response_types": ["code"],
"scope": "atproto transition:generic",
"token_endpoint_auth_method": "none"
}

Confidential server-side client example:

{
"client_id": "https://yourapp.com/oauth/client-metadata.json",
"application_type": "web",
"client_name": "Your App Name",
"client_uri": "https://yourapp.com",
"dpop_bound_access_tokens": true,
"grant_types": ["authorization_code", "refresh_token"],
"redirect_uris": ["https://yourapp.com/oauth/callback"],
"response_types": ["code"],
"scope": "atproto transition:generic",
"token_endpoint_auth_method": "private_key_jwt",
"token_endpoint_auth_signing_alg": "ES256",
"jwks": {
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "YOUR_PUBLIC_KEY_X",
"y": "YOUR_PUBLIC_KEY_Y",
"kid": "key-1"
}
]
}
}

Required fields

FieldRequiredNotes
client_idYesMust exactly match the metadata URL.
dpop_bound_access_tokensYesMust be true.
grant_typesYesUse authorization_code and refresh_token.
redirect_urisYesCallback URL list.
response_typesYesUse code.
scopeYesMust include atproto.
token_endpoint_auth_methodNonone for public clients, private_key_jwt for confidential clients.

Generate ES256 keys

Using Node.js:

import crypto from "node:crypto";

const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
namedCurve: "P-256",
});

const privateJwk = privateKey.export({ format: "jwk" });
const publicJwk = publicKey.export({ format: "jwk" });

privateJwk.kid = "key-1";
publicJwk.kid = "key-1";

console.log("Public JWK:", JSON.stringify(publicJwk, null, 2));
console.log("Private JWK:", JSON.stringify(privateJwk, null, 2));

Keep the private key in a secret manager or encrypted server configuration. The public key goes in the metadata document.

OAuth flow requirements

Bluesky OAuth uses:

  • PKCE for authorization code exchange.
  • PAR, or pushed authorization requests.
  • DPoP proofs for token and API requests.
  • Nonce handling from DPoP-Nonce headers.

Scheduler implements this for connected Bluesky accounts. If you are building your own OAuth flow, use the official AT Protocol OAuth libraries where possible.