~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/hatk

AT Protocol app framework

1
0
main
122 lines · 5.1 KB Raw History

Server Directory Design

Goal: Consolidate all server-side code into a single server/ directory with Vite SSR integration, inspired by Nitro's DX.

Status: Design complete


Overview

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.

Define Functions

FunctionPurposeKey args
defineQuery(nsid, opts)XRPC query handlerlexicon-typed input/output
defineProcedure(nsid, opts)XRPC mutation handlerlexicon-typed input/output
defineFeed(name, opts)Feed generatorhandler + hydrator
defineHook(event, opts)Lifecycle hookevent name (e.g. 'on-login')
defineSetup(fn)Boot-time setupruns before server starts
defineLabel(defs)Label definitionsarray of label configs
defineOG(path, fn)OpenGraph imageroute path, returns JSX

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.

Vite Integration

Adding hatk() to your Vite config is the entire setup. No separate server process, no CLI to run alongside Vite.

// vite.config.ts
import { defineConfig } from 'vite'
import { hatk } from '@hatk/hatk/vite-plugin'

export default defineConfig({
  plugins: [hatk()]
})

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.

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.

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.

What stays in the long-running core (no HMR):

  • Database connections (SQLite/DuckDB)
  • Firehose indexer + websocket
  • OAuth server state
  • Backfill workers

What gets HMR'd:

  • Feeds, queries, procedures, hooks, labels, OG handlers — anything defined in server/

Project Structure

A minimal hatk app:

vite.config.ts
hatk.config.ts
lexicons/
  xyz/statusphere/
    profile.json
    status.json
server/
  feed.ts
  get-profile.ts
src/
  index.html
  App.tsx

A larger app organizes with optional subdirectories:

vite.config.ts
hatk.config.ts
lexicons/
  xyz/teal/
    post.json
    profile.json
    like.json
server/
  setup/
    seed-data.ts
  feeds/
    recent.ts
    popular.ts
    my-posts.ts
  xrpc/
    get-profile.ts
    set-status.ts
    create-post.ts
  hooks/
    on-login.ts
  labels.ts
  og-card.tsx
src/
  ...frontend code

Both are valid. hatk doesn't enforce directory structure inside server/ — it scans recursively and only cares about exports.

hatk.config.ts gets simpler. Only non-code config: collections, OAuth, relay URL, database path. Everything behavioral moves into server/.

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.

Implementation Scope

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.

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.

3. Define functions — defineQuery, defineProcedure, defineFeed, defineHook, defineSetup, defineLabel, 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.

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.

5. Template updates — Rewrite statusphere and teal templates to use the new server/ layout.

What doesn't change: Database layer, indexer, backfill, OAuth, lexicon loading, schema migration — all the infrastructure work stays as-is.