about things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add appviews notes: XRPC, frameworks, backfill, link previews

learnings from migrating status app from quickslice to hatk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+120
+1
protocols/atproto/README.md
··· 67 67 - [firehose](./firehose.md) - event streaming, jetstream 68 68 - [auth](./auth.md) - OAuth, scopes, permission sets 69 69 - [labels](./labels.md) - moderation, signed assertions 70 + - [appviews](./appviews.md) - building appviews, XRPC, frameworks (quickslice, hatk), backfill, link previews 70 71 - [sync-verification](./sync-verification.md) - inductive proof chains, MST inversion, sync 1.1 71 72 72 73 ## sources
+119
protocols/atproto/appviews.md
··· 1 + # appviews 2 + 3 + an appview indexes AT Protocol data into a queryable database and serves it to clients. it's the "app" layer — what users interact with. bluesky's main appview indexes posts, likes, follows into search and timelines. but anyone can build one for any lexicon. 4 + 5 + ## the job 6 + 7 + 1. subscribe to the firehose (via relay) 8 + 2. filter for records matching your lexicons 9 + 3. index them into a local database (usually sqlite or postgres) 10 + 4. serve queries to clients over XRPC 11 + 5. proxy writes back to the user's PDS 12 + 13 + this is a lot of plumbing. the interesting part is your lexicons and your UI — the rest is boilerplate. frameworks exist to handle it. 14 + 15 + ## XRPC 16 + 17 + AT Protocol's RPC layer. every operation is a **namespaced ID** (NSID) like `dev.hatk.getFeed` or `com.atproto.repo.createRecord`. two types: 18 + 19 + - **queries** (GET): read data. params go in query string. 20 + - **procedures** (POST): write data. input goes in request body as JSON. 21 + 22 + defined by lexicons — the schema says what params/input/output look like. frameworks can generate typed client and server code from these definitions. 23 + 24 + ``` 25 + GET /xrpc/dev.hatk.getFeed?feed=recent&limit=50 26 + POST /xrpc/dev.hatk.createRecord { collection, record } 27 + ``` 28 + 29 + compared to graphql (which quickslice used): XRPC is simpler, more aligned with how atproto actually works. graphql gives you flexible queries but adds a translation layer between the client and the protocol. XRPC operations map directly to protocol concepts. 30 + 31 + ## framework options 32 + 33 + ### quickslice 34 + 35 + "appview in a bottle." pre-built docker image, zero code generation. you define lexicons, point it at a relay, and it indexes records into sqlite. exposes a **graphql** API. 36 + 37 + - good for: getting started fast, simple CRUD appviews 38 + - data access: graphql queries, inline in your frontend JS 39 + - frontend: bring your own (vanilla JS, React, whatever) 40 + - deploy: run the image, point your frontend at it 41 + 42 + the frontend/backend split means two deployments. link previews for social sharing need a separate edge function (cloudflare pages function, vercel edge, etc.) since the SPA can't do SSR. 43 + 44 + ### hatk 45 + 46 + full-stack AT Protocol framework by the same author (chad). replaces quickslice with a much richer approach: **SvelteKit frontend + typed XRPC backend**, auto-generated from lexicons. 47 + 48 + - runs `vp build` (vite-plus) which reads your lexicons and generates: 49 + - `hatk.generated.ts` — server-side types, XRPC handlers, db schema 50 + - `hatk.generated.client.ts` — client-side `callXrpc()` with full type safety 51 + - **feeds** are first-class: `defineFeed("recent", ...)` with pagination, hydration 52 + - **hooks**: `defineHook("on-login")` for running code when a user first authenticates 53 + - **OAuth** built in: AT Protocol native, session cookies, `parseViewer(cookies)` 54 + - **SSR**: SvelteKit means link previews work without edge functions 55 + 56 + ```ts 57 + // server/feeds/recent.ts 58 + export default defineFeed("recent", async ({ limit, cursor, db }) => { 59 + const rows = db.prepare(` 60 + SELECT * FROM io_zzstoatzz_status_record 61 + WHERE repo_did NOT IN (SELECT did FROM _takendown_repos) 62 + ORDER BY created_at DESC LIMIT ? 63 + `).all(limit); 64 + return { items: hydrate(rows, db) }; 65 + }); 66 + ``` 67 + 68 + ```ts 69 + // client — fully typed from lexicons 70 + const res = await callXrpc("dev.hatk.getFeed", { feed: "recent", limit: 50 }); 71 + // res.items is StatusView[] 72 + ``` 73 + 74 + reference implementation: [grain](https://grain.social) (a social grain/mood tracker). 75 + 76 + ## backfill 77 + 78 + when an appview starts (or restarts), it needs to catch up on records it missed. hatk handles this automatically — it downloads repo CAR files from each user's PDS and indexes the records. 79 + 80 + ### operational reality 81 + 82 + backfill is the most resource-intensive thing an appview does. each repo download means fetching a CAR file (binary, contains the full merkle tree) from a potentially slow or unreachable PDS. on a small VM: 83 + 84 + - **parallelism matters**: 5 concurrent backfills on a 1GB VM will OOM. 2 is safer. the CAR parsing + record extraction holds a lot in memory. 85 + - **heap limits help**: `--max-old-space-size=768` on node forces more aggressive GC before the OS kills you. 86 + - **some PDSes are slow**: if a user's PDS is down or throttling, backfill stalls on that connection. parallelism means one slow PDS doesn't block everything. 87 + - **no data migration needed**: this is the beauty of AT Protocol — repos are the source of truth. blow away your database, restart, and backfill rebuilds it from the network. your indexed data is a cache, not canonical. 88 + 89 + ## link previews 90 + 91 + social bots (Discord, Slack, Twitter, Bluesky) fetch URLs and look for OG meta tags. SPAs can't serve these — the bot sees empty HTML. two approaches: 92 + 93 + **edge function** (quickslice approach): intercept bot user agents at the CDN, fetch the record, return HTML with OG tags. everything else passes through to the SPA. 94 + 95 + **SSR** (hatk approach): the page renders server-side with the data already loaded. OG tags are just part of the HTML. no special bot detection needed — every request gets the full page. 96 + 97 + ```svelte 98 + <!-- status permalink — OG tags rendered during SSR --> 99 + <svelte:head> 100 + {#if status} 101 + <meta property="og:title" content="{ogTitle}" /> 102 + <meta property="og:description" content="{ogDescription}" /> 103 + {#if ogImage} 104 + <meta property="og:image" content="{ogImage}" /> 105 + <meta name="twitter:card" content="summary_large_image" /> 106 + {/if} 107 + {/if} 108 + </svelte:head> 109 + ``` 110 + 111 + for custom emoji (like bufo images), the image URL goes straight into `og:image`. unicode emoji don't have a natural image representation — those get `summary` card type instead. 112 + 113 + ## sources 114 + 115 + - [hatk](https://github.com/bigmoves/hatk) — full-stack AT Protocol framework 116 + - [quickslice](https://github.com/bigmoves/quickslice) — appview in a bottle 117 + - [grain](https://grain.social) — hatk reference implementation 118 + - [status](https://status.zzstoatzz.io) — status app (migrated from quickslice to hatk, march 2026) 119 + - [XRPC spec](https://atproto.com/specs/xrpc) — AT Protocol RPC