@atjam/web#
TanStack Start app for atjam.
Setup#
pnpm install # from the repo root, hydrates all workspaces
cp .env.example .env # then edit
pnpm dev # starts on http://127.0.0.1:3000
Use
127.0.0.1, notlocalhost. The OAuth spec requires a loopback IP for local-dev redirect URIs, and the OAuth helpers insrc/lib/oauth.tsonly switch into dev mode whenwindow.location.hostname === "127.0.0.1".
src/routeTree.gen.ts is generated by the TanStack Router Vite plugin on first dev/build. It's gitignored. Until you've run pnpm dev (or pnpm build) once, pnpm typecheck will fail with "cannot find module './routeTree.gen'" — this is expected.
Routes#
/— home feed of rounds acrossVITE_KNOWN_ORGANIZERS./jam/$did/$rkey— jam detail + its rounds./round/$did/$rkey— round detail: assignment, milestones, signups, submissions./oauth/callback— OAuth redirect target. Don't navigate here directly.
Authentication#
OAuth is wired via @atcute/oauth-browser-client (PKCE + DPoP, browser-only). The flow:
- Header
<AuthBar />renders an input where the user types their handle (alice.bsky.social). startLogin(handle)resolves the handle → DID → PDS → authorization server, then redirects.- The PDS redirects back to
/oauth/callback, which callscompleteLogin()(consumes the code, persists the DID inlocalStorage), then sends the user home. - On every page load,
<AuthProvider />callsresumeAgent()which rehydrates the OAuth session and exposes a typedClient(@atcute/client) for record writes.
Use useAuth() for the auth state, or useSignedInAgent() when a component only renders for signed-in users.
Client metadata#
- Production:
public/oauth-client-metadata.jsonis served athttps://atjam.at/oauth-client-metadata.jsonand is theclient_idthe PDS sees. - Local dev: uses the loopback
client_idpattern (http://localhost?redirect_uri=...&scope=...) — no metadata file needed, the params are inline. This is why the dev mode check is hardcoded to127.0.0.1.
If you change the canonical hostname (e.g. add a staging deployment), update both public/oauth-client-metadata.json and the production branch of getOAuthMetadata() in src/lib/oauth.ts.
Scope#
The app requests atproto plus write access to all five atjam collections (at.atjam.{jam,round,signup,invitation,submission}). The scope string is derived from NSIDS in @atjam/lexicons so it stays in sync with the lexicons. The metadata file's scope field must be kept manually consistent with this.
Data flow#
| Concern | Mechanism |
|---|---|
| List rounds across the network | com.atproto.repo.listRecords against each DID in KNOWN_ORGANIZERS (no global indexer in v1). |
| Get a specific record | com.atproto.repo.getRecord against the owning PDS. |
| Backlinks (signups for a round, submissions for a round, rounds for a jam) | Constellation /links (Microcosm). |
| DID → PDS endpoint | plc.directory for did:plc:, .well-known/did.json for did:web:. |
Every external read is wrapped in a TanStack Query queryOptions in src/lib/queries.ts, and route loaders call queryClient.ensureQueryData(...) so SSR is uniform.
Open assumptions (will need to verify or change)#
- Constellation endpoint shape.
[GET /links?target=&collection=&path=&limit=&cursor=]is a guess — wrapped insrc/lib/constellation.ts, single point to update. - Write paths still needed. Signup (Step 4), submit (Step 5), and create-round (Step 6) UIs are not yet built — see
../PLAN.md. Auth (Step 3) is in. KNOWN_ORGANIZERSis a v1 shortcut. Replace with a real Jetstream-backed indexer when the network has more than a handful of organizers.