This page explains how to use permission scopes in your OAuth flow. If you're building an app with custom Lexicons and want to define and publish your own permission sets, see Permission Sets.
Overview
This guide describes how to request and define permissions for your atproto application. OAuth handles authorization, and this page covers how to specify what your app can access.
Control over atproto resources is granted using permissions. Individual permissions like repo:com.example.post grant access to writing specific record types or API endpoints. Related permissions can be bundled into a permission set with a human-readable title and description — these are what users see in the authorization dialog.
If you're building an app with custom Lexicons, you should define permission sets for them. This gives users clear descriptions of what your app can do, rather than a list of opaque namespace IDs. See Permission Set design for the structure.
If you're building an app that only uses existing Lexicons (like Bluesky's app.bsky.*), you can reference their published permission sets, or request individual granular permissions. Refer to the OAuth flow examples section to see how these appear to users.
Requesting permissions
When your app initiates an OAuth flow with an atproto identity provider, it can request specific permission sets by including them in the scope parameter of the authorization request. This should be supplied as a space-separated list in the oauth-client-metadata.json file when registering your OAuth client:
{
scope: "atproto repo:com.example.post rpc:app.bsky.feed.searchPosts?aud=* blob:*/*",
...
}
OAuth in AT uses Pushed Authorization Requests (PAR). The client metadata file will be fetched dynamically during the session lifecycle, and the scope parameter in oauth-client-metadata.json needs to match the actual authorization HTTP request made by your app:
https://your-authorization-server.com/authorize?
client_id=1234567890&
redirect_uri=https://your-app/callback&
scope=atproto repo:com.example.post rpc:app.bsky.feed.searchPosts?aud=* blob:*/*
response_type=code&
audience=https://myapi&
state=1234567890
You should try to only request enough permissions to cover the features your app needs. We provide transition scopes that grant broad permissions to create, update, or delete any of a user's AT records, including posts from other applications — but in general, these broad permission requests should be avoided. Granular permissions help build trust with your users and minimize friction during the authorization process.
From a user experience perspective, it's best to keep the number of requested permission sets low. Each additional permission set adds more information to the authorization dialog, which can lead to decision fatigue. It is possible to allow users to grant only a subset of requested permissions, but this can lead to edge cases.
Permission types
Permissions relate to user-owned resources on PDS instances and are represented by five types:
| Type | Description | Examples |
|---|---|---|
| repo | Write access to records in the user's repository | repo:com.example.post, repo:com.example.post?action=create |
| rpc | Authenticated calls to remote API endpoints | rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview, rpc:app.example.moderation.createReport?aud=* |
| blob | Media file uploads to the PDS. Cannot be included in permission sets. | blob:*/* (any type), blob:image/* (images only) |
| identity | DID document and handle | identity:handle, identity:* |
| account | Account hosting details. Cannot be included in permission sets. | account:email, account:email?action=manage |
| repo | |
| Description | Write access to records in the user's repository |
|---|---|
| Examples | repo:com.example.post, repo:com.example.post?action=create |
| rpc | |
| Description | Authenticated calls to remote API endpoints |
|---|---|
| Examples | rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview, rpc:app.example.moderation.createReport?aud=* |
| blob | |
| Description | Media file uploads to the PDS. Cannot be included in permission sets. |
|---|---|
| Examples | blob:*/* (any type), blob:image/* (images only) |
| identity | |
| Description | DID document and handle |
|---|---|
| Examples | identity:handle, identity:* |
| account | |
| Description | Account hosting details. Cannot be included in permission sets. |
|---|---|
| Examples | account:email, account:email?action=manage |
Wildcards (*) are allowed in scope string syntax, and grants access to all records — the most common example being blob:*/*, for working with images and video. Partial wildcards are not supported — for example, repo: app.bsky.* is not a valid scope string. If you wanted to request multiple permissions from a particular namespace or Lexicon, you would need to list them out individually, or use a permission set. This is intentional, to avoid overbroad permission requests.
Permission sets are themselves Lexicon schemas and are namespaced like other Lexicons. Permission sets are prefixed with include: in the scope string. Unlike other Lexicons, they can be updated in most contexts.
Clients
OAuth client software is identified by a globally unique client_id. Distinct variants of client software may have distinct client_id values; for example the browser app and Android (mobile OS) variants of the same software might have different client_id values. The client_id must be a fully-qualified web URL from which the client-metadata JSON document can be fetched. For example, https://app.example.com/oauth-client-metadata.json. Some more about the client_id:
- it must be a well-formed URL, following the W3C URL specification
- the schema must be
https://, and there must not be a port number included. Note that there is a special exception forhttp://localhostclient_idvalues for development. - the path does not need to include
oauth-client-metadata.json, but it is helpful convention.
If you have been using client-metadata.json rather than oauth-client-metadata.json, you can make this change to have your domain display on the auth flow page, rather than a URL string.
The auth flow page will display all the relevant permissions being granted. This is true both for apps using granular permissions, and for those using the transitional OAuth scopes.
Client Apps should be sure to check which auth scopes were actually approved during the auth flow. In particular, when requesting permission to read an account email address (account:email), the user might decline that permission while granting others. In theory, any individual permission might be denied while the overall request is granted.
Rolling out changes
When you update your app to use new permission sets, it's important to coordinate the software release with the permission-set updates. If you add new permission sets that require user approval, users will need to re-authorize your app to grant these new permissions.
Your release and deployment process for the client metadata document may be separate from your app's code deployment process. Make sure to plan accordingly — an updated oauth-client-metadata.json that requests new permissions must be live before a version of your app that requires those permissions.
Permission sets can be revised, but removing them or making destructive changes can break existing applications. In general, permission sets should be closely tied to the applications they are designed for, but not necessarily 1:1. Remember that atproto applications can utilize multiple different Lexicons.
OAuth flow examples
Apps that are using atproto identity providers solely for authentication only need to request the atproto permission. This provides the same OAuth flow that you might expect from other identity providers; no features of the Atmosphere social graph are used, and no additional permissions are required:
This is a fully usable Atmosphere integration, and many applications will use this as a starting point. Note that it produces a straightforward, readable "Authorize" flow. The URL of the requesting app is clearly visible, and the permissions dialog states that the app "wants to uniquely identify you," but nothing else.
Next, consider a more complex integration, that requests additional permissions. By default, these will be presented in a summarized view:
In this image, note that there are two different Lexicon groupings: Bluesky permissions, and Skyblur permissions. Each Lexicon defines its own permission sets, as well as its own grouping and presentation of those permissions.
You can click through on the ? icons to see the details of each permission set:
From the expanded atproto and Bluesky permissions, you might recognize atproto (for OAuth login), blob:*/* (for image and video handling), and several other app.bsky.* permissions for profile and social graph interactions.
The Skyblur Lexicon permissions are defined and summarized differently:
This is all at the discretion of app developers and Lexicon designers. We provide a model for using different Lexicons side by side, and for each Lexicon to define its own permission sets and presentation.