Community App Lexicon WG

App Lexicon Working Group — Proposal Draft

Status: DRAFT

Authors: pixeline.be (Alexandre Plennevaux), byarielm.fyi (Ariel M. Lighty), jluther.net (John Luther)

Date: 2026-04-25


Part 1: Lexicon Design Proposal

Context

The AT Protocol ecosystem is growing fast. Multiple independent efforts currently track apps, tools, and services — from Semble collections (200+ apps) to BlueskyDirectory.com, atproto.brussels, AlternativeProto, sdk.blue, and more. At one point, there were 21 separate lists.

There is no shared, machine-readable way to describe what an app is, what it does, or how it plugs into ATProto. This makes discovery fragmented and maintenance duplicative.

Prior Art

  • atproto.garden (dame.is): The most developed starting point. Defines garden.atproto.directory.submission — a record type stored on users’ PDS with fields like name, tagline, description, url, category, tags, projectState, creators, logo, screenshots, and lexicon metadata. Open source at github.com/dame-is/atproto-garden.
  • store.stucco.software (nikolas.ws): Working implementation that creates records to PDS and checks rel=me for verification. Building an AppView that listens to the firehose.
  • BlueskyDirectory.com (jluther.net): Largest meta-listing. Willing to participate.
  • atproto.brussels (pixeline.be): Maintains a curated directory with fields like name, url, platform, short description, category, alternative_to, last_checked. Experiments with PDS-stored user ratings (brussels.loves.appRating).
  • SoftwareApplication - Schema.org Type : Established web vocabulary for describing software applications.
  • W3C Web Application Manifest / MASL: Manifest structures for web apps, noted as structurally similar by ngerakines.me and bmann.ca.
  • ATProto OAuth client metadata: Already contains some app info (name, logo, urls) for apps that implement OAuth.

Design Principles (distilled from the thread)

  1. Start minimal — resist scope creep (bmann.ca’s consistent message). Ship a small, useful set; iterate via PRs.
  2. Apps first — leave SDKs, infrastructure, and libraries for a future, separate lexicon.
  3. Resolve, don’t store — handles should be resolved from DIDs, not stored redundantly (byarielm.fyi).
  4. Signals over taxonomy — rather than a rigid category tree, expose small composable signals (type, capabilities, tags) that consumers can assemble into their own views (pixeline.be).
  5. self rkey convention — if the app’s official ATProto account publishes its own record with rkey self, that’s a self-attestation of ownership (zicklag.dev).
  6. Verification is out-of-band — verification (via rel=me, .well-known, or other methods) is a concern for directories/clients, not the lexicon itself. The lexicon just provides the data.
  7. App icon — apps upload their icon as .png or .jpeg to a designated space on the lexicon to make it easy to display the right app icon. I suggest 1000x1000 (bsky.app compresses to this (if not mistaken).
    "appIcon": {
    "type": "blob",
    "accept": ["image/png", "image/jpeg"],
    "maxSize": 1000000 }
    (dave.self.surf)
  8. App svg — same as above, support .svg so others can display the logo on their app (eg., let me show my roomy icon in the footer).
    (dave.self.surf)

Proposed Lexicon: community.lexicon.app.entry

Below is a proposed minimal lexicon for describing an app in the ATProto ecosystem. It draws heavily from the atproto.garden schema, simplified per thread consensus.

{
  "lexicon": 1,
  "id": "community.lexicon.app.entry",
  "defs": {
    "main": {
      "type": "record",
      "description": "An app entry describing an app built on or for the AT Protocol. This record may be published by third parties or by the app itself. When an official self-published app profile exists, consumers should prefer that profile as the canonical record.",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["name", "url", "recordCreatedAt"],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "maxGraphemes": 100,
            "description": "The display name of the app."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The primary URL for the app, such as its website, landing page, or install page."
          },
          "description": {
            "type": "string",
            "maxLength": 1000,
            "maxGraphemes": 300,
            "description": "A short description of what the app does."
          },
          "logo": {
            "type": "blob",
            "accept": ["image/png", "image/jpeg", "image/webp", "image/svg+xml"],
            "maxSize": 500000,
            "description": "Primary logo for display. Should preferably be square."
          },
          "tags": {
            "type": "array",
            "maxLength": 10,
            "items": {
              "type": "string",
              "maxLength": 64,
              "maxGraphemes": 32
            },
            "description": "Open discovery tags for filtering and search, preferably lowercase."
          },
          "status": {
            "type": "string",
            "knownValues": [
              "unreleased",
              "preview",
              "released",
              "unmaintained",
              "discontinued"
            ],
            "description": "Current release or maintenance status of the app."
          },
          "officialAccountDid": {
            "type": "string",
            "format": "did",
            "description": "The DID of the app's official AT Protocol account, if known."
          },
          "officialProfileUri": {
            "type": "string",
            "format": "at-uri",
            "description": "AT URI of the app's official self-published profile record, if it exists."
          },
          "links": {
            "type": "array",
            "maxLength": 12,
            "description": "Relevant links for the app, including trust/compliance, support, and project resources.",
            "items": {
              "type": "object",
              "required": ["type", "url"],
              "properties": {
                "type": {
                  "type": "string",
                  "knownValues": [
                    "privacy",
                    "terms",
                    "support",
                    "contact",
                    "docs",
                    "blog",
                    "changelog",
                    "source",
                    "status",
                    "other"
                  ],
                  "description": "The kind of link."
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The destination URL."
                },
                "label": {
                  "type": "string",
                  "maxLength": 100,
                  "maxGraphemes": 50,
                  "description": "Optional human-readable label, especially useful when type is 'other'."
                }
              }
            }
          },
          "recordCreatedAt": {
            "type": "string",
            "format": "datetime",
            "description": "Timestamp when this entry record was created."
          },
          "recordUpdatedAt": {
            "type": "string",
            "format": "datetime",
            "description": "Timestamp when this entry record was last updated."
          }
        }
      }
    }
  }
}

{
  "lexicon": 1,
  "id": "community.lexicon.app.profile",
  "defs": {
    "main": {
      "type": "record",
      "description": "The official self-published profile for an app built on or for the AT Protocol. This record should be published by the app's official account and used by consumers as the canonical record for that app.",
      "key": "literal:self",
      "record": {
        "type": "object",
        "required": ["name", "url", "recordCreatedAt"],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "maxGraphemes": 100,
            "description": "The display name of the app."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The primary URL for the app, such as its website, landing page, or install page."
          },
          "description": {
            "type": "string",
            "maxLength": 1000,
            "maxGraphemes": 300,
            "description": "A short description of what the app does."
          },
          "logo": {
            "type": "blob",
            "accept": ["image/png", "image/jpeg", "image/webp", "image/svg+xml"],
            "maxSize": 500000,
            "description": "Primary logo for display. Should preferably be square."
          },
          "tags": {
            "type": "array",
            "maxLength": 10,
            "items": {
              "type": "string",
              "maxLength": 64,
              "maxGraphemes": 32
            },
            "description": "Open discovery tags for filtering and search, preferably lowercase."
          },
          "status": {
            "type": "string",
            "knownValues": [
              "unreleased",
              "preview",
              "released",
              "unmaintained",
              "discontinued"
            ],
            "description": "Current release or maintenance status of the app."
          },
          "links": {
            "type": "array",
            "maxLength": 12,
            "description": "Relevant links for the app, including trust/compliance, support, and project resources.",
            "items": {
              "type": "object",
              "required": ["type", "url"],
              "properties": {
                "type": {
                  "type": "string",
                  "knownValues": [
                    "privacy",
                    "terms",
                    "support",
                    "contact",
                    "docs",
                    "blog",
                    "changelog",
                    "source",
                    "status",
                    "other"
                  ],
                  "description": "The kind of link."
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The destination URL."
                },
                "label": {
                  "type": "string",
                  "maxLength": 100,
                  "maxGraphemes": 50,
                  "description": "Optional human-readable label, especially useful when type is 'other'."
                }
              }
            }
          },
          "recordCreatedAt": {
            "type": "string",
            "format": "datetime",
            "description": "Timestamp when this profile record was created."
          },
          "recordUpdatedAt": {
            "type": "string",
            "format": "datetime",
            "description": "Timestamp when this profile record was last updated."
          }
        }
      }
    }
  }
}

What’s intentionally left out (for now)

These were discussed but deferred to keep v1 minimal:

  • Screenshots, keyFeatures — useful for rich directories but not core metadata.
  • customLexicons / supportedLexicons — valuable for developer-facing directories, but unlikely to be kept up-to-date by most submitters. Can be a v2 addition or a separate linked record.
  • creators array — the submitter’s DID is already the record author. A brandDid covers the brand/org case. Full contributor lists can come later.
  • alternative_to — interesting idea (raised by pixeline.be, supported by yamarten.bsky.social), but adds complexity. Better as a separate record type or tag convention.
  • Verification — per thread consensus, verification is out-of-band. Directories can verify using self rkey convention, rel=me, .well-known, or their own criteria.
  • Ratings / reviews — atproto.garden already has separate lexicons for these (garden.atproto.directory.upvote, .review, etc.). These should remain separate from the directory entry itself.

The self rkey convention

When the app’s official ATProto account creates the record with rkey self (instead of a TID), it signals: “I am the official account for this app.” Any ATProto user can independently verify this by checking that the record’s author DID matches the brandDid (or is the only DID associated). This convention is borrowed from how app.bsky.actor.profile/self works.

Third-party users can also submit entries about apps (using a normal TID rkey), which lets community members list apps whose developers haven’t created records themselves.


Part 2: Working Group Template (filled out)


Name

App Directory Lexicon Working Group

Representatives

Lexicon Community TSC: @bmann.ca

WG Lead: @pixeline.be (Alexandre Plennevaux)

The Working Group lead is not expected to be the most experienced member on the topic of the WG, but is in charge of:

  • Providing updates to the TSC representative about progress made
  • Make sure that WG progress is happening regularly, or (if it’s not happening) that there is a defined plan for when work will restart

Objective

Define a minimal, community-maintained ATProto Lexicon for describing apps in the atmosphere, so that multiple directories, listings, and discovery tools can interoperate on a shared data format — stored in users’ PDS repos and available via the firehose.

Aims & Deliverables

[Last updated: 2026-03-20]

Aim 1: Research and consolidate existing approaches

Survey the data models currently used by atproto.garden, BlueskyDirectory, atproto.brussels, store.stucco.software, AlternativeProto, Semble collections, and the ATProto OAuth client metadata spec. Identify the common core fields and the points of divergence.

  • Deliverable: A comparison document mapping fields across existing directories
  • Status: Partially done (the original thread has surfaced most of the data)

Aim 2: Define a v1 App Directory Entry lexicon

Produce a minimal lexicon (record type) for an app directory entry, covering: identity (name, url), presentation (description, logo), classification (category, tags, platforms, project state), and provenance (brandDid, createdAt).

  • Deliverable: A lexicon JSON file in the Lexicon Community repo, with accompanying documentation
  • Status: In progress (proposal above)

Aim 3: Document the self rkey convention and verification guidance

Write a short specification for how official app accounts signal ownership, and how directory consumers can verify entries using out-of-band methods (rel=me, .well-known, etc.).

  • Deliverable: A guidance document in the Lexicon Community repo
  • Status: Not started

Aim 4: Build or adapt at least one working implementation

At least one app (AppView + client) should use the finalized lexicon to create records on PDS and display a directory. atproto.garden and store.stucco.software are both candidates.

  • Deliverable: A working, open-source directory app that reads/writes the shared lexicon
  • Status: In progress (nikolas.ws and dame.is are both building)

Timeline

[Last updated: 2026-03-20]

  • Research & consolidation: 2 weeks (much already done in the thread)
  • Lexicon v1 definition: 3-4 weeks from WG formation
  • Verification guidance: Can run in parallel, ~2 weeks
  • Working implementation: 4-6 weeks (can overlap with lexicon finalization)

Current Members

[Last updated: 2026-04-25]

Interested contributors from the original discussion thread: @dame.is @nikolas.ws @zicklag.dev @jluther.net @flo-bit.dev @yamarten.bsky.social

If you’d like to formally join the WG, you can edit the wiki to add your name to the bulleted list above and remove it from the ‘interested contributors’ line.

Meeting Details and How to Join

[Last updated: 2026-03-20]

  • Where: TBD — likely a dedicated channel on the ATProto Community Discord, with async discussion on the Discourse forum
  • When: Bi-weekly video/voice calls (day/time to be decided by members), with ongoing async work between meetings
  • Who to contact: pixeline.be (on Discourse or Bluesky) if meetings haven’t happened in a while
  • How to join: Post in the Discourse thread or reach out to any current member. New members should:
    • Read this proposal and the original thread
    • Be willing to contribute to lexicon design, implementation, or testing
    • Ideally, be building or maintaining a directory/listing of ATProto apps

What makes a good member: We’re looking for people who are actively maintaining app directories or building tools for ATProto app discovery. We especially need people who are ready to implement the lexicon in real apps — either by adapting existing directories or building new ones. Experience with ATProto lexicon design or app store / directory taxonomy is a plus, but enthusiasm and commitment to ship something minimal are more important.

Stretch Goals and Long-term Explorations

  • SDK / Library lexicon — a separate record type for developer resources (SDKs, libraries, infrastructure tools). Deliberately excluded from v1 to keep scope tight.
  • Rich metadata extensions — screenshots, key features, supported/custom lexicons, OAuth client ID linking. These can be added as optional fields or separate linked records once v1 is stable.
  • alternative_to relationships — a way for apps to declare what mainstream product they’re an alternative to (e.g., “alternative to Instagram”). Could be a separate record type that references directory entries.
  • Cross-directory trust / curation signals — standardized way for directories to endorse or curate entries (the “trust signal” concept from zicklag.dev). Relates to labeling and moderation.
  • Automated health checking — conventions for directories to report whether an app’s URL is still live.
  • schema.org alignment — mapping the lexicon to SoftwareApplication - Schema.org Type for SEO and web interoperability.
  • Curated lists — a meta-feature allowing anyone to create and publish a curated subset of directory entries (e.g., “Best photo apps”, “Tools for developers”).

Additional Notes and References

original discussion

9 Likes

I made it a wiki. I actually think you can do it yourself, I just haven’t checked permissions.

3 Likes

i think it would be better to have a separate field for svg and bitmap logos

Could you share concrete reasons for separate SVG and bitmap logo fields (implementation, performance, UX, etc.) rather than just a preference?

I could see an argument for that being an array, including wordmarks vs logos and such, light vs dark etc etc but I think “add the square logo that will display” is the right place to start.

Someone can make a “brand kit” lexicon to extend this with.

3 Likes

This allows for consumers to request the “correct” logo for the app with respect to how they need to render it. You’d probably also want the size information included for the bitmap version, where as the svg is scalable.

Sourcing and resizing all the logos for Eurosky Portal took quite a bit of time.

1 Like

You probably want to change sourceUrl to:

  • sourceCode
    • repository
    • provider (github, tangled, codeberg, etc)
    • license (SPDX license)
  • brandDid → account or did
  • on category, I don’t think any of these make too much sense, “client” is extremely broad (This wouldn’t give me meaningful grouping as it stands now)
  • on tags, it may be best to have a set of references to well defined tags? That way we can guarantee some consistency between apps.

createdAt and updatedAt seem misleading in this context: These should probably not be about the record, but about the app itself.

We may also want social links to things like blog / changelog / etc.

The current schema we use for the apps in Eurosky Portal is: eurosky-portal/app/collections/apps.ts at main · eurosky-social/eurosky-portal · GitHub

3 Likes

Many different things will be using a base app lexicon if we keep it minimal and unopinionated.

Using open tags that Leaflet and bsky posts are using is likely the right starting point.

Even category is likely pretty application specific and if it were me I’d remove out of the base definition. Including that tags can be used as categories.

Thanks for the link to EuroSky - great to see another in the wild example.

1 Like

I took the liberty to integrate some of your remarks from the previous version:
• removed category
• renamed projectState to status
• renamed brandDid to officialAccountDid
• replaced sourceUrl with structured sourceCode
• separated record timestamps from app lifecycle timestamps
• kept tags open rather than controlled
• clarified the record description around third-party vs official submissions

One open question I’d like us to discuss explicitly: should official apps have a special fixed key like self, or should we treat them like any other entry and just use normal IDs (TIDs)?

There’s a mode here where I see third parties entering app entries, but then an app owner claiming it, where it should move to their own PDS.

Trezy has this for Cartridge, because he got a canonical database of all games.

Is this relevant for thinking about app lexicon records?

1 Like

Liking this. Some comments, which also reflect some of what I brought up in the discord:

  • why include source code URL and provider? latter can be inferred from the former
  • i think TOS and privacy policy links are far more important than docs/blog/changelog. i can see value in link to blog url for atmospheric reasons, support for customer-product reasons, but not sure about docs/changelog - those seem like things folks can get if they use the app or go to the source code url, respectively.
  • re:categories → tags, i like this and also think platforms should be removed in response. ios vs android vs web is vague to me bc of PWAs, so making this tags lets people do what makes sense to them (e.g., I’d label them mobile-only, ios-only, etc instead).
  • i think the status categories should be refined to: unreleased, preview, released, and discontinued*
  • is app releasedAt that useful? seems like neat but mostly useless info

*or discontinued and unmaintained, undecided if that distinction matters for users - example: zeppelin discontinued, whitewind unmaintained.

1 Like

Very interesting thread and possibly similar to something I’m defining for identifying providers of Atmosphere services, such as PDS (aka “atproto_pds”) and Bluesky AppViews (aka “bskk_appview”). Ideally, service providers would have a lexicon that describes the services they provide and publish it into their PDS repositories to be broadcast through the relay network.

Possible nsid: community.lexicon.provider.declaration

Wondering if I should start a separate discussion or reply with the details here for everyone to assess possible alignment?

1 Like

Yes start a new topic - we really want to converge on a minimal app lexicon here.

A reminder to everyone — the base lexicon can be extended for custom use cases. Then we’ll see what is actually needed when it is used in the wild for a while.

3 Likes

question: what’s the definition of done here? What are the criteria to meet to decide this lexicon is good for publishing? Or… Who gets the final say?

1 Like

How I personally see this:

  • someone else can create an app record before the official team does
  • later the real owner should be able to publish the authoritative version
  • the authoritative version should live under the owner’s account / data store
  • consumers should be able to treat that as the canonical record

EDIT: the more I think about it, the more this wants to be split into two lexicons, because we’re trying to model two different things:

  • an open app entry that anyone can publish
  • an official self-published app profile that consumers can treat as canonical when it exists

Trying to make one record handle both cases creates confusion around ownership, trust, and how to resolve third-party vs official submissions.

With two lexicons, the model stays clearer:

  • third parties can still publish entries early
  • app owners can later publish their own official profile on their own PDS
  • consumers can prefer the official profile when present
  • third-party entries can point to that official record instead of competing with it

That feels cleaner to me than overloading a single lexicon with both directory-entry and canonical-identity semantics. What do you think ?

2 Likes

Thank you for your inputs!

sourcode+tos: agreed. I would consolidate the links entry as an open-ended array with prepared key for TOS, user support contact, and then leave it up to add more, such as the sourcecode. ?

re: platform: I remember someone asking that I include their Discord plugins for Atproto in my curated directory. I declined but ideally, I should have. So I think platforms should be open-ended, with canonical entries (“web”,“android”,“ios”,“linux”,“macosx”,…). It’s very useful for the end-user.
releasedAt could be useful to gauge the robustness of the app, if status is not “discontinued” ? google gives more authority to older websites that are still live.

Wiki updated with latest comments. There are now 2 lexicons:

“community.lexicon.app.entry” : to describe someone else’s app
“community.lexicon.app.profile” : to describe one’s own app

If you divide it into two collections, there seems to be no reason to leave the rkeys literal. Developers may want to register mobile and desktop clients with different functions separately, and they may want to combine them into a brand account without creating a separate app account.

1 Like

We kind of skipped a step of you forming a working group. I’m your sponsor, who are your co-leads? You need consensus with your co-leads. @byarielm.fyi and?

1 Like

I would like here to formally invite @byarielm.fyi @thisismissem.social @xan.lol to act as co-leads in this Working Group, so that together we can bring this to the launch line :slight_smile:

1 Like