you're working on an atproto app that needs to write some records to your own collection like app.example.post or app.example.like.
you're already using OAuth instead of app passwords. however you're still requesting transition:generic scope, which means your app is asking for way too much power. that's not good!
you've tried reading the official Permission Sets documentation and felt that this is too difficult. i know -- i couldn't understand them either. turns out, actually using permission sets is easy. it's only reading the documentation that's difficult. so let's skip that part.
here's how you can fix your permissions to be granular and good.
writing to collections
currently you have something like this in your codebase:
scope: "atproto transition:generic",maybe in several places. replace transition:generic with the specific things you need. to write to some collections, add repo:<collection name> permissions per collection -- for example:
scope: "atproto repo:app.example.post repo:app.example.like",one repo for each collection you want to be writing to. yes, you do have to list each of the collections separately unless you want a scary global "write anything" permission (i assume you don't).
i repeat: there is no app.example.* wildcard. it's either repo:* ("i want to write any app's records") or granular repo:foo repo:bla.
optional: granular actions
you can make actions granular per collection. maybe you only want to ask to "create" and "update" but not delete -- this would work:
scope: "atproto repo:app.example.post?action=create&action=update repo:app.example.like",omitting ?action asks for full permissions (create, update, delete), i.e. the same as ?action=create&action=update&action=delete.
for your own app's collections, full permissions usually make sense.
in case you need blobs
if you need to upload images/video, also throw in blob:*/* in there:
scope: "atproto blob:*/* repo:app.example.post repo:app.example.like",but wait... this kinda sucks though?
this will work but the popup the user will see will still be unpleasant and confusing because there's no human-readable description. it will just say "Repository" and pressing "?" will show something like this:
if you want a nicer popup, you need to publish a "permission set".
let's do that now!
making it look nice with a permission set
for a nicer permission dialog, replace that repo:.. stuff with
scope: "atproto include:app.example.fullPermissions",what's app.example.fullPermissions here?
that's a "permission set" which lets you take the permissions above and give them a human-readable description.
your app.example.fullPermissions could look like this:
// at://did/com.atproto.lexicon.schema/app.example.fullPermissions
{
"lexicon": 1,
"id": "app.example.fullPermissions",
"defs": {
"main": {
"type": "permission-set",
"title": "Example App Permissions",
"detail": "Manage Example App posts and likes.",
"permissions": [
{
"type": "permission",
"resource": "repo",
"collection": [
// Collections you want to write to
"app.example.post",
"app.example.like"
]
}
]
}
}
}again, you have to enumerate all collections you want to write to.
as you can see, this is sort of an expanded form of the inline string you had to write earlier. this doc page shows short vs long forms. you'll also find the JSON granular actions syntax and blobs there.
this JSON looks like a lexicon, right? yes! permission sets are just lexicons. you'd publish it the same way you'd publish any lexicon.
after you publish it as a lexicon (create record + update DNS), the include:app.example.fullPermissions syntax in scope will work.
then instead of "Repository" it will say what you wrote, for example something like this. (note clicking "?" would still bring up a table.)
permission set naming
there's nothing special about permission set name. you could've called it app.example.postingAndLikingPermissions. the important bit is that the reverse namespace of your set (here, app.example.) must be "above" the collections that you want that set to write to.
effectively this means that your app's permission sets can only ask for permissions to write to your app's (app.example.*) collections.
if you want to request writing to another app's collections, you'll have to include: that app's permission sets (to display them nicely) or, if the app hasn't published any, you'd have to manually ask for repo:<collection name> permissions for each collection.
evolving permission sets
if you add new collections to your app, you probably will want to broaden permissions of your permission set -- since your already logged in users won't be able to write to those collections.
this is somewhat annoying.
i'm not sure what the official word on this is, but it seems like usually you'd want to add new collections to your existing set and then add some application logic so that it can handle "doesn't have permissions to write this" gracefully and requests re-authentication.
gracefully asking for more permissions as needed is a useful pattern in general. my rule of thumb is to request permissions upfront for writing my own app's records (by bundling them into my main set) but to request permissions as needed to write other apps' records.
hope this helps!
bonus: how to avoid ugly url in the popup
if you're seeing an ugly .json url in the permission request dialog message header, it's because your OAuth client metadata file is in the wrong place. make sure your client_id is set to something like ${PUBLIC_URL}/oauth-client-metadata.json -- that exact name.
then it'll just show your app's name instead of that json path.
of course, for that to work, you'll need to actually serve that file.