atjamalpha

Getting started

Tutorial. One guided path: read a real round and its signups, then write a jam, a round, and a signup. By the end you will have created a working open round on a PDS and signed up for it.

This tutorial uses the @atjam/lexicons TypeScript reference implementation, which is available as workspace:* inside the monorepo. If you are working in another language, read this for the shapes and flow, then write the same records with your own ATProto tooling — see Using the lexicons.

Before you start

You need:

  • The @atjam/lexicons package available in your workspace (it is private, so it is not on npm — see Using the lexicons).
  • For the read steps: nothing. atjam reads are unauthenticated public reads.
  • For the write steps: an authenticated ATProto session. The web app uses OAuth (@atcute/oauth-browser-client); the test harness uses an app password with @atproto/api. Either yields an agent that can call com.atproto.repo.createRecord.

Step 1: Configure the read layer

The read layer talks to two services: a backlinks index (Constellation) and the PLC directory (for DID → PDS resolution). Configure them once at startup. Both default to canonical public instances, so you can skip this if the defaults are fine.

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

Queries.setConstellationUrl("https://constellation.microcosm.blue");
Queries.setPlcDirectoryUrl("https://plc.directory");

The web app does exactly this in web/app/root.tsx.

Step 2: Read a round and its signups

A round lives at an AT URI of the form at://<did>/at.atjam.round/<rkey>. Fetch the round record, then ask Constellation which signups link to it.

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

// Parse the round's AT URI into its parts.
const { did, rkey } = Queries.parseAtUri(roundUri);

// Fetch the round record itself (a direct PDS read).
const round = await Queries.fetchRound(did, rkey);
console.log(round.value.assignment);
console.log(round.value.acceptedSubmissionTypes);

// Find every signup that strong-refs this round.
const signups = await Queries.fetchSignupsForRound(roundUri);
console.log(`${signups.links.length} signups`);

fetchSignupsForRound returns a BacklinksResponse — a list of { uri, cid? } references. To read each signup's body, fetch it with Queries.getRecord (or use the higher-level fetchers for submissions and invitations, which fetch the bodies for you).

Step 3: Derive the round's state

State is not stored — it is derived from the round's milestones. Use the pure Round.deriveState helper:

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

const state = Round.deriveState(round.value); // "open" | "in-progress" | "closed"

"open" means signups are open. "in-progress" means the signup deadline has passed but the submission deadline has not. "closed" means the submission deadline has passed. A round with no deadlines stays "open".

Step 4: Write a jam and a round (as the organizer)

First you need an authenticated agent. Getting one is your ATProto client's job, not atjam's, and it differs by environment — this repo has two real examples: browser OAuth (web/app/lib/oauth.ts) and an app-password AtpAgent (tests/src/accounts.ts). Whatever you use, the result is something that can call com.atproto.repo.createRecord.

@atjam/lexicons ships no write helper, so the createRecord(...) below is the app's own thin wrapper over that call (web/app/lib/writes.ts) — it posts { repo, collection, record } and returns the new { uri, cid }. The record shapes are the part that matters; adapt the wrapper to your client.

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

// 1. Create the jam (the container). Returns { uri, cid }.
const jamRef = await createRecord({
  agent, // your authenticated agent
  collection: NSIDS.jam, // "at.atjam.jam"
  record: {
    $type: NSIDS.jam,
    name: "Old Salt Song Club",
    createdAt: new Date().toISOString(),
  },
});

// 2. Create a round under that jam. Strong-ref the jam by { uri, cid }.
const roundRef = await createRecord({
  agent,
  collection: NSIDS.round, // "at.atjam.round"
  record: {
    $type: NSIDS.round,
    jam: jamRef, // { uri, cid } from step 1
    assignment: "Make anything. Submit a Bluesky post as proof.",
    acceptedSubmissionTypes: ["app.bsky.feed.post"],
    milestones: [
      { label: "submission-deadline", date: daysFromNow(7) },
    ],
    // joinMode omitted → resolves to "open": anyone can sign up.
    createdAt: new Date().toISOString(),
  },
});

(daysFromNow is just any helper that returns an ISO date string, e.g. new Date(Date.now() + 7 * 864e5).toISOString().) The required round fields are jam, assignment, acceptedSubmissionTypes (at least one NSID), milestones (at least one), and createdAt.

Step 5: Sign up for the round (as a participant)

A different account writes a signup that strong-refs the round. For an open round, no invitation is needed.

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

await createRecord({
  agent, // the participant's agent
  collection: NSIDS.signup, // "at.atjam.signup"
  record: {
    $type: NSIDS.signup,
    round: { uri: roundRef.uri, cid: roundRef.cid },
    note: "in, will use guitar",
    createdAt: new Date().toISOString(),
  },
});

The signup button in the web app builds this exact record (web/app/components/signup-button.tsx).

End state

You now have, on real PDSes:

  • a at.atjam.jam record (the organizer's),
  • a at.atjam.round record under it (the organizer's),
  • a at.atjam.signup record from the participant.

Read it back the way Step 2 did: fetchSignupsForRound(roundRef.uri) should return the signup. Note that backlinks travel through the firehose → indexer pipeline, so there is lag (typically a few seconds) before a freshly written signup shows up in Constellation. The test harness has a waitForBacklinks poller for exactly this (tests/src/reads.ts).

Where to go next