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 hosted Scheduler supplies its Bluesky OAuth client. Local CLI OAuth connections use your own hosted client metadata URL in SIMPLE_POST_BLUESKY_CLIENT_ID. Use an app password for a simpler local SDK setup, or connect the CLI to hosted Scheduler to reuse existing accounts.

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.