An AT-URI 'Deeplink' lexicon?

If there isn’t already a plan I’d like to propose a ‘deeplink’ lexicon. It’d declare supported other URI schemes equivalent to the indicated NSID / AT URI.

Please let me know what you think, any prior art, and if you’d find something like this useful!

Quick examples

For example, Bluesky might publish at://atproto-lexicons.bsky.social/com.atproto.lexicon.deeplink/app.bsky.feed.post:

{
  "$type": "com.atproto.lexicon.deeplink",
  "uris": [
    "https://bsky.app/profile/{{did}}/post/{{rkey}}"
  ]
}

Which would declare that the above bsky.app URI — with {{did}} and {{rkey}} subbed in — is a great way to view AT URIs of the app.bsky.feed.post NSID.

Because it’s stored in the same repo where the app.bsky.feed.post NSID is defined (atproto-lexicons.bsky.social), this would also be considered the “canonical” https URI.

However a similar record may also exist at at://blackskyweb.xyz/com.atproto.lexicon.schema/app.bsky.feed.post, which would be the stable pointer to how blackskyweb.xyz recommends you view these NSIDs.

{
  "$type": "com.atproto.lexicon.deeplink",
  "uris": [
    "https://blacksky.community/profile/{{did}}/post/{{rkey}}"
  ]
}

(I’m using an array here, with “use the first that matches your needs” semantics, as one repo may want to support multiple URLs, or URI schemes. Eg. a new site design, or… well I’m sure someone will build a gemini:// view on atproto data at some point; and it might even be me :joy:)

Where it’s useful

This would be useful for AppViews supporting multiple external lexicons, and wanting to remain up-to-date as to how to link to them.

With the addition of another schema, an account could indicate a stable preference for where they like to view given NSIDs. eg. at://byjp.me/com.atproto.lexicon.deeplinkPreference/app.bsky.feed.post might look like this, to indicate that I want Atmospheric apps to open Bluesky posts in Bluepy:

{
  "$type": "com.atproto.lexicon.deeplinkPreference",
  "deeplinks": ["at://bluepy.social/com.atproto.lexicon.deeplink/app.bsky.feed.post"]
}

This approach also allows for site owners to change their URL scheme without affecting people’s preferences (as they own their deeplink record, they can just update).

Open questions

Having uris as an array is smart, but it has challenges.

  • It supports multiple schemes while maintaining the fast lookup of using the nsid as the record key
  • It allows for multiple equivalent URIs with an implicit preference, for example if the owner is switching to a new site design (with a “beta” URL)
  • …but now it’s hard for deeplinkPreference to indicate preference within a given a deeplink’s list (eg. I like Bluepy’s web interface but not their hypothetical Gemini interface and I prefer Example’s Gemini interface but not their HTTPS interface — this conflict is not easily resolvable with the schemas I’m proposing)
    • A picky user like this can create their own deeplink record and point to it as their preference, though they’d have to manually update their own record if Bluepy/Example’s URLs change.

Architecting these records on a per-NSID basis makes lookup easy, but does mean that for more complication NSID spaces (eg. Bluesky) you’d have to create/maintain bundles of deeplink & deeplinkPreference records in tandem (eg. one for posts, one for reposts, likes, lists etc.)

  • Even complicated apps like Bluesky don’t have that many useful-to-link-to NSIDs
  • You’d probably be using an app to manage your preferences anyway?

Would we want to make the distinction between {{did}} and {{handle}} in the templating language? I’ve opted for only did here (as it’s stable by definition) — but there are sites (eg. keytrace.dev) which don’t support dids in their URLs.

  • It seems reasonable to ask a site that wants to offer support for deeplinks to support dids in their URLs
  • It simplifies the logic for the apps creating links too; they only need to have the did ready when rendering a deeplink preference — rather than preparing a handle which they may not have ready, or need.

There is a security risk here; if example.com’s atproto repo is taken over, and lots of people depend on its deeplink record, then a devious bad actor could update the deeplink to point to an identical looking site with a similar URL and harvest credentials without people noticing.

  • People already have to trust that the sites they use have good security & don’t leak sensitive info — this feels like an extension of that.

Lexicon sketches

com.atproto.lexicon.deeplink sketch
{
  "id": "com.atproto.lexicon.deeplink",
  "defs": {
    "main": {
      "key": "nsid",
      "type": "record",
      "record": {
        "type": "object",
        "required": ["uris"],
        "properties": {
          "uris": {
            "type": "array",
            "min": 1,
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description":  "Template URIs which are equivalent to at-uris with this NSID. {{did}} and {{rkey}} will be replaced with their values, eg. https://example.com/profiles/{{did}}/things/{{rkey}}"
          }
        }
      },
      "description": "A declaration of URIs that support viewing at-uris with this NSID. If the repo hosting this record is the same as the one hosting the NSID's lexicon schema, the first of the listed URIs that is suitable will be considered the canonical alternate URI."
    }
  },
  "$type": "com.atproto.lexicon.schema",
  "lexicon": 1
}
com.atproto.lexicon.deeplinkPreference sketch
{
  "id": "com.atproto.lexicon.deeplinkPreference",
  "defs": {
    "main": {
      "key": "nsid",
      "type": "record",
      "record": {
        "type": "object",
        "required": ["deeplink"],
        "properties": {
          "deeplinks": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "at-uri"
              "description":  "The com.atproto.lexicon.deeplink record for the preferred deeplink(s)."
            },
            "description":  "References to deeplink definitions for this NSID, in order of preference."
          }
        }
      },
      "description": "A declared preference for the URIs this account likes to use to view the stated NSIDs."
    }
  },
  "$type": "com.atproto.lexicon.schema",
  "lexicon": 1
}
7 Likes

as the creator of aturi.to, i would love to see something like this happen! the browser extension and tool suite i’ve got going on would benefit greatly from this sort of thing

6 Likes

Nice! Given your experience here @dame.is, do you have any strong opinions or advice about the structure of the record & what it stores?

For example, I considered adding a “name” field (for “Open with {{name}}” link text), but I figured that opens some i18n complexity we may not need?

I put a prototype together at https://deeplink.byjp.me — anyone’s feedback is welcome :slight_smile:

2 Likes

Hi all,

I also have a prototype for this. I needed a way to mark “app X is able to open record Y at this URL”. I uploaded my design at GitHub - Igalia/at-templates · GitHub, if you want to take a look.

Leaving here some notes.

Template language

At first I started with a similar template language as the one that @byjp.me is proposing, but I quickly run into limitations with it. For most records, the did/handle/rkey are not enough to deep link to it into an app.

For example, to deep link to a app.bsky.feed.like record you’d probably want to open the post that the like is for, assuming that the user is logged in so they will see the post with the “like” icon on.

What I ended up doing, based on what I needed for bluesky/sifa/tangled records, was to support:

  • reading properties off the record
  • deferencing DIDs and AT URIs contained in those properties

There are some well-known variables (repo, did, handle, value which is the record contents, as well as some “transforms” to extract parts of an AT URIs (e.g. to extract the authority out of it).

Some example templates are:

  • Post on Bluesky: https://bsky.app/profile/{repo}/post/{rkey}
  • Like on Bluesky: https://bsky.app/profile/{value.subject.uri|atUriAuthority}/post/{value.subject.uri|atUriRkey}
  • standard.site document: {value.site->url}{value.path}
  • Tangled repo: https://tangled.org/{handle}/{value.name}
  • Sifa education information: https://sifa.id/p/{handle}#education

Information ownership

This information shouldn’t be tracked by whoever owns the definition of the record. As mentioned in the original post, users might want to open a Bluesky post in one of the many Bluesky clients that exist, and it’s not realistic for Bluesky to have to maintain that (it’s both too much work for them, and a single point of failure). Same for all the other record types.

Instead, it should be apps that advertise “hey, I know how to open this record, and you can do it as follows”.

The Community App Lexicon WG (Community App Lexicon WG) is already working on an app manifest that would let apps, among other things, define which record types they can deal with. Ideally, that same lexicon would be extended to also say how.

1 Like

I’d second nicr’s observation that this kind of functionality feels most natural to me as part of a general “project declaration”. Eg, bundled with other metadata about a service or software deployment, like a logo/avatar, project name, etc.

And then users express preferences a bit more high-level like “i’d like to use this project (DID/URL) to interact with this app modality (evolving bundle of NSIDs)”, instead of “i’d like to use this URL pattern with this specific NSID”.

1 Like

I really like what you’ve built here @nicr.dev! I see the need for an expanded template language, and also (as @bnewbold.net also noted) the value of attaching this to a “Project” record, rather than it just being on its own.

I figure there will end up being something similar to my me.byjp.atproto.deeplink.preference record which folks can use to assert that “I prefer using the mu.social project”, which would bundle all the deep-linkable templates that it can handle.

(I think it’d be an interesting test of this model to build an “AT URI router” with 3 or 4 projects’ worth of templates loaded, especially where there are overlapping claims to single NSIDs)

I had an initial concern that this was server-driven (“Entirely over public XRPC”) but I see that this is just for the moments when record information needs to be retrieved to be able to resolve the template (and that the methods can be supplied/overridden! :heart_eyes:)

The only thing I think you may want to reconsider is having a default localised string inside the com.example.app.handlers:targets[].label field. In the ‘Global declaration for an app’ space particularly, I think support for multiple languages will be important (eg. what would mu.social put in there, given it operates in many locales?)

(Oh no. I think I’ve just spawned an important side-quest: @bnewbold.net are you aware of any thoughts on protocol-native translations for situations like this? I have some initial thoughts, as I keep bumping into it, but they’re very early!)

How can I help you progress with this @nicr.dev?