atjamalpha

Using the lexicons

Explanation + How-to. How to consume atjam from your own app — in any language — and how the @atjam/lexicons package fits in. Read this before you write integration code.

The contract is the schemas, not the package

atjam's public contract is five JSON Lexicon schemas under the at.atjam.* namespace:

  • at.atjam.jam
  • at.atjam.round
  • at.atjam.signup
  • at.atjam.invitation
  • at.atjam.submission

These are published as com.atproto.lexicon.schema records on the AT Protocol network. Per the package README, DNS-based resolution is live: _lexicon.atjam.at TXT points at the authority DID, and the schemas are published on that DID's PDS. A resolver can therefore go from the NSID at.atjam.jam to its schema with no DID known upfront. See PUBLISHING.md for how that resolution chain is set up.

What this means for you: to build on atjam, you write records that conform to those schemas, using whatever ATProto tooling your stack already has. You do not need the TypeScript package, and you do not need anyone's permission — records are written to your own (or your users') PDSes.

The TypeScript package is a reference implementation

The real published artifact is the five at.atjam.* schemas on the ATProto network (resolve them via the _lexicon.atjam.at DNS record, then fetch the com.atproto.lexicon.schema record on the authority DID). The @atjam/lexicons package is a TypeScript reference implementation of those schemas — a convenience inside this monorepo and a porting guide. Because it is "private": true (see lexicons/package.json), npm install @atjam/lexicons will not resolve — consume it as a workspace dependency (below) or port the helpers.

Inside this monorepo it is consumed as a workspace dependency:

// package.json of a workspace package
{
  "dependencies": {
    "@atjam/lexicons": "workspace:*"
  }
}

Outside the monorepo you have two options:

  1. Conform to the schemas directly with your own ATProto client (any language). This is the supported, language-agnostic path. The validation and read logic in src/ is a guide you can port.
  2. Vendor or port the helpers — copy the relevant src/ modules into your project. They are dependency-light: the read layer (src/queries/), the validator (src/validate.ts), and the eligibility predicates (src/eligibility.ts) use only fetch and the package's own types.

The package ships TypeScript source directly: main and types both point at ./src/index.ts, and the public API is re-exported from there.

Importing the raw JSON schemas (within the monorepo)

package.json exposes a second export path so monorepo code can import the raw schema JSON:

import jamSchema from "@atjam/lexicons/json/jam.json";
import roundSchema from "@atjam/lexicons/json/round.json";
// also: signup.json, invitation.json, submission.json

The mapping is "./json/*": "./at/atjam/*.json", so @atjam/lexicons/json/jam resolves to at/atjam/jam.json. Use this when you need the schema definitions themselves (for codegen, validation, or display) rather than the TS types.

The TypeScript public surface

From the package root (import { ... } from "@atjam/lexicons") you get:

  • Record namespacesJam, Round, Signup, Invitation, Submission. Each exports its NSID constant, its TypeScript interface(s), and (for some) type-guard and policy helpers.
  • Queries — the read layer: AT URI parsing, DID → PDS resolution, raw PDS reads, Constellation backlinks, and composed atjam fetchers.
  • ValidatevalidateSignup, the read-only signup validator.
  • Eligibility — pure predicates for "may this DID invite?" and "which invitation makes this signup valid?".
  • NSIDS — a frozen map of the five collection NSIDs.
  • StrongRef — the { uri, cid } reference type, re-exported.

See the API reference for exact signatures.

Configuring the read layer

The read layer reaches two external services. Configure their URLs once at startup; both fall back to canonical public instances if you do not.

import { Queries } from "@atjam/lexicons";

Queries.setConstellationUrl("https://constellation.microcosm.blue");
Queries.setPlcDirectoryUrl("https://plc.directory");
  • Constellation (setConstellationUrl, default https://constellation.microcosm.blue) is a backlinks index: given a target record, it tells you what links to it. atjam uses it to find a round's signups, submissions, and invitations.
  • PLC directory (setPlcDirectoryUrl, default https://plc.directory) resolves a did:plc:… to the PDS hosting it, so cross-DID reads hit the right host. (did:web: is resolved via its .well-known/did.json and needs no directory.)

The web app calls both setters at module load in web/app/root.tsx, reading the URLs from environment (web/app/lib/config.ts).

A read-layer limitation to plan around

There is no global "list all rounds" endpoint. atjam records are spread across PDSes, and Constellation indexes backlinks, not "all records of a type." Two consequences:

  • To list rounds under a jam, you use the jam as a backlink target (fetchRoundsForJam).
  • To build a cross-organizer feed, you must already know the organizer DIDs: fetchHomeFeed(dids) lists each known DID's rounds and merges them. The web app feeds it an env-configured list of organizer DIDs (VITE_KNOWN_ORGANIZERS). There is no discovery of unknown organizers.

This is honest about the current state: atjam is a coordination layer over self-hosted records, not a centralized index.

Authentication and writes (client-agnostic)

The @atjam/lexicons package provides no write helpers — writing records is the application's job, done with whatever ATProto client you use. A write is a standard com.atproto.repo.createRecord call:

com.atproto.repo.createRecord
  repo:       <your DID>
  collection: <one of the at.atjam.* NSIDs>
  record:     { $type, ...fields, createdAt }

In this repo:

  • The web app authenticates users via ATProto OAuth using @atcute/oauth-browser-client, then writes through an atcute Client (agent.rpc.post("com.atproto.repo.createRecord", …)). See web/app/lib/oauth.ts and web/app/lib/writes.ts. The OAuth scope requests write access to exactly the five at.atjam.* collections.
  • The test harness authenticates with an app password via @atproto/api's AtpAgent, then writes through agent.com.atproto.repo.createRecord(…). See tests/src/accounts.ts and tests/src/writes.ts.

Both are just two ways of getting an agent that can create records. Your stack will have its own. The record shapes are identical regardless of client; see Reading, writing, validating for each.