@shaferllc/keel 0.66.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +2 -2
  6. package/dist/core/cache.d.ts +82 -5
  7. package/dist/core/cache.js +181 -23
  8. package/dist/core/cli/stubs.d.ts +12 -0
  9. package/dist/core/cli/stubs.js +120 -0
  10. package/dist/core/events.d.ts +129 -5
  11. package/dist/core/events.js +165 -7
  12. package/dist/core/health.d.ts +141 -0
  13. package/dist/core/health.js +226 -0
  14. package/dist/core/helpers.d.ts +9 -3
  15. package/dist/core/helpers.js +11 -3
  16. package/dist/core/index.d.ts +15 -10
  17. package/dist/core/index.js +7 -4
  18. package/dist/core/lock.d.ts +139 -0
  19. package/dist/core/lock.js +215 -0
  20. package/dist/core/logger.d.ts +82 -4
  21. package/dist/core/logger.js +141 -23
  22. package/dist/core/mail.d.ts +128 -7
  23. package/dist/core/mail.js +264 -16
  24. package/dist/core/queue.d.ts +134 -9
  25. package/dist/core/queue.js +304 -14
  26. package/dist/core/storage.d.ts +159 -6
  27. package/dist/core/storage.js +287 -7
  28. package/dist/mcp/server.d.ts +19 -0
  29. package/dist/mcp/server.js +355 -0
  30. package/docs/ai-manifest.json +2472 -0
  31. package/docs/ai.md +128 -0
  32. package/docs/architecture.md +331 -0
  33. package/docs/authentication.md +453 -0
  34. package/docs/authorization.md +167 -0
  35. package/docs/broadcasting.md +137 -0
  36. package/docs/broker.md +500 -0
  37. package/docs/cache.md +558 -0
  38. package/docs/configuration.md +311 -0
  39. package/docs/console.md +356 -0
  40. package/docs/container.md +467 -0
  41. package/docs/controllers.md +265 -0
  42. package/docs/cors.md +51 -0
  43. package/docs/database.md +530 -0
  44. package/docs/debugging.md +129 -0
  45. package/docs/decorators.md +127 -0
  46. package/docs/errors.md +395 -0
  47. package/docs/events.md +496 -0
  48. package/docs/examples/architecture-app.ts +27 -0
  49. package/docs/examples/authentication.ts +61 -0
  50. package/docs/examples/authorization.ts +79 -0
  51. package/docs/examples/broadcasting.ts +60 -0
  52. package/docs/examples/broker-cache-validate.ts +34 -0
  53. package/docs/examples/broker-fault-tolerance.ts +29 -0
  54. package/docs/examples/broker-middleware.ts +27 -0
  55. package/docs/examples/broker.ts +203 -0
  56. package/docs/examples/cache.ts +222 -0
  57. package/docs/examples/configuration.ts +81 -0
  58. package/docs/examples/container.ts +134 -0
  59. package/docs/examples/controllers.ts +86 -0
  60. package/docs/examples/database.ts +118 -0
  61. package/docs/examples/debugging.ts +41 -0
  62. package/docs/examples/decorators.ts +40 -0
  63. package/docs/examples/errors.ts +121 -0
  64. package/docs/examples/events.ts +204 -0
  65. package/docs/examples/factories.ts +84 -0
  66. package/docs/examples/hashing.ts +71 -0
  67. package/docs/examples/health.ts +94 -0
  68. package/docs/examples/helpers.ts +171 -0
  69. package/docs/examples/hooks.ts +54 -0
  70. package/docs/examples/inertia.ts +81 -0
  71. package/docs/examples/locks.ts +120 -0
  72. package/docs/examples/logger.ts +92 -0
  73. package/docs/examples/mail.ts +160 -0
  74. package/docs/examples/middleware.ts +119 -0
  75. package/docs/examples/migrations.ts +126 -0
  76. package/docs/examples/models.ts +239 -0
  77. package/docs/examples/notification.ts +124 -0
  78. package/docs/examples/providers.ts +123 -0
  79. package/docs/examples/queues.ts +254 -0
  80. package/docs/examples/rate-limiting.ts +42 -0
  81. package/docs/examples/redis.ts +99 -0
  82. package/docs/examples/request-response.ts +197 -0
  83. package/docs/examples/routing.ts +186 -0
  84. package/docs/examples/scheduling.ts +62 -0
  85. package/docs/examples/sessions.ts +102 -0
  86. package/docs/examples/static-files.ts +63 -0
  87. package/docs/examples/storage.ts +132 -0
  88. package/docs/examples/templates.ts +58 -0
  89. package/docs/examples/testing.ts +66 -0
  90. package/docs/examples/transformer.ts +141 -0
  91. package/docs/examples/transformers.ts +49 -0
  92. package/docs/examples/url-builder.ts +86 -0
  93. package/docs/examples/validation.ts +102 -0
  94. package/docs/examples/views.tsx +62 -0
  95. package/docs/examples/vite.ts +106 -0
  96. package/docs/factories.md +166 -0
  97. package/docs/getting-started.md +290 -0
  98. package/docs/hashing.md +259 -0
  99. package/docs/health.md +225 -0
  100. package/docs/helpers.md +347 -0
  101. package/docs/hono.md +186 -0
  102. package/docs/hooks.md +118 -0
  103. package/docs/inertia.md +241 -0
  104. package/docs/locks.md +323 -0
  105. package/docs/logger.md +290 -0
  106. package/docs/mail.md +678 -0
  107. package/docs/middleware.md +425 -0
  108. package/docs/migrations.md +476 -0
  109. package/docs/models.md +810 -0
  110. package/docs/notifications.md +474 -0
  111. package/docs/providers.md +363 -0
  112. package/docs/queues.md +679 -0
  113. package/docs/rate-limiting.md +155 -0
  114. package/docs/redis.md +178 -0
  115. package/docs/request-response.md +953 -0
  116. package/docs/routing.md +804 -0
  117. package/docs/scheduling.md +110 -0
  118. package/docs/security.md +85 -0
  119. package/docs/sessions.md +354 -0
  120. package/docs/social-auth.md +174 -0
  121. package/docs/static-files.md +211 -0
  122. package/docs/storage.md +450 -0
  123. package/docs/templates.md +315 -0
  124. package/docs/testing.md +125 -0
  125. package/docs/transformers.md +381 -0
  126. package/docs/url-builder.md +295 -0
  127. package/docs/validation.md +288 -0
  128. package/docs/views.md +267 -0
  129. package/docs/vite.md +434 -0
  130. package/llms-full.txt +17694 -0
  131. package/llms.txt +116 -0
  132. package/package.json +26 -7
package/docs/ai.md ADDED
@@ -0,0 +1,128 @@
1
+ # Building Keel apps with AI
2
+
3
+ Keel is built to be **written with an AI agent**. This page is the map of the
4
+ AI-facing surface: an MCP server, machine-readable docs (`llms.txt` /
5
+ `llms-full.txt`), an agent playbook (`AGENTS.md`), and code generators an agent
6
+ can drive directly.
7
+
8
+ If you only read one thing: point your agent at the [MCP server](#the-mcp-server)
9
+ and have it call `keel_overview` first.
10
+
11
+ ## Why this exists
12
+
13
+ An agent working in a Keel app needs three things: to know the **conventions**
14
+ (where things live, what imports to use), to **look up APIs and guides** without
15
+ hallucinating, and to **generate correct boilerplate**. Keel provides each as a
16
+ first-class, always-current surface — generated from the same source as the
17
+ human docs, so they never drift.
18
+
19
+ ## The MCP server
20
+
21
+ Keel ships an [MCP](https://modelcontextprotocol.io) server that exposes its
22
+ documentation, the full public API surface (380+ exports), the generators, and
23
+ its conventions to any MCP-capable client.
24
+
25
+ ### Connect it
26
+
27
+ **Claude Code**, in an app that depends on `@shaferllc/keel`:
28
+
29
+ ```bash
30
+ claude mcp add keel -- npx -y keel-mcp
31
+ ```
32
+
33
+ Hacking on the framework repo itself:
34
+
35
+ ```bash
36
+ claude mcp add keel -- npm --prefix /path/to/keel run mcp
37
+ ```
38
+
39
+ **`.mcp.json`, Cursor, Windsurf, or any client** that reads the standard config:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "keel": { "command": "npx", "args": ["-y", "keel-mcp"] }
45
+ }
46
+ }
47
+ ```
48
+
49
+ The server speaks over stdio and prints its banner to **stderr** (stdout is the
50
+ protocol channel). It resolves docs from the installed `@shaferllc/keel`
51
+ package, so it always matches your installed version.
52
+
53
+ ### Tools
54
+
55
+ | Tool | What it does |
56
+ |------|--------------|
57
+ | `keel_overview` | Version, conventions, folder layout, every doc topic, and the generators. **Call this first.** |
58
+ | `keel_search_docs` | Full-text search across all guides; returns snippets + slugs. |
59
+ | `keel_read_doc` | A full guide by slug, optionally with its runnable example appended. |
60
+ | `keel_search_api` | Search the public export surface; returns each symbol's module and its guide. |
61
+ | `keel_list_generators` | The `keel make:*` generators, what they produce, and their flags. |
62
+ | `keel_scaffold` | Generate a controller/provider/middleware/factory/seeder/job/notification/transformer stub. Returns code + target path — it does **not** write to disk. |
63
+
64
+ ### Resources
65
+
66
+ - `keel://overview` — the same orientation text as `keel_overview`
67
+ - `keel://llms-full` — every guide concatenated (drop into a fresh context)
68
+ - `keel://docs/<slug>` — one resource per guide (e.g. `keel://docs/routing`)
69
+
70
+ ### A typical agent loop
71
+
72
+ 1. `keel_overview` → learn the conventions and topic list.
73
+ 2. `keel_search_docs { query: "belongsToMany pivot" }` → find the right guide.
74
+ 3. `keel_read_doc { slug: "models", include_example: true }` → read it in full.
75
+ 4. `keel_scaffold { kind: "controller", name: "Post", resource: true }` → get the stub.
76
+ 5. Write the file, add the route, run `npm run typecheck`.
77
+
78
+ ## `llms.txt` and `llms-full.txt`
79
+
80
+ At the package root:
81
+
82
+ - **[`llms.txt`](../llms.txt)** — the [llms.txt-spec](https://llmstxt.org) index:
83
+ a titled, summarized, linked list of every guide and example. Good for AI
84
+ crawlers and "add this URL as context" flows.
85
+ - **[`llms-full.txt`](../llms-full.txt)** — every guide concatenated into one
86
+ file (~17k lines), ordered from getting-started outward. Paste it into a fresh
87
+ context window when you want the agent to have all of Keel at once.
88
+
89
+ Both are generated by `npm run build:ai` from `docs/` and shipped in the npm
90
+ package, so they're available at `node_modules/@shaferllc/keel/llms-full.txt`.
91
+
92
+ ## `AGENTS.md`
93
+
94
+ [`AGENTS.md`](../AGENTS.md) at the repo root is the agent playbook: the one
95
+ import rule, the folder map, the container/provider mental model, a "how to add
96
+ X" table, the commands, and the guardrails (typecheck before finishing, rerun
97
+ `build:ai` after doc changes). It follows the emerging cross-tool `AGENTS.md`
98
+ convention and is shipped in the package. `CLAUDE.md` points to it.
99
+
100
+ ## Generators
101
+
102
+ Everything scaffoldable is available three ways, all emitting the same stubs:
103
+
104
+ - **MCP:** `keel_scaffold` (returns code; you write it)
105
+ - **Console:** `keel make:controller Post -r` (writes the file, won't overwrite)
106
+ - **By hand:** copy from `keel_read_doc` examples
107
+
108
+ See [the console guide](./console.md) for the full command list.
109
+
110
+ ## Keeping it current
111
+
112
+ The AI surface is generated, not hand-maintained:
113
+
114
+ ```bash
115
+ npm run build:ai # regenerates llms.txt, llms-full.txt, docs/ai-manifest.json
116
+ ```
117
+
118
+ `npm run build` runs it automatically before compiling. After you add or edit a
119
+ doc, or change the public exports in `src/core/index.ts`, run `build:ai` so the
120
+ manifest the MCP server reads stays in sync. `docs/ai-manifest.json` is the
121
+ single machine-readable index (docs + API + generators); treat it as generated
122
+ output.
123
+
124
+ ## See also
125
+
126
+ - [Getting Started](./getting-started.md) — the human first-hour guide
127
+ - [Architecture](./architecture.md) — how a request flows through Keel
128
+ - [The Console](./console.md) — the `keel` command and generators
@@ -0,0 +1,331 @@
1
+ # Architecture
2
+
3
+ Keel is small on purpose. This page maps the pieces and traces a request from
4
+ socket to response. Nothing here is magic — every layer is a short, readable file
5
+ in `src/core/`, and this guide is mostly a reading order for it.
6
+
7
+ ## The layers
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────────────┐
11
+ │ bin/keel.ts console entry (serve, make:*, …) │
12
+ ├─────────────────────────────────────────────────────────┤
13
+ │ bootstrap/app.ts createApplication() │
14
+ │ └─ boots providers, binds HTTP kernel, loads routes │
15
+ ├─────────────────────────────────────────────────────────┤
16
+ │ Application (extends Container) │
17
+ │ ├─ loads .env + config/*.ts │
18
+ │ └─ provider register() → boot() lifecycle │
19
+ ├─────────────────────────────────────────────────────────┤
20
+ │ Container bind / singleton / instance / make │
21
+ ├─────────────────────────────────────────────────────────┤
22
+ │ HttpKernel global middleware → compiles routes → Hono │
23
+ ├─────────────────────────────────────────────────────────┤
24
+ │ @hono/node-server the actual HTTP server │
25
+ └─────────────────────────────────────────────────────────┘
26
+ ```
27
+
28
+ Read it top to bottom as the flow of control at boot, and bottom to top as the
29
+ flow of a request. The console and the server both enter through
30
+ `createApplication()` — the difference is only what they do with the app once
31
+ it's booted (serve it, or run a command against it).
32
+
33
+ ## Core building blocks
34
+
35
+ - **`Container`** ([container.ts](../src/core/container.ts)) — the dependency
36
+ registry. Two maps (bindings, cached instances) and a `make()` resolver.
37
+ - **`Application`** ([application.ts](../src/core/application.ts)) — a
38
+ `Container` with a lifecycle: load env, auto-load config, register and boot
39
+ providers.
40
+ - **`Config`** ([config.ts](../src/core/config.ts)) — a dot-notation repository
41
+ plus the `env()` coercion helper.
42
+ - **`ServiceProvider`** ([provider.ts](../src/core/provider.ts)) — the
43
+ `register()` / `boot()` contract.
44
+ - **`Router`** ([http/router.ts](../src/core/http/router.ts)) — collects route
45
+ definitions; resolves controller tuples out of the container.
46
+ - **`HttpKernel`** ([http/kernel.ts](../src/core/http/kernel.ts)) — holds global
47
+ middleware and compiles the router onto a Hono instance.
48
+
49
+ ## The container is the center
50
+
51
+ Everything else hangs off the container. `Application` **is** a `Container` — it
52
+ extends it — so the same `bind` / `singleton` / `instance` / `make` surface that
53
+ registers a service also holds `Config`, `Router`, `View`, the `Logger`, and
54
+ your own controllers.
55
+
56
+ ```ts
57
+ app.singleton(Router, (a) => new Router(a)); // registered at construction
58
+ const router = app.make(Router); // resolved anywhere later
59
+ ```
60
+
61
+ Two properties make this the spine of the framework:
62
+
63
+ - **Everything resolves through one place.** A controller doesn't `new` its
64
+ dependencies — it receives the container in its constructor and pulls what it
65
+ needs, so tests can swap any binding for a fake without touching the code under
66
+ test.
67
+ - **Classes auto-resolve.** `make(SomeClass)` builds `SomeClass` even with no
68
+ explicit binding, handing its constructor the container. You only register a
69
+ binding when construction needs configuration or should be shared.
70
+
71
+ `bind` gives a fresh value each resolve; `singleton` caches after the first;
72
+ `instance` stores an already-built value. The global helpers (`make()`,
73
+ `bind()`, `config()`, `app()`) are thin wrappers that resolve against the active
74
+ application, so you rarely thread the container by hand. See
75
+ [The Service Container](./container.md) for the full API and resolution rules.
76
+
77
+ ## Service providers wire it up
78
+
79
+ Providers are the seams where your services enter the container. Each has two
80
+ phases, and the split matters:
81
+
82
+ ```ts
83
+ export class AppServiceProvider extends ServiceProvider {
84
+ register(): void {
85
+ // Bind only. Nothing else is guaranteed registered yet.
86
+ bind("clock", () => new Date().toISOString());
87
+ }
88
+
89
+ boot(): void {
90
+ // Every provider has registered — safe to resolve and wire things.
91
+ }
92
+ }
93
+ ```
94
+
95
+ `Application.boot()` runs **all** providers' `register()` before **any**
96
+ provider's `boot()`. That ordering is the whole point: `register()` may only
97
+ add bindings, so `boot()` can safely depend on anything any provider bound,
98
+ regardless of order. Reaching for another service inside `register()` is the
99
+ classic bug — the binding may not exist yet. [Service Providers](./providers.md)
100
+ goes deeper.
101
+
102
+ ## Boot sequence
103
+
104
+ When `keel serve` runs:
105
+
106
+ 1. **`createApplication()`** constructs the `Application` with the project root.
107
+ 2. The constructor registers the active application (for global helpers) and
108
+ binds core services (`Config`, `Router`, `View`, `Events`, `Cache`,
109
+ `Logger`).
110
+ 3. **`app.boot(providers)`**:
111
+ - loads `.env`, then every `config/*.ts` file into the `Config` repository,
112
+ - runs each provider's **`register()`** (bind-only phase),
113
+ - runs each provider's **`boot()`** (wire-up phase).
114
+ 4. The HTTP kernel (`app/Http/Kernel.ts`) is bound as a singleton.
115
+ 5. `routes/web.ts` registers routes on the `Router`.
116
+ 6. `HttpKernel.build()` returns a Hono app; `@hono/node-server` serves it.
117
+
118
+ Steps 1–3 are identical whether you're serving or running a console command —
119
+ `createApplication()` is the single door in. Only steps 4–6 are HTTP-specific.
120
+
121
+ ## The application object
122
+
123
+ `Application` is Keel's central object — the [container](./container.md) plus a
124
+ lifecycle. Beyond `register()`/`boot()`, it carries a small ergonomic surface
125
+ modelled on the classic service-app pattern (Feathers' `app`): a settings store,
126
+ an inline plugin hook, and app-level events.
127
+
128
+ **Configure with a plain function.** `register(Provider)` gives you the
129
+ two-phase register/boot lifecycle; `configure(fn)` is the one-shot alternative
130
+ for inline setup — call a function with the app, chain the next:
131
+
132
+ ```ts
133
+ app
134
+ .configure((a) => a.set("mail.from", "hi@keel.dev"))
135
+ .configure(installBilling); // (app) => { … }
136
+ ```
137
+
138
+ **Store app-wide values.** `set`/`get` are a thin façade over the `Config`
139
+ repository, so `app.set("db.url", …)` and `config().get("db.url")` read the same
140
+ store — no second bag to keep in sync:
141
+
142
+ ```ts
143
+ app.set("db.url", process.env.DATABASE_URL);
144
+ const url = app.get<string>("db.url");
145
+ const port = app.get("port", 3000); // typed fallback
146
+ ```
147
+
148
+ **Emit and listen at the app level.** `on`/`once`/`off`/`emit` delegate to the
149
+ [`Events`](./events.md) singleton, so `app.on(...)` and the global `listen()`
150
+ helper share one emitter. Listeners may be async; `emit` awaits them in order:
151
+
152
+ ```ts
153
+ const off = app.on("user.registered", (user) => sendWelcome(user));
154
+ await app.emit("user.registered", user);
155
+ off(); // unsubscribe
156
+ ```
157
+
158
+ ### API reference
159
+
160
+ #### `app.configure(fn)`
161
+
162
+ Run a `Configurator` — `(app) => unknown` — against the app and return the app
163
+ for chaining. The lightweight alternative to a `ServiceProvider` when you don't
164
+ need the register/boot split.
165
+
166
+ ```ts
167
+ app.configure((a) => a.router().get("/health", () => "ok"));
168
+ ```
169
+
170
+ Notes: runs immediately and synchronously in call order. Its return value is
171
+ ignored (return-for-chaining is the app, not the fn's result). For anything that
172
+ must bind before another service boots, use a provider instead.
173
+
174
+ #### `app.set(key, value)` / `app.get(key, fallback?)`
175
+
176
+ Write and read an app-wide value. Both use dot-notation and are backed by
177
+ `Config`, so values set here are visible to `config()` and vice-versa. `set`
178
+ returns the app (chainable); `get` takes an optional typed fallback.
179
+
180
+ ```ts
181
+ app.set("app.name", "Keel");
182
+ config().get("app.name"); // "Keel"
183
+ app.get("app.name"); // "Keel"
184
+ app.get("app.locale", "en"); // fallback when unset
185
+ ```
186
+
187
+ Notes: because the store is shared with `Config`, prefer namespaced keys
188
+ (`"mail.from"`, not `"from"`) to avoid collisions with `config/*.ts` files.
189
+
190
+ #### `app.on(event, listener)` / `app.once(event, listener)`
191
+
192
+ Subscribe to an app event; `once` auto-unsubscribes after the first emission.
193
+ Both return an **unsubscribe function**. Delegates to the `Events` singleton.
194
+
195
+ ```ts
196
+ const off = app.on<Order>("order.paid", (o) => fulfil(o));
197
+ ```
198
+
199
+ Notes: the listener signature is `(payload) => void | Promise<void>`. Identical
200
+ to `app.make(Events).on(...)` — the method is sugar so you rarely resolve
201
+ `Events` by hand.
202
+
203
+ #### `app.off(event, listener)`
204
+
205
+ Remove a listener registered with `on`/`once`. Returns the app (chainable). Pass
206
+ the *same* function reference used to subscribe.
207
+
208
+ #### `app.emit(event, payload?)`
209
+
210
+ Emit an app event, awaiting every listener in registration order. Returns a
211
+ `Promise<void>`. An async listener that rejects propagates out of `emit`.
212
+
213
+ ```ts
214
+ await app.emit("cache.cleared", { at: Date.now() });
215
+ ```
216
+
217
+ ## Request lifecycle
218
+
219
+ For each incoming request:
220
+
221
+ ```
222
+ request
223
+ → Hono matches the route
224
+ → contextStorage() stashes the context for the request helpers
225
+ → context middleware sets c.get("app") = the container
226
+ → global middleware stack (e.g. requestLogger) runs, in order
227
+ → the route handler runs:
228
+ • a closure → called with (c)
229
+ • a [Controller, m] → controller resolved from the container,
230
+ then method(c) is called (DI in the ctor)
231
+ → the handler's return value becomes the response
232
+ (a string is wrapped as HTML; a Response passes through)
233
+ → middleware unwinds on the way back out
234
+ response
235
+ ```
236
+
237
+ A few details worth knowing:
238
+
239
+ - **The context is stashed per request.** Before your middleware runs, the
240
+ kernel enables Hono's `contextStorage()`. That's what lets the [request
241
+ helpers](./request-response.md) (`request`, `param()`, `json()`) reach the
242
+ current request without you passing `c` around.
243
+ - **Controllers are resolved lazily, per request.** The router turns a
244
+ `[Controller, method]` tuple into a function that resolves the controller from
245
+ the container when the route fires — so constructor DI runs against the live
246
+ app. Tuples may also be `() => import(...)` loaders for code-splitting.
247
+ - **Errors funnel through the kernel.** A thrown `HttpException` renders at its
248
+ status; anything else is a 500. The kernel content-negotiates — HTML for
249
+ browsers, JSON otherwise — and hides internals unless `app.debug` is on.
250
+ Unmatched routes go through the same path as a `NotFoundException`.
251
+
252
+ [Middleware](./middleware.md) covers the stack in detail, and [Routing](./routing.md)
253
+ covers how handlers are declared and matched.
254
+
255
+ ## Edge-safe by design
256
+
257
+ Keel's core imports **no** Node built-ins at module load. `fs`, `path`, `url`,
258
+ and `dotenv` are pulled in **dynamically**, and only when filesystem
259
+ discovery is enabled:
260
+
261
+ ```ts
262
+ // application.ts — dynamic, guarded, optional
263
+ const { readdir } = await import("node:fs/promises");
264
+ ```
265
+
266
+ The payoff is that the same `Application`, `Router`, `View`, and query builder
267
+ run unchanged on Cloudflare Workers, Deno, and Bun — anywhere with web-standard
268
+ `fetch`, `Request`, `Response`, and Web Crypto. On Workers, where there's no
269
+ filesystem to scan, you skip discovery and pass config inline:
270
+
271
+ ```ts
272
+ await app.boot(providers, { discoverConfig: false, config: { app: { name: "Keel" } } });
273
+ ```
274
+
275
+ The second argument is `BootOptions`: `discoverConfig` (skip filesystem config
276
+ discovery — the default on the edge) and `config` (an inline config object merged
277
+ in). Together they let the app boot with no filesystem at all.
278
+
279
+ Everything that would normally reach for a platform API is designed around this
280
+ seam: the [database](./database.md) layer talks to a `Connection` you provide
281
+ rather than importing a driver; signed URLs use Web Crypto's `crypto.subtle`;
282
+ views render to strings with no filesystem. The rule is simple — the core owns
283
+ logic, the platform owns I/O, and the two meet at an interface you supply.
284
+
285
+ ## Two repos: library and starter
286
+
287
+ Keel is distributed like most frameworks — a **library** you install plus an
288
+ **app** that depends on it:
289
+
290
+ | Repo | Role |
291
+ |------|------|
292
+ | `shaferllc/keel` | The framework. Published as `@shaferllc/keel`; userland imports `@shaferllc/keel/core`. |
293
+ | `shaferllc/keel-app` | The starter app — clone it to build something. Picks up core updates via `npm update`. |
294
+
295
+ The split mirrors the classic application-vs-library separation. Your code lives
296
+ in `app/` (controllers, providers, middleware); the framework lives behind the
297
+ package boundary. Because the two are versioned separately, a framework upgrade
298
+ is an ordinary dependency bump — your `app/` doesn't move. [Getting
299
+ Started](./getting-started.md#install) walks through both install paths.
300
+
301
+ ## Design principles
302
+
303
+ - **One container, resolved everywhere.** Testability and composition follow
304
+ from routing every dependency through it.
305
+ - **Convention over configuration.** Fixed folders (`app/`, `config/`,
306
+ `routes/`, `bootstrap/`) mean no manual wiring for the common case.
307
+ - **Thin over clever.** The framework is a few hundred readable lines. When in
308
+ doubt, open the source — there is no hidden magic.
309
+ - **Wrap the best, own the surface.** Hono does HTTP; Keel owns the developer-
310
+ facing API so the underlying library can change without breaking your app.
311
+ [Built on Hono](./hono.md) draws that line precisely.
312
+ - **Edge-safe, driver-agnostic.** The core imports no database driver, no mail
313
+ SDK, no socket. Every backend — [database](./database.md), [mail](./mail.md),
314
+ [redis](./redis.md), [queues](./queues.md), [storage](./storage.md),
315
+ [broadcasting](./broadcasting.md) — plugs in behind a small interface, so the
316
+ same app runs on Node and on the edge. You bring the driver; Keel brings the
317
+ ergonomics.
318
+ - **Explicit over implicit.** No hidden runtime magic: body parsing is a method
319
+ you call, [templates](./templates.md) interpret rather than `eval`, and
320
+ providers register into one predictable global scope. Boring and predictable
321
+ beats clever and surprising.
322
+
323
+ ## Extending Keel
324
+
325
+ The MVP core is deliberately small. Natural extension points:
326
+
327
+ - **New services** → a service provider that binds them into the container.
328
+ - **New console commands** → add to [cli/index.ts](../src/core/cli/index.ts).
329
+ - **New subsystems** (ORM, queues, mail) → a provider that registers the
330
+ subsystem plus a `config/*.ts` file for its settings. This is exactly how the
331
+ roadmap items will land.