Lexicon Design Proposal for describing an app in the ATProto ecosystem.
5 1 0

Clone this repository

https://tangled.org/pixeline.be/community.lexicon.app.entry https://tangled.org/did:plc:v4zpi74gy7enfiwke7hmoxv5/community.lexicon.app.entry
git@tangled.org:pixeline.be/community.lexicon.app.entry git@tangled.org:did:plc:v4zpi74gy7enfiwke7hmoxv5/community.lexicon.app.entry

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

App Lexicon Working Group — Proposal Draft#

Status: DRAFT — for review before posting to discourse.atprotocol.community

Author: pixeline.be (Alexandre Plennevaux)

Original Discussion: https://discourse.atprotocol.community/t/a-community-app-lexicon/656/49

Date: 2026-03-20


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 various spreadsheets. 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).
  • schema.org/SoftwareApplication: 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#

  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.

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.

Note on NSID: The namespace community.lexicon is used as a placeholder. The actual NSID should be decided by the working group (e.g., under the Lexicon Community's domain).

{
  "lexicon": 1,
  "id": "community.lexicon.app.entry",
  "defs": {
    "main": {
      "type": "record",
      "description": "An entry describing an app built on or for the AT Protocol. Lives on the submitter's PDS. Use rkey 'self' to signal that the publishing account is the app's official account; use a TID rkey for third-party submissions.",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["name", "url", "createdAt"],
        "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 (website or landing page)."
          },
          "description": {
            "type": "string",
            "maxLength": 1000,
            "maxGraphemes": 300,
            "description": "A short description of what the app does (1-3 sentences)."
          },
          "logo": {
            "type": "blob",
            "accept": ["image/png", "image/jpeg", "image/webp", "image/svg+xml"],
            "maxSize": 500000,
            "description": "App logo (must be square aspect ratio)."
          },
          "category": {
            "type": "string",
            "knownValues": [
              "client",
              "tool",
              "service",
              "game",
              "labeler",
              "other"
            ],
            "description": "The primary category of the app."
          },
          "tags": {
            "type": "array",
            "maxLength": 10,
            "items": {
              "type": "string",
              "maxLength": 64,
              "maxGraphemes": 32
            },
            "description": "Free-form tags for filtering and discovery (e.g. 'photos', 'messaging', 'publishing', 'feeds', 'moderation')."
          },
          "platforms": {
            "type": "array",
            "maxLength": 6,
            "items": {
              "type": "string",
              "knownValues": [
                "web",
                "ios",
                "android",
                "macos",
                "windows",
                "linux"
              ]
            },
            "description": "Platforms the app runs on."
          },
          "projectState": {
            "type": "string",
            "knownValues": [
              "released",
              "beta",
              "developing",
              "unmaintained",
              "archived"
            ],
            "description": "Current development state."
          },
          "brandDid": {
            "type": "string",
            "format": "did",
            "description": "The DID of the app's official/brand ATProto account, if different from the submitter."
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri",
            "description": "Link to the source code repository (if open source)."
          },
          "createdAt": {
            "type": "string",
            "format": "datetime",
            "description": "Timestamp when this record was created."
          },
          "updatedAt": {
            "type": "string",
            "format": "datetime",
            "description": "Timestamp when this record was last updated."
          }
        }
      }
    }
  }
}

Possible improvements#

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.