chadtmiller.com/hatk
AT Protocol app framework
1
0
Template System Design
Context
The hatk new command currently scaffolds a bare project with empty directories and core framework lexicons. Users building AT Protocol apps need a way to start from a working example rather than an empty shell. The Statusphere app (from the ATConf workshop) is the first template — a complete working app with custom lexicons, feeds, XRPC handlers, seeds, a Svelte frontend, and tests.
Design
CLI Interface
hatk new my-app --template statusphere
--template <name>selects a bundled template- Without
--template, behaves exactly as today (bare scaffold) --svelteflag is still respected; templates can also declare"svelte": truein their manifest
How It Works
- Scaffold first — run the normal scaffold logic (config.yaml, package.json, docker-compose.yml, Dockerfile, core
dev.hatk.*lexicons, tsconfig, linting config, .gitignore) - Read manifest — load
template.jsonfrom the template directory - Apply Svelte — if manifest declares
"svelte": true, generate SvelteKit files (same as--svelte) - Merge config — deep-merge template's
configobject into the generated config.yaml - Merge dependencies — add template's
dependenciesanddevDependenciesto package.json - Copy files — recursively copy all template files (except template.json) into the project, overwriting scaffold defaults where they overlap
- Finalize — run
npm install,hatk generate types, andsvelte-kit syncif Svelte
Template Location
Templates are bundled inside the hatk package at packages/appview/templates/<name>/. Discovered by listing directories in templates/.
Template Structure
packages/appview/templates/statusphere/
├── template.json
├── lexicons/
│ └── xyz/statusphere/
│ ├── defs.json
│ ├── status.json
│ └── getProfile.json
├── feeds/
│ └── recent.ts
├── xrpc/
│ └── xyz/statusphere/
│ └── getProfile.ts
├── seeds/
│ └── seed.ts
├── test/
│ ├── feeds/
│ │ └── recent.test.ts
│ └── fixtures/
│ ├── _repos.yaml
│ ├── app.bsky.actor.profile.yaml
│ └── xyz.statusphere.status.yaml
└── src/
├── routes/
│ ├── +page.svelte
│ ├── +layout.svelte
│ └── oauth/callback/+page.svelte
├── lib/
│ ├── api.ts
│ ├── auth.ts
│ └── query.ts
├── app.html
├── app.css
└── error.html
template.json Manifest
{
"description": "Statusphere example app",
"svelte": true,
"dependencies": {
"@tanstack/svelte-query": "^5"
},
"config": {
"oauth": {
"scope": "atproto repo:xyz.statusphere.status?action=create&action=delete"
}
}
}
| Field | Type | Description |
|---|---|---|
description | string | Shown in help text / template listing |
svelte | boolean | Auto-enable SvelteKit scaffold |
dependencies | object | Merged into package.json dependencies |
devDependencies | object | Merged into package.json devDependencies |
config | object | Deep-merged into generated config.yaml |
Files to Modify
packages/appview/src/cli.ts— add--templateflag parsing, template discovery, manifest loading, config merging, dependency merging, file copyingpackages/appview/templates/statusphere/— new directory with template files copied from exercise 10 (with NSIDs kept asxyz.statusphere.*, imports updated to usehatk/package names andhatk.generated.ts)
Key Decisions
- Templates are plain file copies — no string interpolation or parameterization
xyz.statusphere.*NSIDs are kept as-is (they're the app's domain, not the framework's)- Template's config is deep-merged, not replaced — base config (relay, plc, port, database) stays intact
- If a template includes
src/, it implies Svelte — the manifest's"svelte": trueensures the scaffold generates SvelteKit config files before the template's frontend files are copied on top
Verification
- Run
hatk new test-app— should produce bare scaffold as before (no regression) - Run
hatk new test-app --template statusphere— should produce a working Statusphere app cd test-app && npm install && hatk dev— app should start and be functional- Run
hatk test— tests should pass