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=mefor 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)
- Start minimal — resist scope creep (bmann.ca’s consistent message). Ship a small, useful set; iterate via PRs.
- Apps first — leave SDKs, infrastructure, and libraries for a future, separate lexicon.
- Resolve, don’t store — handles should be resolved from DIDs, not stored redundantly (byarielm.fyi).
- 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).
selfrkey convention — if the app’s official ATProto account publishes its own record with rkeyself, that’s a self-attestation of ownership (zicklag.dev).- 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. - 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) - 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
brandDidcovers 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
selfrkey 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]
- @pixeline.be (Alexandre Plennevaux) — maintains atproto.brussels, proposing to lead the WG
- @byarielm.fyi
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_torelationships — 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 thread: A community app lexicon
- Related thread: Taxonomy for ATmospheric Lexicon and App Directories
- WG Template: Lexicon Community Working Group Template
- Existing lexicon (atproto.garden): garden.atproto.directory.submission.json
- Sample data (UFOs explorer): garden.atproto.directory.submission records
- store.stucco.software: https://store.stucco.software/
- W3C Web Application Manifest: https://w3c.github.io/manifest/
- schema.org SoftwareApplication: https://schema.org/SoftwareApplication
- ATProto Garden (live): https://atproto.garden/
- BlueskyDirectory: https://blueskydirectory.com
- ATProto Brussels app list: https://atproto.brussels/atproto-apps