{
"$type": "site.standard.document",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#didMention",
"did": "did:plc:w64dlsa4zwjv2wljlvmymldc"
}
],
"index": {
"byteEnd": 141,
"byteStart": 134
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#didMention",
"did": "did:plc:vahtz72vqgetnz3cf4xsa7iv"
}
],
"index": {
"byteEnd": 159,
"byteStart": 146
}
}
],
"plaintext": "This is a draft specification for the implementation of a kind of ATProto universal profile service. Developed during discussion with @lou.gg and @felinus.fish."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 11,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 80,
"byteStart": 66
}
}
],
"plaintext": "Assumption: For now we'll assume that the service is scoped under atmosphere.onl for lexicon / hosting purposes. This might change"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
],
"index": {
"byteEnd": 6,
"byteStart": 0
}
}
],
"plaintext": "tl;dr:"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Create a universal profile \"intermediate representation\" lexicon where all fields are optional"
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This allows us to convert registered app's profile to any other registered app's profile, while still only maintaining one lens per participating app."
}
}
],
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Create panproto lenses that map from all kinds of app specific profiles to this intermediate profile."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The intermediate representation can be used by apps that haven't registered a lens for their own lexicon."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#italic"
}
],
"index": {
"byteEnd": 34,
"byteStart": 31
}
}
],
"plaintext": "The service allows you to list all of a user's profiles, auto-translated to any supported lexicon that you request."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "Apps can now use this to show a list of possible profiles to import to your app-specific profile as just a quick starter profile / import, without that app having to do anything to support any registered profile lexicons."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Target User Experiences"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "To start will focus on target user experiences and how they will be implemented."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Set My Profile In a New App"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 10,
"byteStart": 0
}
}
],
"plaintext": "Situation: I login to an ATProto app and I want to set up my profile."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I go to wherever the profile configuration is in the app. Maybe this is when I first signed in and the app is asking me to configure my profile as part of onboarding."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "The user is presented with all the profiles that they have on other ATProto apps, but adjusted to show only the information supported in this app, possibly as a profile card."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I am able to select a profile card to use as a base, and then customize the profile, if desired, for this particular application."
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Configure a Universal Default Profile"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 10,
"byteStart": 0
}
}
],
"plaintext": "Situation: I want a centralized place I can manage all my profiles, and set a \"default\" profile that I can use in anything supporting the profile service."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 22,
"byteStart": 8
}
}
],
"plaintext": "I go to atmosphere.onl and login and it shows me all the profiles that I have across all applications."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I can click on any profile and edit it, which will update that profile in the corresponding app."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 48,
"byteStart": 34
}
}
],
"plaintext": "I can also create a new \"generic\" atmosphere.onl profile that lets me set almost any kind of profile settings that is possible, and will automatically be translatable into any supported lexicons."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 81,
"byteStart": 67
}
}
],
"plaintext": "Later when setting my profile in other apps, I'm able to select my atmosphere.onl profile as a base, so I can use it as a universal default profile."
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Universal Profile Endpoint"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 10,
"byteStart": 0
}
}
],
"plaintext": "Situation: I'm making an ATProto app and I want to be able to fetch the user's profile, but I'm not really interested in providing an app-specific profile. I just want a sensible way to get their profile without being Bluesky specific."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.orderedList",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"children": [
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 33,
"byteStart": 19
}
}
],
"plaintext": "The user's default atmosphere.onl if they have specified one."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 124,
"byteStart": 110
}
}
],
"plaintext": "A deterministic ( yet currently unspecified ) existing profile from another app, automatically converted to a atmosphere.onl profile lexicon, or another supported lexicon of the app's choice."
}
}
],
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 55,
"byteStart": 41
}
}
],
"plaintext": "My app can fetch the user's profile from atmosphere.onl, and it will infer a sensible default."
}
},
{
"$type": "pub.leaflet.blocks.orderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 99,
"byteStart": 85
}
}
],
"plaintext": "For example, my app can request a profile in the Bluesky profile lexicon format from atmosphere.onl for a user, and receive a valid profile, even if that user only ever has a Roomy or Tangled account, and doesn't have a Bluesky profile. Normie will detect the profile that the user has, and return that with an automatic conversion."
}
}
],
"startIndex": 1
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "Implementation Notes"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Service Complexity and Features"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "I think that we should start off by making the service as simple as possible, and avoid any state, other than possibly some simple caching."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This will allow the service to be easily self-hosted, and allow us to reasonably provide a public instance for free."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 70,
"byteStart": 16
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#link",
"uri": "https://pdsls.dev"
}
],
"index": {
"byteEnd": 224,
"byteStart": 215
}
}
],
"plaintext": "That means that the service will not index profiles across the network. We don't want to have to build a database of all profiles across the entire network. We can just fetch the profiles directly from the PDS like pdsls.dev when the API requests them."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Multiple Profiles Per App"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 51,
"byteStart": 47
}
}
],
"plaintext": "I think most if not all apps generally use the self rkey for the profile record. Some apps like Roomy might allow you to have different profiles for different chat spaces that you join, so there may be more than one profile in the same collection."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 65,
"byteStart": 61
}
}
],
"plaintext": "The server should resolve all the profiles regardless of the rkey."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"plaintext": "API Endpoints"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "xrpc/onl.atmosphere.getProfile"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This will resolve to the user's default profile."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 4,
"plaintext": "Determining the Default Profile"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 57,
"byteStart": 43
}
}
],
"plaintext": "The default profile can be set through the atmosphere.onl app, or it will be inferred by some mechanism which is undefined, other than that it must return the same profile deterministically for multiple request. This doesn't need to remain stable across versions of the service."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 4,
"plaintext": "Query Parameters"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 4,
"byteStart": 0
}
}
],
"plaintext": "did: The DID of the user to resolve the profile for."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 19,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 114,
"byteStart": 92
}
}
],
"plaintext": "lexicon (optional): The lexicon to convert the profile to. If not specified it will use the onl.atmosphere.profile lexicon. If specified, it will use any lenses available to convert the profile to the requested lexicon. It will error if the requested lexicon is not supported."
}
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "xrpc/onl.atmosphere.getProfiles"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This will return the full list of user profiles that can be found across any app lexicon."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"plaintext": "This can be used by apps for the \"Set My Profile In a New App\"."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 3,
"plaintext": "Query Parameters"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 4,
"byteStart": 0
}
}
],
"plaintext": "did: The DID of the user to resolve the profile for."
}
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
],
"index": {
"byteEnd": 19,
"byteStart": 0
}
},
{
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
],
"index": {
"byteEnd": 115,
"byteStart": 93
}
}
],
"plaintext": "lexicon (optional): The lexicon to convert the profiles to. If not specified it will use the onl.atmosphere.profile lexicon. If specified, it will use any lenses available to convert the profiles to the requested lexicon. It will error if the requested lexicon is not supported."
}
}
]
}
}
],
"id": "019dcfd7-194a-777a-9b4c-05aabc879f0f"
}
]
},
"description": "Draft specification of a service that could be used to solve the profile-interop issue we have in the Atmosphere.",
"path": "/3mm7mwpofxk2m",
"publishedAt": "2026-05-19T14:44:54.397Z",
"site": "https://leaflet.pub/p/did:plc:ulg2bzgrgs7ddjjlmhtegk3v",
"tags": [],
"title": "Universal AT Profile Converter Service"
}