@shaferllc/keel 0.66.0 → 0.74.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 (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -7
@@ -0,0 +1,347 @@
1
+ # Helpers
2
+
3
+ Keel gives you a handful of **global helper functions** so you can reach the
4
+ running application from anywhere — a route handler, a model, a plain function —
5
+ without threading a container reference through every call. `config('app.name')`,
6
+ `cache().get(…)`, `emit('user.registered', user)`: no `this.app`, no imports of
7
+ the container.
8
+
9
+ They all resolve against the **active application**, which registers itself the
10
+ moment an `Application` is constructed. In a normal single-app process — one Node
11
+ server, or one Worker isolate — that's exactly the app you mean, so the globals
12
+ just work.
13
+
14
+ ```ts
15
+ import { config, cache, emit, logger, view } from "@shaferllc/keel/core";
16
+
17
+ const name = config<string>("app.name", "Keel");
18
+ const stats = await cache().remember("stats", 60, () => computeStats());
19
+ await emit("user.registered", user);
20
+ logger().info("welcome sent", { userId: user.id });
21
+ ```
22
+
23
+ ## How they resolve
24
+
25
+ Every helper is sugar over `app()` — the one helper that returns the active
26
+ `Application`. `config()` is `app().make(Config).get(…)`; `cache()` is
27
+ `app().make(Cache)`; `make()` is `app().make(…)`. So the whole set shares one
28
+ precondition: **an application must exist first.** Call any helper before
29
+ bootstrapping and `app()` throws:
30
+
31
+ ```
32
+ No Keel application has been bootstrapped. Create an Application first.
33
+ ```
34
+
35
+ In practice the application is created at boot, long before any request runs, so
36
+ you never see this outside of a bare unit test that forgot to construct one.
37
+
38
+ ## The map
39
+
40
+ The helpers fall into groups, most of which have a dedicated guide. This page is
41
+ the quick index — reach for the deep doc when you need the full surface.
42
+
43
+ | Helper(s) | What it reaches | Deep dive |
44
+ | --- | --- | --- |
45
+ | `app` | the active `Application` | this page |
46
+ | `config` | configuration values | [configuration](./configuration.md) |
47
+ | `bind` `singleton` `instance` `make` `bound` | the service container | [container](./container.md) |
48
+ | `events` `emit` `listen` | the event emitter | [events](./events.md) |
49
+ | `cache` | the cache | [cache](./cache.md) |
50
+ | `logger` | the logger | [logger](./logger.md) |
51
+ | `view` | the view renderer | [views](./views.md) |
52
+
53
+ ## Container helpers, up close
54
+
55
+ The five container helpers let you register and resolve services from anywhere,
56
+ exactly as `app().bind(…)` would — handy inside a factory or a helper function
57
+ that has no container reference of its own:
58
+
59
+ ```ts
60
+ import { singleton, make, bound } from "@shaferllc/keel/core";
61
+
62
+ singleton(Mailer, (app) => new Mailer(app.make(Config)));
63
+ const mailer = make(Mailer);
64
+ if (bound("clock")) { /* someone registered it */ }
65
+ ```
66
+
67
+ The factory you pass to `bind`/`singleton` receives the container, so a service
68
+ can resolve its own dependencies. Unlike the `Container` methods (which return
69
+ `this` to chain), the `bind`/`singleton` **helpers return `void`** — there's no
70
+ builder to chain off of at the global level. See [container](./container.md) for
71
+ the binding lifecycle, auto-resolution, and tokens.
72
+
73
+ ## Events, cache, logger
74
+
75
+ `events()`, `cache()`, and `logger()` each return the singleton service, so you
76
+ call methods on the result:
77
+
78
+ ```ts
79
+ import { events, cache, logger, listen, emit } from "@shaferllc/keel/core";
80
+
81
+ listen("order.paid", (order) => fulfil(order)); // subscribe
82
+ await emit("order.paid", order); // fan out, awaiting listeners
83
+ events().listenerCount("order.paid"); // the emitter itself
84
+
85
+ await cache().put("otp", code, 300);
86
+ logger().warn("retrying", { attempt: 2 });
87
+ ```
88
+
89
+ `emit` and `listen` are shortcuts over `events().emit` / `events().on`, so you
90
+ rarely need `events()` directly — reach for it when you want `once`, `off`,
91
+ `listenerCount`, or `clear`. Full surface in [events](./events.md),
92
+ [cache](./cache.md), and [logger](./logger.md).
93
+
94
+ ## Rendering a view
95
+
96
+ `view()` renders a component to a complete HTML document in one call — return it
97
+ straight from a handler. Props are type-checked against the component:
98
+
99
+ ```ts
100
+ import { view } from "@shaferllc/keel/core";
101
+
102
+ function Welcome({ appName }: { appName: string }) {
103
+ return `<h1>Welcome to ${appName}</h1>`;
104
+ }
105
+
106
+ return view(Welcome, { appName: "Keel" }); // Promise<string>
107
+ return view(HomePage); // no props
108
+ ```
109
+
110
+ See [views](./views.md) for the component contract and async (Suspense) rendering.
111
+
112
+ ## Related
113
+
114
+ These globals are the front door to services documented in depth elsewhere:
115
+ [configuration](./configuration.md), the [container](./container.md),
116
+ [events](./events.md), [cache](./cache.md), [logger](./logger.md), and
117
+ [views](./views.md). Everything here is a thin, typed shortcut into one of those.
118
+
119
+ ---
120
+
121
+ ## API reference
122
+
123
+ Every helper below is exported from `@shaferllc/keel/core`. All of them resolve
124
+ against the active application and therefore throw
125
+ `No Keel application has been bootstrapped…` if called before one is created.
126
+
127
+ ### `app()`
128
+
129
+ `app(): Application`
130
+
131
+ Returns the active `Application` — the container everything else resolves out of.
132
+
133
+ ```ts
134
+ import { app } from "@shaferllc/keel/core";
135
+
136
+ const port = app().config().get<number>("app.port", 3000);
137
+ ```
138
+
139
+ **Notes:** throws if no application has been constructed yet. Every other helper
140
+ on this page is built on `app()`, so this is the single point where a
141
+ "no application" error can originate.
142
+
143
+ ### `config(key, fallback?)`
144
+
145
+ `config<T = unknown>(key: string, fallback?: T): T`
146
+
147
+ Reads a configuration value by dot-path, returning `fallback` when the path is
148
+ missing.
149
+
150
+ ```ts
151
+ config<string>("app.name");
152
+ config("app.port", 3000); // 3000 if unset
153
+ ```
154
+
155
+ **Notes:** shorthand for `app().make(Config).get(key, fallback)`. Read-only —
156
+ use `app().make(Config).set(…)` to write. See [configuration](./configuration.md).
157
+
158
+ ### `view(component, props?)`
159
+
160
+ `view<P>(component: (props: P, ...rest: any[]) => Renderable, props: P): Promise<string>`
161
+ `view(component: (...rest: any[]) => Renderable): Promise<string>`
162
+
163
+ Renders a component (with optional props) to a complete HTML document.
164
+
165
+ ```ts
166
+ return view(Welcome, { appName: "Keel" });
167
+ return view(HomePage);
168
+ ```
169
+
170
+ **Notes:** props are type-checked against the component's parameter. Resolves to
171
+ a `Promise<string>` (a full HTML document, doctype included) — return it directly
172
+ from a route handler. Sugar over `app().make(View).render(component(props))`. See
173
+ [views](./views.md).
174
+
175
+ ### `bind(token, factory)`
176
+
177
+ `bind<T>(token: Token<T>, factory: Factory<T>): void`
178
+
179
+ Registers a **transient** binding — the factory runs on every `make`.
180
+
181
+ ```ts
182
+ bind("clock", () => new Date());
183
+ ```
184
+
185
+ **Notes:** the factory receives the container. Returns `void` (the underlying
186
+ `Container.bind` returns `this`, but the helper does not). See
187
+ [container](./container.md).
188
+
189
+ ### `singleton(token, factory)`
190
+
191
+ `singleton<T>(token: Token<T>, factory: Factory<T>): void`
192
+
193
+ Registers a **shared** binding — the factory runs once, then the value is cached.
194
+
195
+ ```ts
196
+ singleton(Mailer, (app) => new Mailer(app.make(Config)));
197
+ ```
198
+
199
+ **Notes:** the cached value lives for the life of the application. Returns
200
+ `void`. See [container](./container.md).
201
+
202
+ ### `instance(token, value)`
203
+
204
+ `instance<T>(token: Token<T>, value: T): T`
205
+
206
+ Registers an already-constructed value as a shared instance, and returns it.
207
+
208
+ ```ts
209
+ const version = instance("app.version", "0.30.0");
210
+ ```
211
+
212
+ **Notes:** unlike `bind`/`singleton`, this returns the value you passed in, so
213
+ you can register-and-use in one expression. See [container](./container.md).
214
+
215
+ ### `make(token)`
216
+
217
+ `make<T>(token: Token<T>): T`
218
+
219
+ Resolves a token out of the container.
220
+
221
+ ```ts
222
+ const mailer = make(Mailer);
223
+ const version = make<string>("app.version");
224
+ ```
225
+
226
+ **Notes:** a zero-arg class token resolves even without an explicit binding (the
227
+ container builds it); an unbound string/symbol token throws
228
+ `Nothing bound in the container for […]`. See [container](./container.md).
229
+
230
+ ### `bound(token)`
231
+
232
+ `bound(token: Token): boolean`
233
+
234
+ `true` if the token has a binding or a cached instance.
235
+
236
+ ```ts
237
+ if (bound("clock")) make<Date>("clock");
238
+ ```
239
+
240
+ **Notes:** a guard for optional services. Note a class token that `make` could
241
+ auto-build still reports `false` here until it's explicitly bound. See
242
+ [container](./container.md).
243
+
244
+ ### `events()`
245
+
246
+ `events(): Events`
247
+
248
+ Returns the application's event emitter singleton.
249
+
250
+ ```ts
251
+ events().listenerCount("order.paid");
252
+ events().clear("order.paid");
253
+ ```
254
+
255
+ **Notes:** use for `once`, `off`, `listenerCount`, and `clear`; for the common
256
+ subscribe/emit pair prefer `listen`/`emit` below. See [events](./events.md).
257
+
258
+ ### `emit(event, payload?)`
259
+
260
+ `emit<T = unknown>(event: string, payload?: T): Promise<void>`
261
+
262
+ Emits an event, awaiting every listener in registration order.
263
+
264
+ ```ts
265
+ await emit("user.registered", user);
266
+ ```
267
+
268
+ **Notes:** shorthand for `events().emit(…)`. The returned promise resolves once
269
+ all listeners (including async ones) have run. No listeners → resolves
270
+ immediately. See [events](./events.md).
271
+
272
+ ### `listen(event, listener)`
273
+
274
+ `listen<T = unknown>(event: string, listener: Listener<T>): () => void`
275
+
276
+ Subscribes to an event; returns an unsubscribe function.
277
+
278
+ ```ts
279
+ const off = listen("user.registered", (user) => sendWelcome(user));
280
+ off(); // stop listening
281
+ ```
282
+
283
+ **Notes:** shorthand for `events().on(…)`. The listener may be sync or async.
284
+ Call the returned function to remove it. See [events](./events.md).
285
+
286
+ ### `cache()`
287
+
288
+ `cache(): Cache`
289
+
290
+ Returns the application's cache singleton.
291
+
292
+ ```ts
293
+ const stats = await cache().remember("stats", 60, () => computeStats());
294
+ await cache().put("otp", code, 300);
295
+ ```
296
+
297
+ **Notes:** memory-backed per process/isolate by default; swap the store via a
298
+ `singleton(Cache, …)` binding. See [cache](./cache.md).
299
+
300
+ ### `logger()`
301
+
302
+ `logger(): Logger`
303
+
304
+ Returns the application's logger singleton.
305
+
306
+ ```ts
307
+ logger().info("user registered", { userId: user.id });
308
+ logger().error("payment failed", { orderId });
309
+ ```
310
+
311
+ **Notes:** structured JSON by default. `logger().child({ … })` returns a logger
312
+ with bound fields (e.g. a request id). See [logger](./logger.md).
313
+
314
+ ### Interfaces & types
315
+
316
+ The helpers surface a few types from the services they front. You implement or
317
+ pass these; you rarely construct them here.
318
+
319
+ #### `Listener<T>`
320
+
321
+ `type Listener<T = unknown> = (payload: T) => void | Promise<void>`
322
+
323
+ The shape of a function passed to `listen`. Sync or async; the payload type
324
+ flows from `listen<T>`.
325
+
326
+ ```ts
327
+ const onOrder: Listener<{ id: number }> = async (order) => fulfil(order.id);
328
+ listen("order.paid", onOrder);
329
+ ```
330
+
331
+ #### `Token<T>` / `Factory<T>`
332
+
333
+ `type Token<T = unknown> = string | symbol | Constructor<T>`
334
+ `type Factory<T> = (app: Container) => T`
335
+
336
+ The key and the factory used by `bind`/`singleton`/`instance`/`make`/`bound`. A
337
+ token is a string, symbol, or class constructor; a factory receives the container
338
+ so it can resolve its own dependencies. Documented in full under
339
+ [container](./container.md).
340
+
341
+ #### `Renderable`
342
+
343
+ `type Renderable = string | Promise<string> | { toString(): string | Promise<string> } | null | undefined`
344
+
345
+ What a component passed to `view()` may return — a string, a JSX node, a promise
346
+ of either, or nullish (renders empty). Documented in full under
347
+ [views](./views.md).
package/docs/hono.md ADDED
@@ -0,0 +1,186 @@
1
+ # Built on Hono
2
+
3
+ Keel's HTTP layer **is** [Hono](https://hono.dev) — an ultrafast, web-standard
4
+ router that runs on Node, Cloudflare Workers, Deno, Bun, and more. Keel adds the
5
+ container, providers, routing sugar, and helpers on top; everything Hono can do
6
+ is available to you underneath.
7
+
8
+ Keel's convenience helpers (`json()`, `param()`, `request`, `response`, `view()`)
9
+ are thin wrappers over Hono's context. You never have to use them — you can
10
+ always take the context (`c`) directly and use the full Hono API.
11
+
12
+ ## What Hono provides, what Keel adds
13
+
14
+ The division of labor is worth holding in your head, because it tells you which
15
+ docs to reach for:
16
+
17
+ | Concern | Owned by |
18
+ |---------|----------|
19
+ | The `fetch` handler, request matching, method routing | **Hono** |
20
+ | `Context` — `c.req`, `c.json`, `c.html`, cookies, headers | **Hono** |
21
+ | JSX rendering (`hono/jsx`), streaming, SSE, WebSockets | **Hono** |
22
+ | Runtime adapters (Node, Workers, Deno, Bun, Lambda) | **Hono** |
23
+ | The service container, providers, config, the console | **Keel** |
24
+ | Fluent routing: names, groups, resources, param matchers, URL generation | **Keel** ([routing](./routing.md)) |
25
+ | `[Controller, method]` handlers resolved from the container | **Keel** ([controllers](./controllers.md)) |
26
+ | Request/response helpers that reach `c` without threading it | **Keel** ([request & response](./request-response.md)) |
27
+ | `view()`, error rendering, exceptions, validation | **Keel** |
28
+
29
+ Hono is the engine; Keel is the wheelhouse. Keel never hides Hono — it sits
30
+ beside it. When Keel wraps a Hono feature it's for ergonomics (fluent routes,
31
+ container DI, ambient helpers), and the raw feature is always one `c` away.
32
+
33
+ ## The context (`c`)
34
+
35
+ Every closure handler receives Hono's `Context`, and controller methods can too:
36
+
37
+ ```ts
38
+ router.get("/users/:id", (c) => {
39
+ c.req.param("id"); // route param
40
+ c.req.query("q"); // query string
41
+ c.req.header("authorization");
42
+ await c.req.json(); // parse a JSON body
43
+
44
+ return c.json({ ok: true }); // c.text() · c.html() · c.body() · c.redirect()
45
+ });
46
+ ```
47
+
48
+ Keel's `Ctx` type is exactly Hono's `Context` — it's a re-export, not a wrapper:
49
+
50
+ ```ts
51
+ import type { Ctx } from "@shaferllc/keel/core";
52
+ // type Ctx = import("hono").Context
53
+ ```
54
+
55
+ So a Keel handler and a Hono handler have the identical signature. Anything that
56
+ accepts a Hono `Context` accepts a Keel `Ctx`, and vice versa — there is no
57
+ adapter, boxing, or conversion between the two. When a guide says "the request
58
+ context," it means this object.
59
+
60
+ Common context surface: `c.req.{param, query, header, json, parseBody, valid, path, method, url, raw}`,
61
+ `c.{json, text, html, body, redirect, status, header, notFound}`, `c.set/c.get`
62
+ for request-scoped variables, and on Workers `c.env` (bindings like D1/KV/R2) and
63
+ `c.executionCtx` (`waitUntil`). Full reference:
64
+ [hono.dev/docs/api/context](https://hono.dev/docs/api/context).
65
+
66
+ Keel does set a few request-scoped variables of its own on the context, which
67
+ you can read with `c.get(...)`:
68
+
69
+ - `c.get("app")` — the service container for this request.
70
+ - `c.get("route")` — the matched route (`{ name, pattern, methods }`).
71
+ - `c.get("subdomains")` — captured subdomain params on domain-bound routes.
72
+
73
+ These are exactly what the ambient [request helpers](./request-response.md) read
74
+ under the hood. That's the trade: the helpers are terse and don't need `c`
75
+ passed around, but they only work inside a request; `c` is explicit and works
76
+ anywhere you're handed it.
77
+
78
+ ## Hono middleware works as-is
79
+
80
+ Any Hono middleware — built-in or third-party — drops straight into Keel's kernel
81
+ or onto a route, because Keel middleware **is** Hono middleware:
82
+
83
+ ```ts
84
+ import { cors } from "hono/cors";
85
+ import { secureHeaders } from "hono/secure-headers";
86
+ import { compress } from "hono/compress";
87
+
88
+ // app/Http/Kernel.ts
89
+ this.use(cors());
90
+ this.use(secureHeaders());
91
+ this.use(compress());
92
+ ```
93
+
94
+ Hono ships CORS, Secure Headers, Body Limit, Cache, Compress, ETag, Basic/Bearer
95
+ Auth, JWT, Logger, and more — see
96
+ [hono.dev/docs/middleware/builtin](https://hono.dev/docs/middleware/builtin).
97
+ Keel just gives you nicer places to attach them: global in the kernel, or
98
+ per-route and per-group via the fluent router. [Middleware](./middleware.md)
99
+ covers the ordering and named-middleware conveniences Keel layers on top.
100
+
101
+ ## What else you get from Hono
102
+
103
+ Because Keel is Hono underneath, these are all available directly:
104
+
105
+ | Hono feature | Use it in Keel |
106
+ |--------------|----------------|
107
+ | **JSX** | Keel [views](./views.md) are Hono JSX (`hono/jsx`) |
108
+ | **Cookies** | `hono/cookie`; Keel wraps common cases in `request.cookie` / `response.cookie` |
109
+ | **Streaming / SSE** | `hono/streaming` — return a streamed `Response` from a handler |
110
+ | **WebSockets** | Hono's upgrade helpers on supported runtimes |
111
+ | **Testing** | `hono.request(path, init)` — exactly what Keel's own test suite uses |
112
+ | **Validators / RPC** | `hono/validator`, the `hc` typed client |
113
+ | **Runtime adapters** | Node (`@hono/node-server`), Workers, Deno, Bun, Lambda |
114
+
115
+ [Views](./views.md) are the clearest example of Keel building on a Hono
116
+ primitive: a Keel view is a Hono JSX function component, and `view()` just
117
+ renders it to a full HTML document through the `View` service. Drop the helper
118
+ and `return c.html(<Page />)` and you get the same result — Keel's version only
119
+ adds the doctype and props typing.
120
+
121
+ ## When to drop to raw Hono
122
+
123
+ Reach for `c` and the Hono API directly when:
124
+
125
+ - You need a context feature Keel doesn't wrap — `c.executionCtx.waitUntil`,
126
+ streaming responses, content negotiation beyond the helpers, Workers bindings
127
+ on `c.env`.
128
+ - You're pulling in a Hono (or Hono-ecosystem) middleware — it already speaks
129
+ the native `Context`, so hand it `c` unchanged.
130
+ - You want the typed RPC client (`hc`) or `hono/validator`'s `c.req.valid(...)`.
131
+
132
+ Reach for Keel's helpers and fluent router when you want named routes, groups,
133
+ resource routes, container-resolved controllers, or the ambient
134
+ `request`/`response` accessors. The two mix freely in the same handler — start
135
+ with Keel's ergonomics and drop to `c` for the exact spot that needs it. Nothing
136
+ you do at the Hono level is "off the map"; it's the same object either way.
137
+
138
+ ## Reaching the Hono app
139
+
140
+ The HTTP kernel compiles your routes onto a Hono instance and returns it —
141
+ that's the `fetch` handler you serve (Node) or export (Workers). If you need to
142
+ attach something at the Hono level, do it where you build the kernel:
143
+
144
+ ```ts
145
+ const hono = new Kernel(app).build(); // a Hono instance
146
+ // hono.get(...), hono.use(...), export default hono, serve({ fetch: hono.fetch })
147
+ ```
148
+
149
+ Because `build()` hands back a plain Hono app, the same kernel serves every
150
+ runtime — `serve({ fetch: hono.fetch })` under Node, `export default hono` on
151
+ Workers. That single return value is the seam that keeps Keel edge-portable (see
152
+ [Architecture](./architecture.md#edge-safe-by-design)).
153
+
154
+ For anything HTTP-layer that Keel doesn't wrap yet, drop down to Hono — the docs
155
+ at [hono.dev](https://hono.dev/docs/) apply directly.
156
+
157
+ ## Serving over HTTP/2
158
+
159
+ There's nothing to configure in Keel for HTTP/2 — it's a transport concern, not a
160
+ framework one. Keel's handlers are Hono's fetch-based `Request`/`Response`, which
161
+ are HTTP-version-agnostic, so the protocol is decided entirely by whatever serves
162
+ the `fetch` handler:
163
+
164
+ - **On the edge** (Cloudflare Workers — the headline target), the platform
165
+ terminates HTTP/2 *and* HTTP/3 for you. Nothing to do, nothing to control from
166
+ app code.
167
+ - **In Node production**, HTTP/2 is almost always terminated at a reverse proxy
168
+ or CDN (nginx, Cloudflare, ALB) in front of the process — the usual setup.
169
+ - **In-process h2**, if you really want it, is a `@hono/node-server` option — hand
170
+ `serve()` a `node:http2` server. No Keel change:
171
+
172
+ ```ts
173
+ import { serve } from "@hono/node-server";
174
+ import { createSecureServer } from "node:http2";
175
+ import { readFileSync } from "node:fs";
176
+
177
+ const hono = new Kernel(app).build();
178
+ serve({
179
+ fetch: hono.fetch,
180
+ createServer: createSecureServer,
181
+ serverOptions: { key: readFileSync("key.pem"), cert: readFileSync("cert.pem") },
182
+ });
183
+ ```
184
+
185
+ So HTTP/2 is available on every Keel deployment without the framework
186
+ implementing anything — the same `fetch` handler just gets served over it.
package/docs/hooks.md ADDED
@@ -0,0 +1,118 @@
1
+ # Lifecycle Hooks
2
+
3
+ Tap into the **application lifecycle** — run code once the app is ready, clean up
4
+ on shutdown, and observe route registration. Inspired by
5
+ [Fastify's hooks](https://fastify.dev/docs/latest/Reference/Hooks/), scoped to the
6
+ parts Keel doesn't already cover.
7
+
8
+ > **Request-lifecycle hooks** (before/after a request, on error) are
9
+ > [middleware](./middleware.md) in Keel — `HttpKernel.use()`, route/group
10
+ > `.middleware()`, and `onError()`. This page is the *application* lifecycle:
11
+ > ready, shutdown, and route registration.
12
+
13
+ ## onReady
14
+
15
+ Run a callback once the application has finished booting (all providers
16
+ registered and booted). Register it before boot; if the app is already booted, it
17
+ runs immediately.
18
+
19
+ ```ts
20
+ import { onReady } from "@shaferllc/keel/core";
21
+
22
+ onReady(async (app) => {
23
+ await warmCaches();
24
+ logger().info("app ready");
25
+ });
26
+ ```
27
+
28
+ ## Graceful shutdown
29
+
30
+ `onShutdown` registers cleanup — closing database/Redis connections, flushing
31
+ queues, draining work. Hooks run **newest-first (LIFO)** when you call
32
+ `terminate()`, so teardown unwinds in the reverse order things were set up:
33
+
34
+ ```ts
35
+ import { onShutdown, terminate } from "@shaferllc/keel/core";
36
+
37
+ onShutdown(async () => {
38
+ await db().close?.();
39
+ await redis().flushAll();
40
+ });
41
+ ```
42
+
43
+ **`keel serve` already traps SIGINT and SIGTERM**, stops accepting connections,
44
+ and calls `terminate()` for you — so the hook above just runs. You only wire
45
+ signals by hand in a **custom entrypoint** that doesn't go through `keel serve`:
46
+
47
+ ```ts
48
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
49
+ process.on(signal, async () => {
50
+ await terminate(); // runs every shutdown hook
51
+ process.exit(0);
52
+ });
53
+ }
54
+ ```
55
+
56
+ A [service provider](./providers.md)'s `shutdown()` method joins the same queue,
57
+ so provider teardown and hand-registered `onShutdown` hooks unwind together.
58
+ `terminate()` is **idempotent** — a second call does nothing. A hook that throws
59
+ doesn't stop the others; the first error is re-thrown after all have run, so one
60
+ failing cleanup can't strand the rest.
61
+
62
+ ## onRoute
63
+
64
+ Observe routes as they're registered — for request logging, an API map, or
65
+ metrics. The hook is called for each route added *after* registration, and
66
+ replayed for routes already registered, so you see them all regardless of order:
67
+
68
+ ```ts
69
+ const router = app.make(Router);
70
+
71
+ router.onRoute((def) => {
72
+ logger().debug("route", { methods: def.methods, path: def.path, name: def.name });
73
+ });
74
+ ```
75
+
76
+ The `def` is the live route definition, so reading it later reflects fluent
77
+ config applied after `add()` — `.name()`, `.middleware()`, and so on.
78
+
79
+ ## API reference
80
+
81
+ ### `onReady(hook)`
82
+
83
+ `onReady(hook: (app: Application) => void | Promise<void>): void`
84
+
85
+ Global helper — registers a ready hook on the active application. Runs after boot
86
+ (or immediately if already booted). Also available as `app.onReady(hook)`.
87
+
88
+ ### `onShutdown(hook)`
89
+
90
+ `onShutdown(hook: (app: Application) => void | Promise<void>): void`
91
+
92
+ Registers a shutdown hook on the active application. Also `app.onShutdown(hook)`.
93
+
94
+ ### `terminate()`
95
+
96
+ `terminate(): Promise<void>`
97
+
98
+ Gracefully shuts the active application down — runs every shutdown hook LIFO.
99
+ Idempotent. Re-throws the first hook error after running all. Also
100
+ `app.terminate()`.
101
+
102
+ ### `Application.onReady` / `onShutdown` / `terminate`
103
+
104
+ The same three as methods on the `Application`, returning `this` (chainable) for
105
+ `onReady`/`onShutdown`. `app.isTerminated` reports whether `terminate()` has run.
106
+
107
+ ### `Router.onRoute(hook)`
108
+
109
+ `onRoute(hook: (def: RouteDefinition) => void): this`
110
+
111
+ Called with each route's definition as it's registered, and replayed for existing
112
+ routes. Chainable.
113
+
114
+ ### `LifecycleHook`
115
+
116
+ `type LifecycleHook = (app: Application) => void | Promise<void>`
117
+
118
+ The signature of `onReady` / `onShutdown` hooks.