AppView in a box as a Vite plugin thing hatk.dev

docs: update site docs and remove hatk schema command

- Rename hero to hat://k, update tagline
- Remove hatk schema CLI command and docs
- Add createReport to API reference
- Update hooks guide to use ctx. style and hooks/ directory
- Simplify mutations guide to callXrpc only
- Remove experimental SvelteKit features section

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

+77 -161
+1
docs/site/api/index.md
··· 67 67 | [`getPreferences`](/api/preferences) | Query | Yes | Get user preferences | 68 68 | [`putPreference`](/api/preferences) | Procedure | Yes | Set a preference | 69 69 | [`describeLabels`](/api/labels) | Query | No | List label definitions | 70 + | [`createReport`](/api/labels) | Procedure | Yes | Report an account or record | 70 71 | `describeCollections` | Query | No | List indexed collections |
+36
docs/site/api/labels.md
··· 32 32 } 33 33 ``` 34 34 35 + ## `dev.hatk.createReport` 36 + 37 + Report an account or record for moderation review. Reports appear in the admin interface for review. 38 + 39 + - **Type:** Procedure (POST) 40 + - **Auth:** Required (OAuth) 41 + 42 + ### Input 43 + 44 + | Field | Type | Required | Description | 45 + | --------- | ------ | -------- | ------------------------------------------ | 46 + | `subject` | union | Yes | `{ did }` for accounts, `{ uri, cid }` for records | 47 + | `label` | string | Yes | Label identifier (must match a defined label) | 48 + | `reason` | string | No | Free-text explanation (max 2000 chars) | 49 + 50 + ### Example 51 + 52 + ```bash 53 + curl -X POST "http://127.0.0.1:3000/xrpc/dev.hatk.createReport" \ 54 + -H "Authorization: DPoP <token>" \ 55 + -H "Content-Type: application/json" \ 56 + -d '{"subject": {"uri": "at://did:plc:abc/app.bsky.feed.post/123", "cid": "bafyrei..."}, "label": "spam"}' 57 + ``` 58 + 59 + ### Response 60 + 61 + ```json 62 + { 63 + "id": 1, 64 + "subject": { "uri": "at://did:plc:abc/app.bsky.feed.post/123", "cid": "bafyrei..." }, 65 + "label": "spam", 66 + "reportedBy": "did:plc:reporter", 67 + "createdAt": "2026-03-24T12:00:00.000Z" 68 + } 69 + ``` 70 + 35 71 --- 36 72 37 73 See [Labels guide](/guides/labels) for how to define labels and hydrate them in responses.
-9
docs/site/cli/development.md
··· 49 49 50 50 This removes the SQLite database file and resets the local PDS container, giving you a fresh start. 51 51 52 - ## `hatk schema` 53 - 54 - Print the SQLite table schema derived from your lexicon definitions. 55 - 56 - ```bash 57 - hatk schema 58 - ``` 59 - 60 - Shows the SQL `CREATE TABLE` statements that would be generated from your record lexicons. Useful for understanding how lexicon fields map to database columns.
-1
docs/site/cli/index.md
··· 34 34 | `hatk start` | Start the server (production mode) | 35 35 | `hatk seed` | Run seed data against local PDS | 36 36 | `hatk reset` | Wipe database and PDS | 37 - | `hatk schema` | Print SQLite schema from lexicons | 38 37 39 38 ## Code Quality 40 39
+28 -92
docs/site/frontend/mutations.md
··· 1 1 --- 2 2 title: Mutations 3 - description: Create, update, and delete records from your frontend using remote functions and callXrpc. 3 + description: Create, update, and delete records from your frontend using callXrpc. 4 4 --- 5 5 6 6 # Mutations 7 7 8 - hatk provides two patterns for mutations from the frontend: **remote functions** for server-side logic callable from components, and **`callXrpc`** for direct API calls. Remote functions are the recommended approach -- they run on the server but are imported and called like normal functions. 9 - 10 - ## Remote functions 11 - 12 - Remote functions use SvelteKit's experimental remote functions feature. You define server-side functions in `.remote.ts` files, then import and call them from your components as if they were local. 13 - 14 - ### Defining remote functions 15 - 16 - Create a `.remote.ts` file in your routes directory: 8 + Mutations use `callXrpc` to call hatk's built-in record management endpoints. `callXrpc` is isomorphic, it works in server load functions, Svelte components, and anywhere else you have access to it. 17 9 18 10 ```typescript 19 - // app/routes/status.remote.ts 20 - import { command } from "$app/server"; 21 - import { callXrpc, getViewer } from "$hatk/client"; 22 - 23 - export const createStatus = command("unchecked", async (emoji: string) => { 24 - const viewer = await getViewer(); 25 - if (!viewer) throw new Error("Not authenticated"); 26 - return callXrpc("dev.hatk.createRecord", { 27 - collection: "xyz.statusphere.status" as const, 28 - repo: viewer.did, 29 - record: { status: emoji, createdAt: new Date().toISOString() }, 30 - }); 31 - }); 32 - 33 - export const deleteStatus = command("unchecked", async (rkey: string) => { 34 - const viewer = await getViewer(); 35 - if (!viewer) throw new Error("Not authenticated"); 36 - return callXrpc("dev.hatk.deleteRecord", { 37 - collection: "xyz.statusphere.status" as const, 38 - rkey, 39 - }); 40 - }); 41 - ``` 42 - 43 - Key points: 44 - - `command` comes from SvelteKit's `$app/server` -- it marks a function as a server-only remote function 45 - - `"unchecked"` is the validation mode (SvelteKit experimental API) 46 - - `getViewer()` reads the current user from the session 47 - - `callXrpc("dev.hatk.createRecord", ...)` and `callXrpc("dev.hatk.deleteRecord", ...)` are typed calls to hatk's built-in record management endpoints 48 - 49 - ### Calling remote functions from components 50 - 51 - Import remote functions directly in your Svelte components: 52 - 53 - ```svelte 54 - <script lang="ts"> 55 - import { 56 - createStatus as serverCreateStatus, 57 - deleteStatus as serverDeleteStatus, 58 - } from './status.remote' 59 - 60 - async function handleCreate(emoji: string) { 61 - const res = await serverCreateStatus(emoji) 62 - // res.uri contains the AT URI of the created record 11 + // Server-side: in a +layout.server.ts or +page.server.ts 12 + export const load = async () => { 13 + return { 14 + feed: callXrpc("dev.hatk.getFeed", { feed: "recent", limit: 50 }), 63 15 } 16 + } 64 17 65 - async function handleDelete(uri: string) { 66 - const rkey = uri.split('/').pop()! 67 - await serverDeleteStatus(rkey) 68 - } 69 - </script> 18 + // Client-side: in a Svelte component or query helper 19 + const res = await callXrpc("dev.hatk.createRecord", { 20 + collection: "xyz.statusphere.status" as const, 21 + repo: viewer.did, 22 + record: { status: "🚀", createdAt: new Date().toISOString() }, 23 + }) 70 24 ``` 71 25 72 - Even though these functions run on the server, you call them like any async function. SvelteKit handles the serialization and transport automatically. 73 - 74 - ### Enabling remote functions 75 - 76 - Remote functions require two settings in `svelte.config.js`: 26 + Pass SvelteKit's `fetch` as the optional third argument in load functions for request deduplication: 77 27 78 - ```javascript 79 - // svelte.config.js 80 - export default { 81 - compilerOptions: { 82 - experimental: { 83 - async: true, 84 - }, 85 - }, 86 - kit: { 87 - experimental: { 88 - remoteFunctions: true, 89 - }, 90 - }, 91 - }; 28 + ```typescript 29 + callXrpc("dev.hatk.getFeed", { feed: "recent" }, fetch) 92 30 ``` 93 31 94 - ## Record mutations with `callXrpc` 32 + ## Record mutations 95 33 96 34 hatk generates three built-in procedures for managing records: 97 35 ··· 142 80 143 81 ## Optimistic UI 144 82 145 - For a responsive feel, update the UI before the server responds and roll back on failure. Here's the pattern from the statusphere template: 83 + For a responsive feel, update the UI before the server responds and roll back on failure: 146 84 147 85 ```svelte 148 86 <script lang="ts"> 149 87 import { callXrpc } from '$hatk/client' 150 - import { createStatus as serverCreateStatus } from './status.remote' 151 88 import type { StatusView } from '$hatk/client' 152 89 153 90 let { data } = $props() ··· 170 107 171 108 try { 172 109 // 2. Call the server 173 - const res = await serverCreateStatus(emoji) 110 + const res = await callXrpc('dev.hatk.createRecord', { 111 + collection: 'xyz.statusphere.status' as const, 112 + repo: did, 113 + record: { status: emoji, createdAt: new Date().toISOString() }, 114 + }) 174 115 // 3. Replace optimistic item with real URI 175 116 items = items.map(i => 176 117 i.uri === optimisticItem.uri ··· 198 139 isMutating = true 199 140 200 141 try { 201 - await serverDeleteStatus(uri.split('/').pop()!) 142 + const rkey = uri.split('/').pop()! 143 + await callXrpc('dev.hatk.deleteRecord', { 144 + collection: 'xyz.statusphere.status' as const, 145 + rkey, 146 + }) 202 147 } catch { 203 148 if (removed) items = [removed, ...items] 204 149 } finally { ··· 207 152 } 208 153 </script> 209 154 ``` 210 - 211 - ## When to use remote functions vs. `callXrpc` 212 - 213 - | Use case | Approach | 214 - |---|---| 215 - | Mutations that need auth checks | Remote functions -- call `getViewer()` server-side | 216 - | Multi-step server logic | Remote functions -- keep it in one server round-trip | 217 - | Simple reads from components | `callXrpc` directly -- no server function needed | 218 - | Client-side infinite scroll | `callXrpc` directly in the component |
-22
docs/site/frontend/setup.md
··· 100 100 101 101 This is a convention, not a requirement -- but all hatk templates use it and the CLI scaffolding expects it. 102 102 103 - ## Experimental SvelteKit features 104 - 105 - hatk templates enable two experimental SvelteKit features: 106 - 107 - ```javascript 108 - // svelte.config.js 109 - export default { 110 - compilerOptions: { 111 - experimental: { 112 - async: true, // Async Svelte 5 components 113 - }, 114 - }, 115 - kit: { 116 - experimental: { 117 - remoteFunctions: true, // Server functions callable from components 118 - }, 119 - }, 120 - }; 121 - ``` 122 - 123 - The `remoteFunctions` feature powers the `.remote.ts` pattern described in the [mutations guide](./mutations). 124 - 125 103 ## Regenerating types 126 104 127 105 After adding or changing lexicons, regenerate the typed files:
+10 -12
docs/site/guides/hooks.md
··· 3 3 description: Run custom logic at key points in the server lifecycle. 4 4 --- 5 5 6 - Hooks let you run custom logic at key points in the Hatk lifecycle, like when a user logs in via OAuth. Define them with `defineHook()` in the `server/` directory. 6 + Hooks let you run custom logic at key points in the hatk lifecycle, like when a user logs in via OAuth. Define them with `defineHook()` in the `hooks/` directory. 7 7 8 8 ## `on-login` 9 9 10 10 The `on-login` hook runs after a successful OAuth login. The most common use is calling `ensureRepo` to backfill the user's data so it's available immediately: 11 11 12 12 ```typescript 13 - // server/on-login.ts 13 + // hooks/on-login.ts 14 14 import { defineHook } from '$hatk' 15 15 16 - export default defineHook('on-login', async ({ did, ensureRepo }) => { 17 - await ensureRepo(did) 16 + export default defineHook('on-login', async (ctx) => { 17 + await ctx.ensureRepo(ctx.did) 18 18 }) 19 19 ``` 20 20 ··· 25 25 Since the hook has full database and record access, you can check for records and create them if needed. For example, copying a user's Bluesky profile to a custom profile collection on first login: 26 26 27 27 ```typescript 28 - // server/on-login.ts 28 + // hooks/on-login.ts 29 29 import { defineHook, type BskyActorProfile, type MyAppProfile } from '$hatk' 30 30 31 31 export default defineHook('on-login', async (ctx) => { 32 - const { did, ensureRepo, lookup } = ctx 33 - 34 - await ensureRepo(did) 32 + await ctx.ensureRepo(ctx.did) 35 33 36 34 // Check if user already has an app profile 37 - const existing = await lookup<MyAppProfile>('my.app.profile', 'did', [did]) 38 - if (existing.has(did)) return 35 + const existing = await ctx.lookup<MyAppProfile>('my.app.profile', 'did', [ctx.did]) 36 + if (existing.has(ctx.did)) return 39 37 40 38 // Copy from Bluesky profile 41 - const bsky = await lookup<BskyActorProfile>('app.bsky.actor.profile', 'did', [did]) 42 - const profile = bsky.get(did) 39 + const bsky = await ctx.lookup<BskyActorProfile>('app.bsky.actor.profile', 'did', [ctx.did]) 40 + const profile = bsky.get(ctx.did) 43 41 if (!profile) return 44 42 45 43 await ctx.createRecord('my.app.profile', {
+2 -2
docs/site/index.md
··· 2 2 layout: home 3 3 4 4 hero: 5 - name: hatk 6 - tagline: Build AT Protocol apps with typed XRPC endpoints. 5 + name: "hat://k" 6 + tagline: Extends your Vite application with an AT Protocol server. Add feeds, labels, and XRPC endpoints to your app, all typed from your lexicons. 7 7 actions: 8 8 - theme: brand 9 9 text: Get Started
-23
packages/hatk/src/cli.ts
··· 74 74 dev Start PDS, seed, and run hatk 75 75 seed Seed local PDS with fixture data 76 76 reset Reset database and PDS for a clean slate 77 - schema Show database schema from lexicons 78 77 79 78 Code Quality 80 79 check Type-check and lint the project ··· 1549 1548 1550 1549 console.log(`\nResolved ${resolved.size} lexicon(s). Regenerating types...`) 1551 1550 execSync('npx hatk generate types', { stdio: 'inherit', cwd: process.cwd() }) 1552 - } else if (command === 'schema') { 1553 - const config = await loadConfig(resolve('hatk.config.ts')) 1554 - 1555 - const { initDatabase, getSchemaDump } = await import('./database/db.ts') 1556 - const { createAdapter } = await import('./database/adapter-factory.ts') 1557 - const { getDialect } = await import('./database/dialect.ts') 1558 - const configDir2 = resolve('.') 1559 - const lexicons2 = loadLexicons(resolve(configDir2, 'lexicons')) 1560 - const collections2 = config.collections.length > 0 ? config.collections : discoverCollections(lexicons2) 1561 - const { schemas: schemas2, ddlStatements: ddl2 } = buildSchemas( 1562 - lexicons2, 1563 - collections2, 1564 - getDialect(config.databaseEngine), 1565 - ) 1566 - 1567 - if (config.database !== ':memory:') { 1568 - mkdirSync(dirname(config.database), { recursive: true }) 1569 - } 1570 - const { adapter: adapter2 } = await createAdapter(config.databaseEngine) 1571 - await initDatabase(adapter2, config.database, schemas2, ddl2) 1572 - 1573 - console.log(await getSchemaDump()) 1574 1551 } else if (command === 'start') { 1575 1552 const mainPath = resolve(import.meta.dirname!, 'main.js') 1576 1553 await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts'])