@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
package/docs/pages.md ADDED
@@ -0,0 +1,217 @@
1
+ # Pages
2
+
3
+ Page-based routing — **a file is a route**.
4
+
5
+ ```
6
+ resources/pages/index.tsx → /
7
+ resources/pages/about.tsx → /about
8
+ resources/pages/users/index.tsx → /users
9
+ resources/pages/users/[id].tsx → /users/:id
10
+ resources/pages/docs/[...slug].tsx → /docs/* (catch-all)
11
+ ```
12
+
13
+ ```tsx
14
+ // resources/pages/users/[id].tsx
15
+ import { db, type Ctx, type PageProps } from "@shaferllc/keel/core";
16
+
17
+ export const loader = (c: Ctx) => db("users").where("id", c.req.param("id")).first();
18
+
19
+ export default function UserPage({ params, data }: PageProps<{ id: string }, User>) {
20
+ return (
21
+ <Layout>
22
+ <h1>{data.name}</h1>
23
+ <p>User #{params.id}</p>
24
+ </Layout>
25
+ );
26
+ }
27
+ ```
28
+
29
+ That's the whole page. No route file to keep in sync, no controller, no wiring.
30
+
31
+ ## It doesn't replace the router — it drives it
32
+
33
+ Every page becomes an **ordinary named route**. `url()` finds it, route middleware
34
+ applies to it, and `keel routes` lists it. You can mix pages and hand-written
35
+ routes freely, and reach for a controller the moment a page outgrows a file.
36
+
37
+ That matters, because file-based routing is a lovely default and a bad prison.
38
+ Here it's a *convenience over* the router, not a replacement for it.
39
+
40
+ ## Registering them
41
+
42
+ In a service provider's `boot()`:
43
+
44
+ ```ts
45
+ import { pages } from "@shaferllc/keel/core";
46
+
47
+ export class PageServiceProvider extends ServiceProvider {
48
+ async boot(): Promise<void> {
49
+ await pages(); // scans resources/pages
50
+ }
51
+ }
52
+ ```
53
+
54
+ `pages()` reads the filesystem, so it's **Node-only**. On the edge, hand
55
+ `definePages()` a build-time manifest instead — Vite's `import.meta.glob` produces
56
+ exactly the map it wants:
57
+
58
+ ```ts
59
+ definePages(import.meta.glob("./pages/**/*.tsx", { eager: true }));
60
+ ```
61
+
62
+ Same behavior, no filesystem.
63
+
64
+ ## The file conventions
65
+
66
+ | File | URL |
67
+ |------|-----|
68
+ | `index.tsx` | `/` |
69
+ | `about.tsx` | `/about` |
70
+ | `users/index.tsx` | `/users` |
71
+ | `users/[id].tsx` | `/users/:id` |
72
+ | `users/[id]/edit.tsx` | `/users/:id/edit` |
73
+ | `teams/[team]/users/[id].tsx` | `/teams/:team/users/:id` |
74
+ | `docs/[...slug].tsx` | `/docs/*` — a catch-all; `params.slug` is the whole rest |
75
+ | `_layout.tsx` | **not a route** — a leading `_` keeps a file private |
76
+
77
+ A trailing `index` names its directory rather than a child of it. A leading
78
+ underscore is how layouts, partials, and helpers live *beside* your pages without
79
+ becoming URLs.
80
+
81
+ ## Specificity is decided for you
82
+
83
+ This is the part file-based routing usually gets wrong:
84
+
85
+ ```
86
+ users/[id].tsx → /users/:id
87
+ users/new.tsx → /users/new
88
+ ```
89
+
90
+ Register `:id` first and `/users/new` is **unreachable forever** — `:id` happily
91
+ matches `"new"`. Whether your app works would come down to the order the
92
+ filesystem happened to hand back.
93
+
94
+ So pages are **sorted before they're registered**: literal segments beat
95
+ parameters, parameters beat catch-alls, and a catch-all is always the last resort.
96
+ `/users/new` wins, and the file layout stops being a trap.
97
+
98
+ ## Loading data
99
+
100
+ `loader` runs before the page renders; whatever it returns arrives as `data`.
101
+
102
+ ```tsx
103
+ export const loader = async (c: Ctx) => {
104
+ const post = await db("posts").where("slug", c.req.param("slug")).first();
105
+ if (!post) throw new NotFoundException();
106
+ return post;
107
+ };
108
+
109
+ export default function PostPage({ data }: PageProps<{ slug: string }, Post>) {
110
+ return <article>{data.body}</article>;
111
+ }
112
+ ```
113
+
114
+ It takes the request context, so it can read params, query, headers, or the
115
+ session. A page with no `loader` simply gets `data: undefined`.
116
+
117
+ ## Middleware
118
+
119
+ Per page:
120
+
121
+ ```tsx
122
+ export const middleware = [authGuard()];
123
+ ```
124
+
125
+ Or for every page at once:
126
+
127
+ ```ts
128
+ await pages({ middleware: [authGuard()] });
129
+ ```
130
+
131
+ Both run before the `loader`, so a page that's refused never loads its data.
132
+
133
+ ## Names and URLs
134
+
135
+ Each page gets a route name derived from its path — `users/[id].tsx` becomes
136
+ `users.id` — so URL generation works without you naming anything:
137
+
138
+ ```ts
139
+ router.url("users.id", { id: 5 }); // "/users/5"
140
+ ```
141
+
142
+ Override it when the derived name is ugly:
143
+
144
+ ```tsx
145
+ export const name = "users.show";
146
+ ```
147
+
148
+ ## Escape hatches
149
+
150
+ Move a page's URL without moving the file:
151
+
152
+ ```tsx
153
+ export const path = "/pricing"; // even though the file is at marketing/plans.tsx
154
+ ```
155
+
156
+ Mount every page under a prefix:
157
+
158
+ ```ts
159
+ await pages({ prefix: "/app" }); // index.tsx is now /app
160
+ await pages({ dir: "app/pages" }); // ...or keep them somewhere else
161
+ ```
162
+
163
+ ---
164
+
165
+ ## API reference
166
+
167
+ ### `pages(options?)`
168
+
169
+ `pages(options?: PagesOptions): Promise<RegisteredPage[]>`
170
+
171
+ Scan a directory and register every page in it. **Node only** — it reads the
172
+ filesystem (`node:fs` is imported dynamically, so the core still loads on the
173
+ edge).
174
+
175
+ ### `definePages(modules, options?)`
176
+
177
+ `definePages(modules: Record<string, PageModule>, options?: PagesOptions): RegisteredPage[]`
178
+
179
+ Register pages from a `file path → module` map. The edge-safe half — pair it with
180
+ `import.meta.glob`.
181
+
182
+ ### `PagesOptions`
183
+
184
+ | Option | Meaning |
185
+ |--------|---------|
186
+ | `dir` | Where the pages live. Default `"resources/pages"` |
187
+ | `prefix` | Prefix every page's URL |
188
+ | `middleware` | Middleware applied to every page |
189
+ | `router` | The router to register on. Defaults to the application's |
190
+
191
+ ### `PageModule`
192
+
193
+ What a page file exports.
194
+
195
+ | Export | Meaning |
196
+ |--------|---------|
197
+ | `default` | **Required.** The component. May be async |
198
+ | `loader` | `(ctx) => data` — runs before the page renders |
199
+ | `middleware` | Middleware for this page alone |
200
+ | `name` | The route name. Defaults to one derived from the path |
201
+ | `path` | Override the URL entirely |
202
+
203
+ ### `PageProps<P, D>`
204
+
205
+ What the component receives: `params` (typed by `P`), `data` (whatever `loader`
206
+ returned, typed by `D`), and `ctx`.
207
+
208
+ ### `RegisteredPage`
209
+
210
+ `{ file, pattern, name }` — what `pages()` / `definePages()` return, so you can
211
+ see exactly what got mounted.
212
+
213
+ ### `routePattern(file)` / `routeName(file)`
214
+
215
+ The two pure functions behind the conventions, exported so you can test or reuse
216
+ them. `routePattern("users/[id].tsx")` → `"/users/:id"`;
217
+ `routeName("users/[id].tsx")` → `"users.id"`.
@@ -0,0 +1,363 @@
1
+ # Service Providers
2
+
3
+ Service providers are the central place to configure your application. Nearly
4
+ everything Keel boots — config, routing, your own services — is wired up in a
5
+ provider.
6
+
7
+ ## The lifecycle
8
+
9
+ A provider has four hooks, run across the application's lifecycle. Only
10
+ `register()` and `boot()` are needed day-to-day; `ready()` and `shutdown()` are
11
+ there for work that must happen once the whole app is live, or as it stops:
12
+
13
+ ```ts
14
+ import { ServiceProvider } from "@shaferllc/keel/core";
15
+
16
+ export class AppServiceProvider extends ServiceProvider {
17
+ register(): void {
18
+ // Phase 1. Bind things into the container.
19
+ // Do NOT resolve other services here — nothing is guaranteed
20
+ // to be registered yet.
21
+ }
22
+
23
+ boot(): void {
24
+ // Phase 2. Runs after EVERY provider has registered.
25
+ // Safe to resolve services and wire them together.
26
+ }
27
+
28
+ ready(): void {
29
+ // Phase 3. Runs after every provider has booted and the app is fully up.
30
+ // For work that needs a live app — warm a cache, attach to the server.
31
+ }
32
+
33
+ shutdown(): void {
34
+ // Phase 4. Runs on graceful termination, in reverse registration order.
35
+ // Close connections, flush queues, cancel timers.
36
+ }
37
+ }
38
+ ```
39
+
40
+ The `Application` runs **all** `register()` methods first, then **all** `boot()`
41
+ methods, then **all** `ready()` methods — that ordering is what lets providers
42
+ depend on each other without worrying about load order. On `app.terminate()`
43
+ (wired to SIGINT/SIGTERM by `keel serve`), every `shutdown()` runs in **reverse**
44
+ registration order (LIFO), so a provider tears down before the ones it depended
45
+ on. All four hooks may be `async` — the application awaits them. `ready()` and
46
+ `shutdown()` are optional and map onto the app's `onReady()` / `onShutdown()`
47
+ hooks, so a provider's `shutdown()` runs alongside any it registered by hand.
48
+
49
+ ## The `app` reference
50
+
51
+ Every provider is handed the `Application` at construction and holds it as
52
+ `this.app` (a `protected` field). The `Application` **is** the service container,
53
+ so `this.app` gives you `bind`, `singleton`, `instance`, `make`, and `bound`
54
+ directly, plus the framework accessors `config()`, `router()`, and `view()`:
55
+
56
+ ```ts
57
+ import { ServiceProvider } from "@shaferllc/keel/core";
58
+ import { SearchIndex } from "../Services/SearchIndex.js";
59
+
60
+ export class SearchServiceProvider extends ServiceProvider {
61
+ register(): void {
62
+ this.app.singleton("search", () => new SearchIndex());
63
+ }
64
+
65
+ boot(): void {
66
+ const debug = this.app.config().get("app.debug", false);
67
+ this.app.router().get("/health", () => "ok");
68
+ }
69
+ }
70
+ ```
71
+
72
+ You never construct a provider yourself — the `Application` does it for you when
73
+ you register the class. It calls `new Provider(app)`, so `this.app` is always the
74
+ live application instance.
75
+
76
+ ## Registering a provider
77
+
78
+ Add your provider class to `bootstrap/providers.ts`:
79
+
80
+ ```ts
81
+ import type { ProviderClass } from "@shaferllc/keel/core";
82
+ import { AppServiceProvider } from "../app/Providers/AppServiceProvider.js";
83
+ import { BillingServiceProvider } from "../app/Providers/BillingServiceProvider.js";
84
+
85
+ export const providers: ProviderClass[] = [
86
+ AppServiceProvider,
87
+ BillingServiceProvider,
88
+ ];
89
+ ```
90
+
91
+ Providers boot in array order. Under the hood, `app.boot(providers)` calls
92
+ `app.register(Provider)` for each class — which does `new Provider(app)` and
93
+ stashes the instance — then runs the two phases across the whole set.
94
+
95
+ ## Providers are Keel's plugin system
96
+
97
+ A service provider is Keel's answer to a **plugin**: a self-contained slice of
98
+ functionality you register into the app. To make one **reusable**, register it
99
+ with **options** — they arrive as `this.options`, typed via the generic:
100
+
101
+ ```ts
102
+ class RateLimitProvider extends ServiceProvider<{ max: number }> {
103
+ boot() {
104
+ this.app.make(HttpKernel).use(rateLimiter({ max: this.options.max }));
105
+ }
106
+ }
107
+
108
+ app.register(RateLimitProvider, { max: 100 }); // parameterized, like a plugin
109
+ ```
110
+
111
+ The same provider class can be registered more than once with different options.
112
+ Without options, `this.options` is an empty object.
113
+
114
+ > Unlike Fastify plugins, Keel providers are **not encapsulated** — bindings,
115
+ > decorators, and routes are registered into the one global container. That's a
116
+ > deliberate simplification: there's a single, predictable scope, and no
117
+ > plugin-boundary rules to reason about. For per-request behavior in the HTTP
118
+ > pipeline (auth, logging, etc.), reach for [middleware](./middleware.md), which
119
+ > *is* scoped to the routes you attach it to.
120
+
121
+ ## Generating a provider
122
+
123
+ ```bash
124
+ npm run keel make:provider Billing
125
+ ```
126
+
127
+ Writes `app/Providers/BillingServiceProvider.ts`. Remember to add it to
128
+ `bootstrap/providers.ts`.
129
+
130
+ ## A realistic example
131
+
132
+ ```ts
133
+ import { ServiceProvider, Config } from "@shaferllc/keel/core";
134
+ import { StripeClient } from "../Services/StripeClient.js";
135
+
136
+ export class BillingServiceProvider extends ServiceProvider {
137
+ register(): void {
138
+ this.app.singleton(StripeClient, (app) => {
139
+ const key = app.make(Config).get<string>("services.stripe.key");
140
+ return new StripeClient(key);
141
+ });
142
+ }
143
+
144
+ boot(): void {
145
+ // e.g. register webhooks, warm a cache, etc.
146
+ }
147
+ }
148
+ ```
149
+
150
+ Now any controller or service can `this.app.make(StripeClient)` and get the same
151
+ configured instance.
152
+
153
+ ## Async providers
154
+
155
+ Both phases may return a promise, and the application `await`s each one before
156
+ moving on. Reach for this when a binding needs to open a connection or fetch a
157
+ remote manifest:
158
+
159
+ ```ts
160
+ import { ServiceProvider } from "@shaferllc/keel/core";
161
+ import { SearchClient } from "../Services/SearchClient.js";
162
+
163
+ export class SearchServiceProvider extends ServiceProvider {
164
+ async register(): Promise<void> {
165
+ const client = await SearchClient.connect(process.env.SEARCH_URL!);
166
+ this.app.instance(SearchClient, client);
167
+ }
168
+
169
+ async boot(): Promise<void> {
170
+ await this.app.make(SearchClient).warm();
171
+ }
172
+ }
173
+ ```
174
+
175
+ Because every `register()` is awaited before any `boot()` runs, an async
176
+ `register()` in one provider still completes before another provider's `boot()`
177
+ tries to resolve what it bound.
178
+
179
+ ## Error behavior
180
+
181
+ `register()` and `boot()` run inside `app.boot()`, which awaits each call in
182
+ sequence. If any provider throws (or rejects), `app.boot()` rejects and the
183
+ remaining providers never run — so a bad binding fails the whole boot loudly
184
+ rather than leaving a half-wired container. Booting is also idempotent: once
185
+ `app.boot()` has completed, calling it again returns immediately without
186
+ re-running any provider.
187
+
188
+ ## Rules of thumb
189
+
190
+ - **`register()` binds. `boot()` uses.** Resolving a service in `register()` is
191
+ the most common mistake — the thing you need may not be bound yet.
192
+ - **Keep providers focused.** One provider per concern (billing, auth, search)
193
+ reads better than one giant `AppServiceProvider`.
194
+ - **Order matters only for `boot()` side effects**, since all registration
195
+ happens before any booting.
196
+
197
+ ## Related
198
+
199
+ Providers wire services into the [container](./container.md); the
200
+ [Application](./architecture.md) kernel constructs and boots them. See
201
+ [configuration](./configuration.md) for what `this.app.config()` reads.
202
+
203
+ ---
204
+
205
+ ## API reference
206
+
207
+ ### `ServiceProvider`
208
+
209
+ The abstract base class every provider extends. You never instantiate it
210
+ directly — you subclass it and register the subclass (via `bootstrap/providers.ts`
211
+ or `app.register()`), and the `Application` constructs it for you. Override
212
+ `register()` and/or `boot()`; both are no-ops by default.
213
+
214
+ ```ts
215
+ import { ServiceProvider } from "@shaferllc/keel/core";
216
+
217
+ export class AppServiceProvider extends ServiceProvider {
218
+ register(): void {
219
+ this.app.bind("clock", () => new Date().toISOString());
220
+ }
221
+ }
222
+ ```
223
+
224
+ **Notes:** `abstract`, so it can't be `new`ed on its own. A subclass needn't
225
+ override both methods — leave one off to inherit the empty default.
226
+
227
+ #### `constructor(app, options?)`
228
+
229
+ `constructor(app: Application, options?: O)` (on `ServiceProvider<O>`)
230
+
231
+ Receives the live `Application` (stored as `protected this.app`) and the options
232
+ passed to `register` (stored as `protected this.options`, `{}` if none). The
233
+ `Application` invokes this for you; you don't call it.
234
+
235
+ ```ts
236
+ class RateLimitProvider extends ServiceProvider<{ max: number }> {
237
+ boot() {
238
+ this.app.make(HttpKernel).use(rateLimiter({ max: this.options.max }));
239
+ }
240
+ }
241
+ app.register(RateLimitProvider, { max: 100 });
242
+ ```
243
+
244
+ **Notes:** `app` and `options` are `protected` — reachable from subclass methods,
245
+ not from outside. Type the options via the class generic `ServiceProvider<O>`.
246
+
247
+ #### `app.register(Provider, options?)`
248
+
249
+ `register(Provider: ProviderClass, options?: unknown): this`
250
+
251
+ Registers a provider, optionally with options handed to its constructor. Chainable.
252
+ `app.boot([Providers])` registers each without options.
253
+
254
+ #### `register()`
255
+
256
+ `register(): void | Promise<void>`
257
+
258
+ Phase-one hook: bind services into the container. Called for every provider
259
+ before any `boot()` runs. Default implementation is empty.
260
+
261
+ ```ts
262
+ register(): void {
263
+ this.app.singleton(StripeClient, (app) =>
264
+ new StripeClient(app.make(Config).get<string>("services.stripe.key")),
265
+ );
266
+ }
267
+ ```
268
+
269
+ **Notes:** do **not** resolve other services here — another provider may not have
270
+ bound them yet. May be `async`; the application awaits it. Throwing rejects
271
+ `app.boot()`.
272
+
273
+ #### `boot()`
274
+
275
+ `boot(): void | Promise<void>`
276
+
277
+ Phase-two hook: runs after **every** provider has registered, so it's safe to
278
+ resolve services and wire them together. Default implementation is empty.
279
+
280
+ ```ts
281
+ async boot(): Promise<void> {
282
+ await this.app.make(SearchClient).warm();
283
+ }
284
+ ```
285
+
286
+ **Notes:** providers boot in registration (array) order — the only place order
287
+ matters, since all `register()` calls finish first. May be `async`; the
288
+ application awaits it. Throwing rejects `app.boot()`.
289
+
290
+ #### `ready()`
291
+
292
+ `ready(): void | Promise<void>`
293
+
294
+ Phase-three hook: runs after **every** provider has booted and the app is fully
295
+ up (after the app's own `onReady` hooks). For work that needs a live app —
296
+ warming a cache, attaching to the running server. Default implementation is
297
+ empty.
298
+
299
+ ```ts
300
+ async ready(): Promise<void> {
301
+ await this.app.make(Cache).warm(["home", "pricing"]);
302
+ }
303
+ ```
304
+
305
+ **Notes:** runs in registration order, once, at the end of `app.boot()`. Optional
306
+ — omit it and nothing runs. May be `async`; the application awaits it.
307
+
308
+ #### `shutdown()`
309
+
310
+ `shutdown(): void | Promise<void>`
311
+
312
+ Cleanup hook: runs on `app.terminate()` (which `keel serve` wires to SIGINT and
313
+ SIGTERM) in **reverse** registration order. Close database/Redis connections,
314
+ flush logs, cancel timers. Default implementation is empty.
315
+
316
+ ```ts
317
+ async shutdown(): Promise<void> {
318
+ await this.app.make(Redis).quit();
319
+ }
320
+ ```
321
+
322
+ **Notes:** LIFO — the last provider registered shuts down first, so a provider
323
+ tears down before the ones it depends on. It joins the app's `onShutdown` hooks,
324
+ so a hook a provider registered by hand and its `shutdown()` both run. A throw
325
+ doesn't stop the others; the first error is re-thrown after all have run.
326
+
327
+ #### `app` (protected property)
328
+
329
+ `protected app: Application`
330
+
331
+ The application/container this provider configures. Use it in `register()` to
332
+ bind and in `boot()` to resolve.
333
+
334
+ ```ts
335
+ boot(): void {
336
+ const level = this.app.config().get("logger.level", "info");
337
+ }
338
+ ```
339
+
340
+ **Notes:** it's the same `Application` instance across every provider, exposing
341
+ the container methods (`bind`, `singleton`, `instance`, `make`, `bound`) plus
342
+ `config()`, `router()`, and `view()`. Being `protected`, it's only visible to
343
+ subclass code.
344
+
345
+ ### Interfaces & types
346
+
347
+ #### `ProviderClass`
348
+
349
+ `type ProviderClass = new (app: Application) => ServiceProvider`
350
+
351
+ The constructor type of a provider — a class (not an instance) that takes an
352
+ `Application` and yields a `ServiceProvider`. Use it to type the array you export
353
+ from `bootstrap/providers.ts` and anywhere you pass provider classes around.
354
+
355
+ ```ts
356
+ import type { ProviderClass } from "@shaferllc/keel/core";
357
+ import { AppServiceProvider } from "../app/Providers/AppServiceProvider.js";
358
+
359
+ export const providers: ProviderClass[] = [AppServiceProvider];
360
+ ```
361
+
362
+ **Notes:** it references the class itself, so entries are the class name with no
363
+ `new` and no parentheses. `app.register()` and `app.boot()` both accept these.