···181181});
182182```
183183184184-`ctx.viewer` is the same `{ did: string }` shape in both XRPC handlers and feed `generate`/`hydrate` functions.
184184+`ctx.viewer` is the same `{ did: string; handle?: string }` shape in both XRPC handlers and feed `generate`/`hydrate` functions.
185185186186## Complete example
187187
+16-13
docs/site/guides/feeds.md
···5353| `params` | `Record<string, string>` | Query string parameters from the request |
5454| `limit` | number | Requested page size |
5555| `cursor` | string \| undefined | Pagination cursor from the client |
5656-| `viewer` | `{ did: string }` \| null | The authenticated user, or null |
5656+| `viewer` | `{ did: string; handle?: string }` \| null | The authenticated user, or null |
5757| `ok` | function | Wraps your return value with type checking |
5858| `paginate` | function | Run a paginated query (handles cursor, ORDER BY, LIMIT) |
5959| `packCursor` | function | Encode a `(primary, cid)` pair into an opaque cursor string |
···135135136136The optional `hydrate` function enriches feed results with additional data. After `generate` returns URIs, the framework resolves them into full records, then passes those records to `hydrate`.
137137138138-### Hydrate context reference
138138+### Hydrate function signature
139139+140140+`hydrate` receives a `BaseContext` and an array of `Row<T>` items (the resolved records). Each row has `uri`, `did`, `handle`, and `value`.
141141+142142+### BaseContext reference
139143140144| Field | Type | Description |
141145| ------------ | ------------------------- | --------------------------------------------------------------- |
142142-| `items` | `Row[]` | The resolved records (each has `uri`, `did`, `handle`, `value`) |
143143-| `viewer` | `{ did: string }` \| null | The authenticated user, or null |
146146+| `viewer` | `{ did: string; handle?: string }` \| null | The authenticated user, or null |
144147| `db.query` | function | Run SQL queries against your SQLite database |
145148| `getRecords` | function | Fetch records by URI from another collection |
146149| `lookup` | function | Look up records by a field value (e.g. profiles by DID) |
···153156This feed queries status records and hydrates each one with the author's profile:
154157155158```typescript
156156-import { defineFeed, views, type Status, type Profile, type HydrateContext } from "$hatk";
159159+import { defineFeed, views, type Status, type Profile, type BaseContext, type Row } from "$hatk";
157160158161export default defineFeed({
159162 collection: "xyz.statusphere.status",
160163 label: "Recent",
161164162162- hydrate: (ctx) => hydrateStatuses(ctx),
165165+ hydrate: (ctx, items) => hydrateStatuses(ctx, items as Row<Status>[]),
163166164167 async generate(ctx) {
165168 const { rows, cursor } = await ctx.paginate<{ uri: string }>(
···171174 },
172175});
173176174174-async function hydrateStatuses(ctx: HydrateContext<Status>) {
175175- const dids = [...new Set(ctx.items.map((item) => item.did).filter(Boolean))];
177177+async function hydrateStatuses(ctx: BaseContext, items: Row<Status>[]) {
178178+ const dids = [...new Set(items.map((item) => item.did).filter(Boolean))];
176179 const profiles = await ctx.lookup<Profile>("app.bsky.actor.profile", "did", dids);
177180178178- return ctx.items.map((item) => {
181181+ return items.map((item) => {
179182 const author = profiles.get(item.did);
180183 return views.statusView({
181184 uri: item.uri,
···204207Hydration can also use `ctx.viewer` to add viewer-specific data like bookmarks:
205208206209```typescript
207207-async function hydratePlays(ctx: HydrateContext<Play>) {
208208- const dids = [...new Set(ctx.items.map((item) => item.did).filter(Boolean))];
210210+async function hydratePlays(ctx: BaseContext, items: Row<Play>[]) {
211211+ const dids = [...new Set(items.map((item) => item.did).filter(Boolean))];
209212 const profiles = await ctx.lookup<Profile>("app.bsky.actor.profile", "did", dids);
210213211214 // Load viewer's bookmarks
212215 const bookmarks = new Map<string, string>();
213213- if (ctx.viewer?.did && ctx.items.length > 0) {
216216+ if (ctx.viewer?.did && items.length > 0) {
214217 const rows = await ctx.db.query(
215218 `SELECT subject, uri FROM "community.lexicon.bookmarks.bookmark" WHERE did = $1`,
216219 [ctx.viewer.did],
···220223 }
221224 }
222225223223- return ctx.items.map((item) => {
226226+ return items.map((item) => {
224227 const author = profiles.get(item.did);
225228 return views.playView({
226229 record: { uri: item.uri, did: item.did, handle: item.handle, ...item.value },
+3-2
docs/site/guides/xrpc-handlers.md
···8787| `input` | object | Request body (procedures only), typed from the lexicon's input schema |
8888| `db.query` | function | Run SQL queries against your SQLite database |
8989| `db.run` | function | Execute SQL statements (INSERT, UPDATE, DELETE) |
9090-| `viewer` | `{ did: string }` \| null | The authenticated user, or null |
9090+| `viewer` | `{ did: string; handle?: string }` \| null | The authenticated user, or null |
9191| `limit` | number | Requested page size |
9292| `cursor` | string \| undefined | Pagination cursor |
9393| `resolve` | function | Resolve AT URIs into full records |
9494+| `getRecords` | function | Fetch records by URI from another collection |
9495| `lookup` | function | Look up records by a field value |
9596| `count` | function | Count records by field value |
9697| `exists` | function | Check if a record exists matching field filters |
···142143143144### `ctx.viewer`
144145145145-`viewer` is `{ did: string }` when the request comes from an authenticated user, or `null` for unauthenticated requests. Check it to protect endpoints that require authentication:
146146+`viewer` is `{ did: string; handle?: string }` when the request comes from an authenticated user, or `null` for unauthenticated requests. Check it to protect endpoints that require authentication:
146147147148```typescript
148149if (!viewer) throw new Error("Authentication required");