···11+# appviews
22+33+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.
44+55+## the job
66+77+1. subscribe to the firehose (via relay)
88+2. filter for records matching your lexicons
99+3. index them into a local database (usually sqlite or postgres)
1010+4. serve queries to clients over XRPC
1111+5. proxy writes back to the user's PDS
1212+1313+this is a lot of plumbing. the interesting part is your lexicons and your UI — the rest is boilerplate. frameworks exist to handle it.
1414+1515+## XRPC
1616+1717+AT Protocol's RPC layer. every operation is a **namespaced ID** (NSID) like `dev.hatk.getFeed` or `com.atproto.repo.createRecord`. two types:
1818+1919+- **queries** (GET): read data. params go in query string.
2020+- **procedures** (POST): write data. input goes in request body as JSON.
2121+2222+defined by lexicons — the schema says what params/input/output look like. frameworks can generate typed client and server code from these definitions.
2323+2424+```
2525+GET /xrpc/dev.hatk.getFeed?feed=recent&limit=50
2626+POST /xrpc/dev.hatk.createRecord { collection, record }
2727+```
2828+2929+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.
3030+3131+## framework options
3232+3333+### quickslice
3434+3535+"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.
3636+3737+- good for: getting started fast, simple CRUD appviews
3838+- data access: graphql queries, inline in your frontend JS
3939+- frontend: bring your own (vanilla JS, React, whatever)
4040+- deploy: run the image, point your frontend at it
4141+4242+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.
4343+4444+### hatk
4545+4646+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.
4747+4848+- runs `vp build` (vite-plus) which reads your lexicons and generates:
4949+ - `hatk.generated.ts` — server-side types, XRPC handlers, db schema
5050+ - `hatk.generated.client.ts` — client-side `callXrpc()` with full type safety
5151+- **feeds** are first-class: `defineFeed("recent", ...)` with pagination, hydration
5252+- **hooks**: `defineHook("on-login")` for running code when a user first authenticates
5353+- **OAuth** built in: AT Protocol native, session cookies, `parseViewer(cookies)`
5454+- **SSR**: SvelteKit means link previews work without edge functions
5555+5656+```ts
5757+// server/feeds/recent.ts
5858+export default defineFeed("recent", async ({ limit, cursor, db }) => {
5959+ const rows = db.prepare(`
6060+ SELECT * FROM io_zzstoatzz_status_record
6161+ WHERE repo_did NOT IN (SELECT did FROM _takendown_repos)
6262+ ORDER BY created_at DESC LIMIT ?
6363+ `).all(limit);
6464+ return { items: hydrate(rows, db) };
6565+});
6666+```
6767+6868+```ts
6969+// client — fully typed from lexicons
7070+const res = await callXrpc("dev.hatk.getFeed", { feed: "recent", limit: 50 });
7171+// res.items is StatusView[]
7272+```
7373+7474+reference implementation: [grain](https://grain.social) (a social grain/mood tracker).
7575+7676+## backfill
7777+7878+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.
7979+8080+### operational reality
8181+8282+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:
8383+8484+- **parallelism matters**: 5 concurrent backfills on a 1GB VM will OOM. 2 is safer. the CAR parsing + record extraction holds a lot in memory.
8585+- **heap limits help**: `--max-old-space-size=768` on node forces more aggressive GC before the OS kills you.
8686+- **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.
8787+- **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.
8888+8989+## link previews
9090+9191+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:
9292+9393+**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.
9494+9595+**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.
9696+9797+```svelte
9898+<!-- status permalink — OG tags rendered during SSR -->
9999+<svelte:head>
100100+ {#if status}
101101+ <meta property="og:title" content="{ogTitle}" />
102102+ <meta property="og:description" content="{ogDescription}" />
103103+ {#if ogImage}
104104+ <meta property="og:image" content="{ogImage}" />
105105+ <meta name="twitter:card" content="summary_large_image" />
106106+ {/if}
107107+ {/if}
108108+</svelte:head>
109109+```
110110+111111+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.
112112+113113+## sources
114114+115115+- [hatk](https://github.com/bigmoves/hatk) — full-stack AT Protocol framework
116116+- [quickslice](https://github.com/bigmoves/quickslice) — appview in a bottle
117117+- [grain](https://grain.social) — hatk reference implementation
118118+- [status](https://status.zzstoatzz.io) — status app (migrated from quickslice to hatk, march 2026)
119119+- [XRPC spec](https://atproto.com/specs/xrpc) — AT Protocol RPC