diff --git a/AGENTS.md b/AGENTS.md index 2c302ed..8eb9313 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Positioning: **professional presence** — "what Read.cv should have been, but y ## Current phase -**Pre-code.** We are in feasibility/direction. Nothing has been scaffolded; no stack has been chosen (that will be an ADR when it happens). Do not start writing product code without an explicit request from Jacob. +**Pre-code.** We are in feasibility/direction. Nothing has been scaffolded; the stack is chosen (ADR 0010: TypeScript + SvelteKit) but scaffolding hasn't started. Do not start writing product code without an explicit request from Jacob. ## Read this first, in this order diff --git a/docs/FEASIBILITY.md b/docs/FEASIBILITY.md index d62fa63..4ab78a9 100644 --- a/docs/FEASIBILITY.md +++ b/docs/FEASIBILITY.md @@ -82,5 +82,5 @@ Deferred to v2+: teal.fm, writing third-party lexicons, the pluggable translatio - ~~Product name~~ → decided: **Mooring** at mooring.page (PD-8; research in [`research/2026-07-30-naming.md`](research/2026-07-30-naming.md)). Trademark screen pending. Fahrenheit stays as codename. - ~~Our lexicon namespace NSID~~ → decided: `page.mooring.*` (ADR 0009). -- Tech stack (will be an ADR before scaffolding). -- Hosted infrastructure target and cost model. +- ~~Tech stack~~ → decided: TypeScript + SvelteKit, first-party `@atproto/*`, direct PDS reads for v1 (ADR 0010; research in [`research/2026-07-30-language-ecosystems.md`](research/2026-07-30-language-ecosystems.md), [`-appview-infra.md`](research/2026-07-30-appview-infra.md), [`-comparable-stacks.md`](research/2026-07-30-comparable-stacks.md)). +- Hosted infrastructure target and cost model (DB choice finalizes here too — ADR 0010 leaves it provisional). diff --git a/docs/NEXT.md b/docs/NEXT.md index 3993ccf..a5ac001 100644 --- a/docs/NEXT.md +++ b/docs/NEXT.md @@ -2,11 +2,11 @@ The flight plan. Each item carries enough context to start cold; update this file whenever an item lands (move it to "Done") or a new one is queued. Decisions made while working an item still go through `decisions/` as usual. -_Last updated: 2026-07-30 (session that produced the founding docs, PR #1)._ +_Last updated: 2026-07-30 (tech-stack session, ADR 0010)._ ## Where things stand -Feasibility is done and the verdict was **build it** (see `FEASIBILITY.md`). All founding decisions are recorded: PD-1..8 in `decisions/product-decisions.md`, ADRs 0001–0009 in `decisions/adr/` — all Accepted, including ADR 0008 (v1 scope, ratified 2026-07-30). The product is **Mooring** at **mooring.page** (domain secured); the repo keeps the codename Fahrenheit. No product code exists yet. +Feasibility is done and the verdict was **build it** (see `FEASIBILITY.md`). All founding decisions are recorded: PD-1..8 in `decisions/product-decisions.md`, ADRs 0001–0010 in `decisions/adr/` — all Accepted, including ADR 0008 (v1 scope) and ADR 0010 (tech stack: TypeScript + SvelteKit, first-party `@atproto/*`, direct PDS reads for v1, BEAM benched for v2 services). The product is **Mooring** at **mooring.page** (domain secured); the repo keeps the codename Fahrenheit. No product code exists yet — scaffolding is unblocked but starts only on Jacob's explicit request. ## Queued, roughly in order @@ -19,35 +19,26 @@ USPTO TESS + EUIPO search for "Mooring" in software/web-services classes. The na - Confirm auto-renew + registrar lock on **mooring.page**. This is a hard operational requirement: `page.mooring.*` is our lexicon authority (ADR 0009); losing the domain means losing the namespace. - Optional cheap defense: the naming research found mooring.fm and mooring.page-adjacent shapes; consider whether any are worth registering defensively. mooring.site was premium-priced and consciously passed on (PD-8) — don't re-buy that debate without new info. -### 3. Tech stack ADR (the next big decision) - -Nothing is scaffolded; the stack is deliberately undecided. Constraints already locked by prior decisions: - -- AGPL-3.0-compatible dependencies (ADR 0002) -- Renderer cleanly separable from the multi-tenant shell, so static export/self-host stays possible (ADR 0005) -- OAuth client supporting granular scopes against both bsky.social and self-hosted PDSes (ADR 0007) -- Solo nights-and-weekends maintainability trumps architectural ambition (PD-7) - -Evaluate for the indexing/AppView layer: **Slices** (slices.network) and **HappyView** (happyview.dev — lexicon-driven AppView, single-binary SQLite v2, by Trezy) vs. hand-rolling direct PDS reads (v1 may need no AppView at all — direct `com.atproto.repo.listRecords` reads were assessed as sufficient; see `research/2026-07-30-lexicon-feasibility.md`). Output: ADR 0010. - -### 4. `page.mooring.*` schema design +### 3. `page.mooring.*` schema design Design the record types for site config + authored pages + source bindings (ADR 0009 sketches `page.mooring.site` / `.page` / `.source` as starting points — not binding). Before shipping v1 records: review hard (schemas are effectively immutable once records exist in the wild — ADR 0004), check lexicon.community for prior art, publish on-network via `com.atproto.lexicon.schema`, and publish permission sets for our OAuth scopes (ADR 0007). Study `site.standard.*` and `id.sifa.*` for conventions (both linked in the lexicon research file). -### 5. Hosted infrastructure target + cost model +### 4. Hosted infrastructure target + cost model -Where does the hosted multi-tenant service (ADR 0005) run, and what does a free-tier user cost us? Must support custom domains with per-tenant TLS at ~$6–8/mo price points (PD-4). No research done yet. +Where does the hosted multi-tenant service (ADR 0005) run, and what does a free-tier user cost us? Must support custom domains with per-tenant TLS at ~$6–8/mo price points (PD-4). This decision also finalizes the DB (ADR 0010 left it provisional-SQLite; a host that favors Postgres flips it). Starting points from the stack survey (`research/2026-07-30-comparable-stacks.md`): the three documented custom-domain patterns — wisp.place's Caddy on-demand-TLS + DNS-TXT-verification recipe (most transferable to self-managed infra), Leaflet's Vercel domains API, Blento's CNAME-to-apex on Cloudflare Workers. SvelteKit (ADR 0010) has first-class adapters for all three targets. -### 6. Build v1 (per ADR 0008 — the scope is ratified; don't re-scope) +### 5. Build v1 (per ADR 0008 — the scope is ratified; don't re-scope) OAuth login → read-only adapters for Bluesky, standard.site (+ `pub.leaflet.*` legacy fallback), sifa → one professional-presence theme → authored pages/config in our lexicon → hosted, free subdomain, paid custom domain. Deferred list lives in the ADR; resist scope creep. ## Standing / background - **Re-verify stale research before leaning on it** — every file in `research/` is dated 2026-07-30; the Atmosphere moves fast. Spot-check anything older than a few months, especially: sifa lexicons (alpha, may have churned), teal.fm namespace (breaking rename expected), granular-OAuth rollout on self-hosted PDSes. +- **Pin + changelog discipline on `@atproto/*`** — the OAuth packages are pre-1.0 and rename things (breaking change in the very week of ADR 0010). Watch for: `oauth-client-node` 1.0, Tap's "typed indexer" from the Spring 2026 roadmap (both are named revisit triggers in ADR 0010). - **Watch the competition**: Leaflet (one feature-cycle from "your website"), Blento (same idea, one-page scale), Bluesky+ (if it ships bundling domain/profile-site features, revisit PD-4 pricing). A periodic landscape refresh earns a new dated research file. - **Optional runway**: Skyseed grants ($5–25K) are compatible with the indie model (PD-3) if wanted. ## Done +- 2026-07-30 — Tech stack decided: ADR 0010 (TypeScript + SvelteKit core, first-party `@atproto/*`, direct PDS reads for v1 — no quickslice/HappyView, Tap named v2 liveness candidate, BEAM benched for v2 services). Research checkpoints: language ecosystems, AppView infra, comparable stacks. - 2026-07-30 — Feasibility assessment, PD-1..8, ADRs 0001–0009, research checkpoints (landscape, lexicons, monetization, naming), product name Mooring @ mooring.page. (PR #1) diff --git a/docs/decisions/adr/0010-tech-stack.md b/docs/decisions/adr/0010-tech-stack.md new file mode 100644 index 0000000..f2323ec --- /dev/null +++ b/docs/decisions/adr/0010-tech-stack.md @@ -0,0 +1,31 @@ +# ADR 0010: Tech stack — TypeScript + SvelteKit core, direct PDS reads, BEAM on the bench + +**Status:** Accepted · 2026-07-30 (ratified by Jacob same day) + +## Context + +Nothing is scaffolded; this decision unblocks v1 (ADR 0008). Constraints already locked: AGPL-3.0-compatible dependencies (ADR 0002); renderer cleanly separable from the multi-tenant shell (ADR 0005); OAuth client with granular scopes against both bsky.social and self-hosted PDSes (ADR 0007); solo nights-and-weekends maintainability trumps architectural ambition (PD-7); the competitive window is months (FEASIBILITY). Jacob's candidate languages were TypeScript, Elixir, and Gleam; desired shape is SSR public sites + SPA admin. + +Research (all 2026-07-30, in `docs/research/`): `language-ecosystems` (per-language atproto library maturity), `appview-infra` (quickslice/HappyView/Tap/Microcosm), `comparable-stacks` (what 8 comparable atproto apps run on). The short version: + +- TypeScript is the only ecosystem where the whole atproto chain — OAuth client, granular-scope tooling, identity resolution, API client — is first-party, MIT, and actively released; it is also where lexicon tooling for our fastest-churning upstream sources (sifa, standard.site) updates first. 7 of 8 comparable apps are TypeScript-first, and the official Statusphere tutorial now teaches exactly our v1 architecture. +- Elixir has two young full atproto OAuth implementations (atex, latch — both 0.x, single-maintainer); choosing it means co-maintaining the auth layer. Phoenix/LiveView otherwise fits the product shape well. +- Gleam has everything in alpha, single-author form; highest friction for the product core. +- quickslice and HappyView are self-host-only whole-network Jetstream indexers with no hosted offering — operational surface v1's per-user reads don't need. Bluesky's Tap (first-party sync sidecar, webhook delivery) is now the strongest liveness candidate for v2. + +## Decision + +1. **Language: TypeScript/Node for the product core.** +2. **atproto layer: first-party `@atproto/*` packages** — `oauth-client-node` (OAuth w/ granular scopes + permission sets), `api`, `identity`, `oauth-scopes`, `lex`. Pin versions and read changelogs on upgrade — these are pre-1.0 and rename things. The lighter `@atcute` suite (incl. `standard-site` lexicons) is an approved alternative where it fits better. +3. **Framework: SvelteKit (Svelte 5)** serving both the SSR public sites and the admin app, in one repo with the renderer kept a separable package (ADR 0005). Chosen over Next.js for lighter self-hosting and a cheaper separable renderer; Blento proves the stack on our nearest product shape. +4. **v1 data layer: hand-rolled direct PDS reads** (`com.atproto.repo.listRecords`/`getRecord` against the logged-in user's PDS) behind a thin cache honoring deletions. **No AppView dependency** — quickslice and HappyView are declined for v1; re-evaluate alongside **Tap** (leading candidate) when v2 needs Jetstream liveness. +5. **Database: provisional SQLite-family default** for derived state (sessions, cache, domain mappings, billing) — final call lands with the hosted-infrastructure decision (NEXT item 5). Keep the data layer swappable (e.g. Kysely/Drizzle over raw driver calls). +6. **BEAM stays on the bench, deliberately:** Elixir (and Gleam for tooling) are named candidates for v2+ auxiliary services — e.g. a firehose/Jetstream consumer — where long-lived connection handling is the BEAM's home turf. Not for the v1 product core. + +## Consequences + +- Fastest path through the window: least protocol glue to write, most worked examples, upstream schema churn lands in our ecosystem first. All chosen dependencies are MIT/Apache-2.0 — AGPL-compatible. +- Pre-1.0 `@atproto/oauth-*` churn is accepted ongoing maintenance (pinning + changelog discipline), traded against co-maintaining an auth stack on the BEAM. +- SvelteKit's ecosystem is smaller than React's — acceptable solo, and its lighter runtime keeps static-export/self-host (deferred, ADR 0008) realistic. +- Scaffolding is now unblocked; per AGENTS.md, product code still starts only on Jacob's explicit request. +- Revisit triggers: `@atproto/oauth-client-node` hits 1.0 (unpin), v2 liveness work begins (Tap vs. quickslice/HappyView re-eval), or the typed indexer ships from the Spring 2026 roadmap. diff --git a/docs/research/2026-07-30-appview-infra.md b/docs/research/2026-07-30-appview-infra.md new file mode 100644 index 0000000..63fe777 --- /dev/null +++ b/docs/research/2026-07-30-appview-infra.md @@ -0,0 +1,44 @@ +# Research: AppView infrastructure — quickslice (Slices), HappyView, Tap, Microcosm + +**Date:** 2026-07-30. Input to ADR 0010. Question: should Mooring's backend depend on a lexicon-driven AppView service, and does v1 need one at all? + +## quickslice (formerly "Slices", slices.network) + +- slices.network now 301s to [quickslice.slices.network](https://quickslice.slices.network/) — "a quick way to spin up an AppView for AT Protocol applications." +- **Model:** import lexicons → Jetstream ingestion → indexes matching records into SQLite/Postgres → **auto-generated GraphQL API** (queries/mutations/subscriptions), joins by DID/at-uri/strongRef, aggregations, viewer state, notifications, moderation. Handles atproto OAuth and proxies writes to the user's PDS (typed JS client). +- **Self-host only. No hosted offering, no pricing.** Railway one-click (SQLite, recommended), Fly.io, Docker Compose. Min footprint 1 GB RAM / 1 core / 10 GB. ([deployment guide](https://quickslice.slices.network/guides/deployment)) +- **Apache-2.0** ([repo on tangled](https://tangled.org/slices.network/quickslice)) → AGPL-compatible. +- Maintainer: Chad Miller (@chadtmiller.com, also builds Grain Social — dogfooded). Occasional commits from trezy.codes (the HappyView author!). Written in **Gleam** (~91%). v0.20.2, 227 commits; docs carry an explicit "early development, APIs may change" warning; commit cadence appears slowed (~early 2026) though the author ran an ATmosphereConf workshop in March 2026. + +## HappyView (happyview.dev) + +- "A lexicon-driven AppView for ATProto": upload lexicons → generated **XRPC** query/procedure routes (protocol-native, not GraphQL), Jetstream real-time + per-user PDS historical backfill, atproto OAuth with DPoP-bound proxy writes, Lua triggers + sandboxed WASM plugins, labeler subscriptions, admin dashboard. ([repo](https://github.com/gamesgamesgamesgamesgames/happyview)) +- **v2 confirmed shipped** (blog, [Apr 24 2026](https://trezy.com/blog/releasing-happyview-2-into-the-wild)): one binary, embedded SQLite default (Postgres optional), internalized Tap + AIP auth server. +- **Self-host only. No hosted offering, no pricing.** Railway one-click / Docker Compose / cargo. +- **MIT** (LICENSE.md: "Copyright (c) 2024 Lexicon Community") → AGPL-compatible. +- **Extremely active, bus factor one:** every visible commit by Trezy (@trezy.codes). v2.11.8 stable / v2.12.0-dev.2 (Jul 25, 2026). Early-July release burst was **security hardening** (DPoP replay protection, constant-time comparisons, identity-spoofing prevention) — responsive, but the security surface was still shaking out this month. Rust + TS, ~80 stars, 946 commits. + +## Comparison (facts) + +| | quickslice | HappyView | +|---|---|---| +| API generated from lexicons | GraphQL | XRPC (protocol-native) | +| Ingest | Jetstream | Jetstream + per-user backfill | +| Hosted service / pricing | None | None | +| License | Apache-2.0 | MIT | +| Language | Gleam | Rust | +| Activity (Jul 2026) | Cadence slowed | Near-daily releases | +| Maintainers | ~1 (Chad Miller) | 1 (Trezy) | + +## First-party alternative: Tap (and the coming "typed indexer") + +- **[Tap](https://docs.bsky.app/blog/introducing-tap)** (Bluesky, Dec 2025): self-hosted, application-agnostic sync consumer — "a single Go binary backed by a relational database" (SQLite default) handling firehose connection, crypto verification, **backfill**, and collection filtering, delivering plain JSON events **via webhook** to your app. This is what the revamped official [Statusphere tutorial](https://atproto.com/guides/statusphere-tutorial) now teaches. +- The [Spring 2026 atproto roadmap](https://atproto.com/blog/2026-spring-roadmap) announces a planned **"typed indexer"** merging Tap with the rewritten `@atproto/lex` SDK — first-party, lexicon-driven indexing primitives as libraries, not a hosted service. + +## Microcosm (microcosm.blue) + +Hosted community primitives, donation-funded, run by @bad-example.com: **Constellation** (global backlink index), **Slingshot** (firehose-fed record cache / edge fetcher with fast delete-eviction — a faster substitute for raw per-PDS reads), **Spacedust** (interactions firehose). Production-quality but one person + donations — enrichment only, never a hard dependency (consistent with the FEASIBILITY stance). + +## So what (verdict fed into ADR 0010) + +Both quickslice and HappyView are **whole-network indexers whose core value is cross-repo aggregation and liveness — exactly what v1 doesn't need.** Mooring v1 reads one logged-in user's own records via `com.atproto.repo.listRecords` directly from their PDS; neither tool has a mode lighter than "operate the indexer." Both are also single-maintainer and self-host-only, so adopting one adds operational surface without removing much code. Decision: hand-roll the thin read-and-cache layer for v1; when v2 liveness matters, **Tap (first-party) is the leading candidate**, with quickslice/HappyView re-evaluable then. diff --git a/docs/research/2026-07-30-comparable-stacks.md b/docs/research/2026-07-30-comparable-stacks.md new file mode 100644 index 0000000..6da6e11 --- /dev/null +++ b/docs/research/2026-07-30-comparable-stacks.md @@ -0,0 +1,27 @@ +# Research: what comparable atproto web apps actually run on + +**Date:** 2026-07-30. Input to ADR 0010. Method: read the repos (package.json / README / deployment docs) plus project blogs. + +## Per-project findings + +- **Blento** ([github.com/flo-bit/blento](https://github.com/flo-bit/blento), MIT, 204★) — TypeScript, **Svelte 5 + SvelteKit 2**, Tailwind, pnpm/Turbo monorepo, deployed to **Cloudflare Workers**. **No database found** — reads the user's PDS directly at render time. atproto via the **@atcute** suite (incl. `@atcute/standard-site`); OAuth via `@atcute/oauth-browser-client` + `@atcute/oauth-node-client`. Custom domains: user CNAMEs to blento.app ([docs](https://github.com/flo-bit/blento/blob/main/docs/CustomDomain.md)); TLS internals undocumented. +- **Leaflet** ([github.com/hyperlink-academy/leaflet](https://github.com/hyperlink-academy/leaflet), MIT, ~2,400 commits, team of 3) — **Next.js 16 + React 19 (RSC)** on **Vercel**; **Supabase Postgres** + Drizzle, Redis, Replicache, Yjs/ProseMirror; full **@atproto/** suite incl. `@atproto/sync` firehose ingestor and `@atproto/oauth-client-node`. Custom domains via **Vercel domains API** (`@vercel/sdk`) — "dead simple" per [their stack post](https://lab.leaflet.pub/3lrvqjio6e22d). +- **wisp.place** ([monorepo](https://github.com/WaveringAna/wisp.place-monorepo), MIT) — Bun + TypeScript **Elysia** backend, React 19 frontend, Rust CLI; **Postgres** + Redis + S3-tier blob cache; Jetstream consumer. **Best public write-up of multi-tenant custom domains** ([docs](https://docs.wisp.place/deployment)): domain verified via `_wisp. TXT did:plc:...`, domain→DID mapping in Postgres, **Caddy on-demand TLS** gated by an allowlist endpoint, wildcard subdomains via reverse proxy. +- **Linkat** ([github.com/mkizka/linkat](https://github.com/mkizka/linkat), **AGPL-3.0** — same license as us, 120★) — **React Router v8 + Express 5**, Prisma + **Postgres**, Jetstream ingestion, `@atproto/oauth-client-node`, custom lexicon in user's PDS. Docker deploy. No custom domains. +- **Sifa** (app code **not public**; [workspace repo](https://github.com/singi-labs/sifa-workspace)) — Node 26 + TS strict, **Fastify 5** + **Next.js 16**, **Postgres 17** + Drizzle, Valkey, Jetstream, `@atproto/oauth-client-node`, Docker + Caddy. Custom domains as *handles* only; their static-site PoC (`sifa-page`) is minimal/stale. +- **Frontpage** ([github.com/frontpagefyi/frontpage](https://github.com/frontpagefyi/frontpage), MIT, 185★) — Next.js + **libSQL/Turso** + Drizzle on Vercel; **Rust firehose consumer ("drainpipe") → webhook** → app indexes. OAuth **hand-rolled** on `oauth4webapi` + `jose`. The oft-cited "appview-lite" pattern. +- **Smoke Signal** — **Rust** (Axum + MiniJinja SSR, SQLite/SQLx, Caddy), solo maintainer; extracted reusable `atproto-identity-rs` / `atproto-oauth` crates. **Live app sunset July 2026**; repo remains as read-only archive. Cautionary data point for solo + non-mainstream stack. +- **Statusphere (official example)** ([repo](https://github.com/bluesky-social/statusphere-example-app)) — revamped for the Tap era: **Next.js 16 + React 19**, better-sqlite3 + Kysely, `@atproto/oauth-client-node`, `@atproto/lex`, **`@atproto/tap`**. The [official tutorial](https://atproto.com/guides/statusphere-tutorial) now teaches: OAuth writes to PDS → Tap webhook → local SQLite cache → server-rendered reads. + +## Patterns observed + +- **Language:** 7 of 8 TypeScript-first; Rust three times in supporting roles (or Smoke Signal entirely — now sunset). No Go/Python among these apps. +- **Framework:** Next.js ×4 (Leaflet, Sifa, Frontpage, Statusphere); SvelteKit, React Router/Express, Elysia, Axum ×1 each. Public pages server-rendered everywhere. +- **DB:** Postgres ×4, SQLite-family ×3, none ×1 (Blento). Always a derived cache — canonical data in the PDS, matching ADR 0004. Drizzle is the most common ORM. +- **Reads:** firehose/Jetstream indexing dominates multi-user apps; **direct PDS reads at request time** proven viable by Blento (no DB, on Workers). Official direction is **Tap**. Nobody uses Constellation for core reads. +- **OAuth:** `@atproto/oauth-client-node` ×4; @atcute ×1; hand-rolled ×1; Rust crate ×1. +- **Custom domains, three documented approaches:** wisp.place (Caddy on-demand TLS + TXT verification — most transferable to self-managed infra), Leaflet (Vercel domains API), Blento (CNAME to apex on Cloudflare). Directly relevant to NEXT item on hosted infra. + +## So what + +The ecosystem's center of gravity — including the official tutorial — is TypeScript, a meta-framework with SSR, a small SQL cache, `@atproto/oauth-client-node`, and (newly) Tap for liveness. Blento demonstrates the v1-sufficient minimal pattern (direct PDS reads, no indexer) on the exact product shape closest to ours; SvelteKit is its stack. Fed into ADR 0010; custom-domain patterns feed the hosted-infra decision (NEXT item 5). diff --git a/docs/research/2026-07-30-language-ecosystems.md b/docs/research/2026-07-30-language-ecosystems.md new file mode 100644 index 0000000..8e183bd --- /dev/null +++ b/docs/research/2026-07-30-language-ecosystems.md @@ -0,0 +1,64 @@ +# Research: atproto library maturity by language (TypeScript · Elixir · Gleam · others) + +**Date:** 2026-07-30. Input to ADR 0010 (tech stack). The atproto client ecosystem churns fast — re-verify before leaning on this after a few months. + +## State of the OAuth/permissions spec (affects every language) + +- **Granular scopes are live.** Five permission types: `repo:` (record CRUD by collection, attenuable with `?action=create&action=update`), `rpc:`, `blob:`, `account:*`, `identity:*`. On bsky.social since ~Aug 2025; in the self-hosted PDS distribution around v0.4.208 (~Apr 2026). Very stale self-hosted PDSes may only understand transitional scopes. Sources: [scopes guide](https://atproto.com/guides/scopes), [discussion #4118](https://github.com/bluesky-social/atproto/discussions/4118). +- **Permission sets (`include:`) live but recent** — announced Dec 10, 2025; defined as Lexicon schemas with `"type": "permission-set"`; maintainers still asking for "patience." Sources: [permission-sets guide](https://atproto.com/guides/permission-sets), [discussion #4437](https://github.com/bluesky-social/atproto/discussions/4437). +- **Key architectural fact:** granular scopes are enforced **server-side by the PDS**; a client library only passes scope strings through PAR/token requests. So "granular scope support" barely differentiates libraries — what differentiates them is DPoP/PAR/PKCE correctness, identity resolution, and session management. +- Self-hosted vs bsky.social makes no library-level difference: handle → DID → PDS → auth server via metadata discovery, identically. +- First-party OAuth clients exist for **TypeScript and Go only**. Community SDK index: [sdk.blue](https://sdk.blue/). + +## TypeScript / Node — the reference implementation + +| Library | Status (2026-07-30) | +|---|---| +| [`@atproto/api`](https://github.com/bluesky-social/atproto) | v0.20.35, published **today**, MIT. Reference implementation. | +| `@atproto/oauth-client-node` | v0.5.0, published today, MIT. DPoP+PKCE+PAR, pluggable session/state stores, confidential+public clients. **Pre-1.0: breaking rename in this week's 0.5.0.** Five releases in July 2026 alone. | +| [`@atproto/oauth-scopes`](https://www.npmjs.com/package/@atproto/oauth-scopes) | v0.5.7, MIT. First-party granular-scope parsing/validation — only ecosystem with tooling at this level. | +| `@atproto/identity` | First-party DID/handle resolution. | + +Community alternative: **@atcute** suite (lighter; used by Blento; includes `@atcute/standard-site` lexicons and browser/node OAuth clients). + +**Verdict: production-ready, first-party end to end; only caveat is pre-1.0 semver churn — pin and read changelogs.** + +## Go — first-party #2 + +- [`indigo`](https://github.com/bluesky-social/indigo) `atproto/auth/oauth`: first-party full client (PAR, DPoP, PKCE, refresh, multi-session, `ClientAuthStore` interface — you write the DB-backed store). Treats scopes as strings; its own example passes `repo:app.bsky.feed.post?action=create`. Dual MIT/Apache-2.0. Official tutorial: [OAuth for CLI Apps (Go)](https://atproto.com/guides/go-oauth-cli-tutorial). No tagged semver releases is the main wart. +- Community `haileyok/atproto-oauth-golang`: self-described experimental — superseded by indigo. + +## Elixir — OAuth exists twice, both young + +- **[`atex`](https://hex.pm/packages/atex)** — atproto toolkit by comet.sh (dogfooded by their Comet music app). v0.10.0, ~June 2026, MIT, 35 stars. Identity resolution with caching, lexicon → struct codegen, XRPC client **and server**, PLC client. **Full OAuth**: `Atex.OAuth.Flow` does PAR, PKCE, DPoP, refresh, revocation, client assertions, `:scopes` passthrough; Plug session helpers with multi-session. Pre-built lexicon packages incl. **`atex_standard_site`** — directly relevant to Mooring. ([Atex.OAuth.Flow docs](https://atex.hexdocs.pm/Atex.OAuth.Flow.html)) +- **[`latch`](https://hex.pm/packages/latch)** — purpose-built atproto OAuth client by Johanna Larsson ([jola.dev](https://jola.dev)). v0.2.0 published **2026-07-26** (days old), ~108 downloads, MIT. Full flow incl. DPoP nonce caching, transparent token refresh, three client modes, pluggable store, supervision-tree native. +- Hand-rolling is proven feasible solo: [jola.dev writeup](https://jola.dev/posts/automatically-syncing-your-blog-atproto-standard-site) (June 2026) — "there's no battle-hardened atproto OAuth library for Elixir yet"; she hand-rolled for Phoenix app annot.at, then extracted latch. Primitives all exist (erlang-jose ES256, Req, ETS). Known tricky bit: the DPoP nonce dance ([issue #3078](https://github.com/bluesky-social/atproto/issues/3078)). +- Adjacent: **Drinkup** (Jetstream + Tap consumer, by the atex author), hexpds (PDS in Elixir, stale), Potable (repo primitives), [bitcrowd's atproto dataplane PoC](https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto/) (Mar 2026 — BEAM case: sub-20ms p95 @ 200+ rps from ETS). +- Stale/not candidates: ProtoRune (dormant since Jan 2025), moomerman/atproto, bluesky_ex. + +**Verdict: nothing needed is missing outright, but both OAuth libs are 0.x single-maintainer — choosing Elixir means effectively co-maintaining the auth stack. Phoenix/LiveView is otherwise an excellent fit for the product shape.** + +## Gleam — lively but uniformly alpha + +- **gleam-atproto suite** (mokkenstorm.dev, 8 Hex packages, MIT, updated within a week): `atproto_client` 0.3.0 — XRPC, identity, repo CRUD, **confidential-client OAuth with PAR/PKCE/DPoP**; `atproto_browser` (WebCrypto DPoP public client); lexicon AST + codegen. Explicitly "Alpha, pre-release," single author, ~10 stars. +- **possum** v3.0.0 — XRPC client, public reads, handle resolution; **app passwords only, no OAuth**. Active, 42 stars. +- Others: alicia (identity/lexicons), goose (Jetstream consumer, Dec 2025), and notably **quickslice is written in Gleam** (~91%). +- Web stack: Wisp 2.2.2 / Mist 6.0.3 (both Apache-2.0, maintained by Gleam's creator, production-usable); Lustre v5.7 (SPA + SSR + server components, mostly one maintainer). SSR+hydration is a hand-assembled three-project pattern, not batteries-included. +- Primitives: `gose` 2.1.0 (JOSE with ES256 — DPoP prerequisite covered, Erlang/Node targets), `pog` for Postgres (no Ecto-depth tooling). +- FFI: Gleam can call Erlang/Elixir hex packages, but bindings are hand-written and untyped, and **macros don't cross the boundary** — Phoenix, Ecto schemas, and atex's codegen are effectively unusable from Gleam. + +**Verdict: everything needed exists in some alpha form; highest-friction option for the product core against a short window. Low-regret entry point would be side tooling.** + +## Python / Rust / Ruby (brief) + +- **Python:** MarshalX `atproto` v0.0.69 (active, MIT) still has **no OAuth** — password auth only. `atproto-oauth-py` is v0.1.0 (Apr 2026), unproven. Bluesky maintains a from-scratch Flask DPoP reference in [cookbook](https://github.com/bluesky-social/cookbook/blob/main/python-oauth-web-app/atproto_oauth.py). Weak link for an auth core. +- **Rust:** `atrium`/`atrium-oauth` (MIT, active, pre-1.0) and the smokesignal.events `atproto-oauth` crate family (v0.14.x, axum integration). Usable, community-maintained, no first-party backing. +- **Ruby:** `atproto_auth` gem (DPoP/PAR, v0.2.5) + `omniauth-atproto` — exists, low traction. + +## Licensing + +Everything relevant is AGPL-3.0-compatible: MIT (@atproto/*, atex, latch, atrium, MarshalX, Ruby gems), MIT/Apache-2.0 (indigo), Apache-2.0 (Wisp/Mist). No conflicts found. + +## So what + +TypeScript is the only ecosystem where OAuth client, granular-scope tooling, identity, and API client are all first-party and current — and it's where the lexicon tooling for our churning upstream sources (sifa, standard.site) updates first. Go is a legitimate #2. Elixir is defensible-with-eyes-open (co-maintain auth); Gleam is charming but the riskiest core choice. Fed into ADR 0010. diff --git a/docs/research/README.md b/docs/research/README.md index 4695e92..dd4468b 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -8,5 +8,8 @@ Findings from research sessions, preserved so future sessions (human or agent) d | [`2026-07-30-lexicon-feasibility.md`](2026-07-30-lexicon-feasibility.md) | 2026-07-30 | Technical feasibility per data source; OAuth/scopes state | | [`2026-07-30-monetization.md`](2026-07-30-monetization.md) | 2026-07-30 | Monetization precedents in and out of the ecosystem; pricing comps | | [`2026-07-30-naming.md`](2026-07-30-naming.md) | 2026-07-30 | Product name candidates and collision/domain checks | +| [`2026-07-30-language-ecosystems.md`](2026-07-30-language-ecosystems.md) | 2026-07-30 | atproto library maturity by language (TS/Elixir/Gleam/…); OAuth client state | +| [`2026-07-30-appview-infra.md`](2026-07-30-appview-infra.md) | 2026-07-30 | quickslice vs HappyView vs Tap vs Microcosm; do we need an AppView? | +| [`2026-07-30-comparable-stacks.md`](2026-07-30-comparable-stacks.md) | 2026-07-30 | What 8 comparable atproto apps run on; custom-domain serving patterns | Conventions: one dated file per topic, source URLs inline, and an honest "so what" at the end of each. When you re-research a topic, add a new dated file rather than editing the old one, and update this index.