···11+# Server Directory Design
22+33+**Goal:** Consolidate all server-side code into a single `server/` directory with Vite SSR integration, inspired by Nitro's DX.
44+55+**Status:** Design complete
66+77+---
88+99+## Overview
1010+1111+All server-side code lives in a single `server/` directory. hatk recursively scans it on startup, inspects each file's default export, and wires it up based on the define function used. File names and subdirectory structure are purely organizational — hatk derives all routing and semantics from the define calls themselves.
1212+1313+## Define Functions
1414+1515+| Function | Purpose | Key args |
1616+|---|---|---|
1717+| `defineQuery(nsid, opts)` | XRPC query handler | lexicon-typed input/output |
1818+| `defineProcedure(nsid, opts)` | XRPC mutation handler | lexicon-typed input/output |
1919+| `defineFeed(name, opts)` | Feed generator | handler + hydrator |
2020+| `defineHook(event, opts)` | Lifecycle hook | event name (e.g. `'on-login'`) |
2121+| `defineSetup(fn)` | Boot-time setup | runs before server starts |
2222+| `defineLabels(defs)` | Label definitions | array of label configs |
2323+| `defineOG(path, fn)` | OpenGraph image | route path, returns JSX |
2424+2525+**Execution order:** Setup scripts run first (boot), then all other handlers register. During dev, handler files get Vite SSR HMR — edits reload instantly without restarting the database or indexer.
2626+2727+## Vite Integration
2828+2929+Adding `hatk()` to your Vite config is the entire setup. No separate server process, no CLI to run alongside Vite.
3030+3131+```ts
3232+// vite.config.ts
3333+import { defineConfig } from 'vite'
3434+import { hatk } from '@hatk/hatk/vite-plugin'
3535+3636+export default defineConfig({
3737+ plugins: [hatk()]
3838+})
3939+```
4040+4141+**Dev mode (`vite dev`):** hatk boots its core runtime (database, indexer, OAuth) inside Vite's SSR context. Handler files in `server/` are loaded through Vite's module pipeline, giving them true HMR. Editing a feed or XRPC handler reloads just that handler — no database reconnection, no indexer restart, no lost firehose cursor.
4242+4343+**Build mode (`vite build`):** hatk compiles server code alongside the frontend. The output is a self-contained app — static assets plus a Node server entry point. `node dist/server.js` runs everything.
4444+4545+**Proxy rules:** In dev, Vite's dev server handles the frontend. hatk's plugin automatically proxies `/xrpc/*`, `/oauth/*`, `/.well-known/*`, `/og/*`, and other backend routes to the hatk runtime. No manual proxy configuration.
4646+4747+**What stays in the long-running core (no HMR):**
4848+- Database connections (SQLite/DuckDB)
4949+- Firehose indexer + websocket
5050+- OAuth server state
5151+- Backfill workers
5252+5353+**What gets HMR'd:**
5454+- Feeds, queries, procedures, hooks, labels, OG handlers — anything defined in `server/`
5555+5656+## Project Structure
5757+5858+A minimal hatk app:
5959+6060+```
6161+vite.config.ts
6262+hatk.config.ts
6363+lexicons/
6464+ xyz/statusphere/
6565+ profile.json
6666+ status.json
6767+server/
6868+ feed.ts
6969+ get-profile.ts
7070+src/
7171+ index.html
7272+ App.tsx
7373+```
7474+7575+A larger app organizes with optional subdirectories:
7676+7777+```
7878+vite.config.ts
7979+hatk.config.ts
8080+lexicons/
8181+ xyz/teal/
8282+ post.json
8383+ profile.json
8484+ like.json
8585+server/
8686+ setup/
8787+ seed-data.ts
8888+ feeds/
8989+ recent.ts
9090+ popular.ts
9191+ my-posts.ts
9292+ xrpc/
9393+ get-profile.ts
9494+ set-status.ts
9595+ create-post.ts
9696+ hooks/
9797+ on-login.ts
9898+ labels.ts
9999+ og-card.tsx
100100+src/
101101+ ...frontend code
102102+```
103103+104104+Both are valid. hatk doesn't enforce directory structure inside `server/` — it scans recursively and only cares about exports.
105105+106106+**`hatk.config.ts` gets simpler.** Only non-code config: collections, OAuth, relay URL, database path. Everything behavioral moves into `server/`.
107107+108108+**`lexicons/` stays separate.** Lexicons are JSON schema definitions, not server code. They're consumed at build time for type generation and at runtime for validation.
109109+110110+## Implementation Scope
111111+112112+**1. Server scanner** — New module that recursively walks `server/`, imports each file, inspects the default export, and registers it with the appropriate subsystem. Replaces the current `initFeeds()`, `initXrpc()`, `initLabels()`, `initOpengraph()`, `loadOnLoginHook()`, and `initSetup()` calls in `main.ts` with a single `initServer('server/')` call.
113113+114114+**2. Vite SSR integration** — Replace the `tsx watch` spawn in the Vite plugin with Vite's `ssrLoadModule()` for handler files. The hatk core runtime (database, indexer, OAuth) boots once and stays alive. Handler modules get loaded/reloaded through Vite's module graph, giving us HMR for free.
115115+116116+**3. Define functions** — `defineQuery`, `defineProcedure`, `defineFeed`, `defineHook`, `defineSetup`, `defineLabels`, `defineOG` all export from `@hatk/hatk`. Each returns a typed descriptor object that the scanner knows how to register. The define functions themselves are thin — they just tag the config with a type and return it.
117117+118118+**4. Build output** — `vite build` produces a server entry point alongside static assets. The entry point imports the scanned handlers and boots the hatk runtime. Production runs with `node dist/server.js`.
119119+120120+**5. Template updates** — Rewrite statusphere and teal templates to use the new `server/` layout.
121121+122122+**What doesn't change:** Database layer, indexer, backfill, OAuth, lexicon loading, schema migration — all the infrastructure work stays as-is.
···11+# Vite SSR & Environment API Integration Design
22+33+**Goal:** Replace tsx watch child process with Vite 8 Environment API for in-process HMR, rewrite server to Web Standard Request/Response, add framework-agnostic SSR rendering, and produce a single deployable artifact from `vite build`.
44+55+**Status:** Design complete
66+77+---
88+99+## Architecture Overview
1010+1111+hatk registers a custom `hatk` environment with Vite 8's Environment API. In dev, a `RunnableDevEnvironment` runs the hatk server through Vite's module runner with full HMR. In production, `vite build` produces client assets with an SSR manifest plus a server entry point.
1212+1313+**Three layers:**
1414+1515+1. **Infrastructure** — Database, firehose indexer, OAuth, backfill workers. Boots once in `configureServer`, survives handler reloads. Exposed via existing global singletons (`db.ts`, `indexer.ts`).
1616+1717+2. **Handler layer** — Everything in `server/`. Loaded through the module runner. On file change, module graph invalidates the changed module, entry re-imports. Handlers get fresh code while DB connections persist.
1818+1919+3. **Request handler** — A Web Standard `fetch` function (`Request → Response`) exported from the handler entry module. Routes API requests to XRPC/feeds/OG handlers. Routes HTML requests through the user's `defineRenderer` if provided, otherwise serves the SPA shell. A hand-rolled ~50 line adapter bridges this to Node.js `createServer` in production.
2020+2121+**SSR model:** hatk is not an SSR framework. It provides the hook point (`defineRenderer`), the SSR manifest, and the `Request` object. The user brings their own framework renderer (Vue, React, Svelte). Vite handles framework-specific compilation. hatk serves the result with correct asset preloads and OG meta tags.
2222+2323+## Dev Mode
2424+2525+The Vite plugin registers a `hatk` environment and hooks into the dev server lifecycle:
2626+2727+```ts
2828+// vite.config.ts
2929+import { defineConfig } from 'vite-plus'
3030+import { hatk } from '@hatk/hatk/vite-plugin'
3131+3232+export default defineConfig({
3333+ plugins: [hatk()],
3434+ test: { include: ['test/**/*.test.ts'] },
3535+ lint: { ignorePatterns: ['dist/**'] },
3636+})
3737+```
3838+3939+**Boot sequence:**
4040+1. Vite starts, `hatk()` plugin's `config()` hook registers the `hatk` environment
4141+2. `configureServer()` fires — boots infrastructure (DB, indexer, OAuth) from `hatk.config.ts`
4242+3. Module runner imports the handler entry module
4343+4. Entry module scans `server/`, registers handlers, exports a `fetch(Request) → Response` function
4444+5. Plugin mounts the fetch handler as Vite middleware for backend routes (`/xrpc/*`, `/oauth/*`, `/og/*`)
4545+6. For HTML requests: if `defineRenderer` exists, calls it with the `Request` and SSR manifest, serves rendered HTML with asset preloads and OG meta
4646+7. If no renderer: falls through to Vite's client pipeline (SPA mode, same as today)
4747+4848+**HMR flow:**
4949+1. User edits `server/recent.ts`
5050+2. Vite watcher fires, `hotUpdate` hook invalidates the module in the `hatk` environment's graph
5151+3. Module runner re-imports entry — scanner re-registers handlers
5252+4. Next request uses updated code. No restart, no dropped DB connections.
5353+5454+**No proxy, no child process, no ECONNREFUSED errors.** Everything runs in-process.
5555+5656+## Request/Response Architecture
5757+5858+hatk's server rewrites from Node.js `IncomingMessage`/`ServerResponse` to Web Standard `Request`/`Response`. The core becomes a pure function:
5959+6060+```ts
6161+type HatkHandler = (request: Request) => Promise<Response>
6262+```
6363+6464+**Routing order inside the handler:**
6565+1. `/xrpc/*` → XRPC query/procedure dispatch
6666+2. `/oauth/*` → OAuth server
6767+3. `/.well-known/*` → AT Protocol discovery
6868+4. `/og/*` → OpenGraph image generation
6969+5. `/*` with `Accept: text/html` → `defineRenderer` if exists, else SPA shell
7070+6. `/*` → static assets (production only, dev falls through to Vite)
7171+7272+**Node.js adapter (~50 lines):**
7373+```ts
7474+// Converts IncomingMessage → Request
7575+function toRequest(req: IncomingMessage): Request { ... }
7676+7777+// Pipes Response → ServerResponse
7878+function sendResponse(res: ServerResponse, response: Response): Promise<void> { ... }
7979+8080+// Bridge for production
8181+createServer(async (req, res) => {
8282+ const response = await handler(toRequest(req))
8383+ await sendResponse(res, response)
8484+}).listen(port)
8585+```
8686+8787+In dev, Vite middleware calls `handler(request)` directly — no adapter needed since Vite 8's environment API works with the fetch pattern.
8888+8989+**What changes from current `server.ts`:** The 1200-line file gets rewritten as a pure `Request → Response` function. All the route handling logic stays, but `res.writeHead()`/`res.end()` calls become `new Response()` constructors. The viewer auth, CORS, error handling all work the same way, just returning `Response` objects.
9090+9191+## SSR Rendering
9292+9393+hatk provides the plumbing. The user brings the framework.
9494+9595+**The hook:**
9696+```ts
9797+// server/render.tsx
9898+export default defineRenderer(async (request, manifest) => {
9999+ const url = new URL(request.url).pathname
100100+ const { render } = await import('../src/entry-server.tsx')
101101+ const html = render(url)
102102+ const preloads = manifest.getPreloadTags(url)
103103+ return { html, head: preloads }
104104+})
105105+```
106106+107107+**What hatk does with the result:**
108108+1. Reads `index.html` as a template
109109+2. Injects `head` (asset preload tags) into `<head>`
110110+3. Injects OG meta tags into `<head>` (from `defineOG` handlers, same as today)
111111+4. Injects `html` into the app mount point (e.g. `<!--ssr-outlet-->` or `<div id="app">`)
112112+5. Returns the assembled page as a `Response`
113113+114114+**What hatk does NOT do:**
115115+- No routing — the renderer decides what to render based on the URL
116116+- No data fetching magic — the renderer calls its own APIs or uses an XRPC client
117117+- No framework-specific transforms — Vite plugins handle that (`@vitejs/plugin-react`, `@sveltejs/vite-plugin-svelte`, etc.)
118118+119119+**If no `defineRenderer` exists:** hatk falls back to SPA mode — serves `index.html` with OG meta tags injected, exactly like today. SSR is opt-in.
120120+121121+**Client hydration** is entirely the user's responsibility:
122122+```tsx
123123+// src/entry-client.tsx
124124+hydrateRoot(document, <App />)
125125+```
126126+127127+## Production Build
128128+129129+`vite build` produces two outputs via Vite's `buildApp` hook:
130130+131131+**Stage 1: Client**
132132+- Standard Vite client build → `dist/client/`
133133+- Generates SSR manifest at `dist/client/.vite/ssr-manifest.json`
134134+- Static assets with content-hashed filenames
135135+136136+**Stage 2: Server (hatk environment)**
137137+- Bundles handler entry + all `server/` code → `dist/server/index.js`
138138+- Externalizes native dependencies (`better-sqlite3`, `@duckdb/node-api`)
139139+- Embeds the Node.js adapter (~50 lines)
140140+- SSR manifest inlined or referenced for asset injection
141141+142142+**Running in production:**
143143+```bash
144144+node dist/server/index.js
145145+```
146146+147147+This boots infrastructure (DB, indexer, OAuth), imports the bundled handlers, and starts an HTTP server that:
148148+- Serves API routes via the `Request → Response` handler
149149+- SSR renders HTML requests through the bundled renderer with correct asset preloads
150150+- Serves static assets from `dist/client/` with cache headers
151151+- Injects OG meta tags for all HTML responses
152152+153153+**Single deployable artifact.** No separate frontend/backend deploy. `dist/` contains everything.
154154+155155+## Example App Structure (React)
156156+157157+```
158158+vite.config.ts
159159+hatk.config.ts
160160+index.html
161161+lexicons/
162162+ xyz/myapp/
163163+ post.json
164164+ profile.json
165165+server/
166166+ feed.ts → defineFeed(...)
167167+ get-profile.ts → defineQuery(...)
168168+ on-login.ts → defineHook(...)
169169+ render.tsx → defineRenderer(...)
170170+src/
171171+ entry-client.tsx → hydrateRoot(document, <App />)
172172+ entry-server.tsx → renderToString(<App />)
173173+ App.tsx
174174+ routes/
175175+ Home.tsx
176176+ Profile.tsx
177177+```
178178+179179+Framework-agnostic: swap React for Svelte or Vue by changing the entry files and Vite plugin. hatk's `server/` code stays identical.
180180+181181+## Implementation Scope
182182+183183+| Component | What changes |
184184+|---|---|
185185+| `server.ts` | Rewrite as `Request → Response` function |
186186+| `vite-plugin.ts` | Replace tsx watch with DevEnvironment + middleware |
187187+| New: `adapter.ts` | ~50 line Node.js `Request`/`Response` bridge |
188188+| New: `defineRenderer` | New define function + SSR assembly logic |
189189+| `main.ts` | Production entry: boot infra + start server via adapter |
190190+| `cli.ts` codegen | Add `defineRenderer` export to `hatk.generated.ts` |
191191+| Templates | Migrate to vite-plus, add `entry-server`/`entry-client` |
192192+193193+**What doesn't change:** Database layer, indexer, backfill, OAuth, lexicon loading, schema migration, feeds/xrpc/labels/og handler APIs.
194194+195195+## Key Decisions
196196+197197+1. **Vite 8 Environment API** with `RunnableDevEnvironment` (not legacy `ssrLoadModule`)
198198+2. **Web Standard `Request`/`Response`** throughout (not Node.js IncomingMessage/ServerResponse)
199199+3. **Hand-rolled Node.js adapter** (~50 lines, no h3 or @whatwg-node dependency)
200200+4. **`defineRenderer(async (request, manifest) => ...)`** for framework-agnostic SSR
201201+5. **Global singletons** for infrastructure (DB, indexer) — survives HMR reloads
202202+6. **Two-stage production build** (client with SSR manifest + server environment)
203203+7. **Peer dependency on `vite`** — works with both raw Vite and vite-plus
204204+8. **SSR is opt-in** — no renderer = SPA mode, same as today