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
| Field | Required | Notes |
|---|---|---|
client_id | Yes | Must exactly match the metadata URL. |
dpop_bound_access_tokens | Yes | Must be true. |
grant_types | Yes | Use authorization_code and refresh_token. |
redirect_uris | Yes | Callback URL list. |
response_types | Yes | Use code. |
scope | Yes | Must include atproto. |
token_endpoint_auth_method | No | none 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-Nonceheaders.
Scheduler implements this for connected Bluesky accounts. If you are building your own OAuth flow, use the official AT Protocol OAuth libraries where possible.