@shaferllc/keel 0.83.0 → 0.83.1

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.
package/README.md CHANGED
@@ -160,7 +160,7 @@ src/core/ The framework
160
160
  src/db/ Database adapters (D1, Postgres, libSQL)
161
161
  src/api/ CRUD REST resources from a model
162
162
  src/openapi/ Generates an OpenAPI spec from the routes
163
- src/billing/ Subscription billing — Stripe + Paddle (Cashier port)
163
+ src/billing/ Subscription billing — Stripe + Paddle
164
164
  src/watch/ The debug dashboard
165
165
  src/mcp/ The MCP server (docs + API for AI agents)
166
166
  src/vite/ The Vite plugin
@@ -228,7 +228,7 @@ See [docs/architecture.md](./docs/architecture.md) for the full picture.
228
228
  | [Internationalization](./docs/i18n.md) | ICU messages, `Intl` formatters, locale detection |
229
229
  | [Pages](./docs/pages.md) | Page-based routing — a file is a route |
230
230
  | [Packages](./docs/packages.md) | Redistributable slices of an app: routes, migrations, commands |
231
- | [Billing](./docs/billing.md) | Subscriptions, charges & webhooks — Stripe + Paddle (Cashier port) |
231
+ | [Billing](./docs/billing.md) | Subscriptions, charges & webhooks — Stripe + Paddle |
232
232
  | [Watch](./docs/watch.md) | Debug dashboard — requests, queries, jobs, logs at `/watch` |
233
233
  | [Views](./docs/views.md) | Hono JSX components, layouts, the View service |
234
234
  | [Templates](./docs/templates.md) | `{{ }}` + `@`-tag templating engine, edge-safe |
@@ -1,7 +1,6 @@
1
1
  /**
2
- * The `Billable` mixin — Cashier's Billable trait, as a TypeScript mixin. Apply
3
- * it to a model to give that model a gateway customer, subscriptions, charges,
4
- * and checkout:
2
+ * The `Billable` mixin — apply it to a model to give that model a gateway
3
+ * customer, subscriptions, charges, and checkout:
5
4
  *
6
5
  * class User extends Billable(Model) {
7
6
  * static table = "users";
@@ -1,7 +1,6 @@
1
1
  /**
2
- * The `Billable` mixin — Cashier's Billable trait, as a TypeScript mixin. Apply
3
- * it to a model to give that model a gateway customer, subscriptions, charges,
4
- * and checkout:
2
+ * The `Billable` mixin — apply it to a model to give that model a gateway
3
+ * customer, subscriptions, charges, and checkout:
5
4
  *
6
5
  * class User extends Billable(Model) {
7
6
  * static table = "users";
@@ -1,7 +1,7 @@
1
1
  import { env } from "@shaferllc/keel/core";
2
2
 
3
3
  /**
4
- * Billing configuration — a Cashier-style port covering Stripe and Paddle.
4
+ * Billing configuration — subscriptions and charges, covering Stripe and Paddle.
5
5
  * Published with `keel vendor:publish --tag billing-config`.
6
6
  */
7
7
  export default {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The fluent subscription builder — Cashier's `newSubscription(...)->create()`.
3
- * The `Billable` mixin hands it a small `BillableTarget` (so this file needn't
2
+ * The fluent subscription builder — `newSubscription(...).create()`. The
3
+ * `Billable` mixin hands it a small `BillableTarget` (so this file needn't
4
4
  * import the mixin, avoiding a cycle); the builder shapes the request, calls the
5
5
  * gateway, and persists a local `Subscription` synced from the result.
6
6
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The fluent subscription builder — Cashier's `newSubscription(...)->create()`.
3
- * The `Billable` mixin hands it a small `BillableTarget` (so this file needn't
2
+ * The fluent subscription builder — `newSubscription(...).create()`. The
3
+ * `Billable` mixin hands it a small `BillableTarget` (so this file needn't
4
4
  * import the mixin, avoiding a cycle); the builder shapes the request, calls the
5
5
  * gateway, and persists a local `Subscription` synced from the result.
6
6
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Keel Billing — a Cashier-style port covering Stripe and Paddle, shipped as a
2
+ * Keel Billing — subscription billing covering Stripe and Paddle, shipped as a
3
3
  * Keel package. One line in `bootstrap/providers.ts` turns it on:
4
4
  *
5
5
  * app.register(BillingServiceProvider)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Keel Billing — a Cashier-style port covering Stripe and Paddle, shipped as a
2
+ * Keel Billing — subscription billing covering Stripe and Paddle, shipped as a
3
3
  * Keel package. One line in `bootstrap/providers.ts` turns it on:
4
4
  *
5
5
  * app.register(BillingServiceProvider)
@@ -29,7 +29,7 @@ export class BillingServiceProvider extends PackageProvider {
29
29
  registerDefaultGateways(this.manager);
30
30
  setBilling(this.manager);
31
31
  this.app.instance(BillingManager, this.manager);
32
- // Cashier targets the standard `users` billable table.
32
+ // Default billable table is `users`.
33
33
  this.migrations([billingMigration("users")]);
34
34
  this.publishes({ [join(here, "billing.config.stub")]: "config/billing.ts" }, "billing-config");
35
35
  }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * One line of a subscription — a single price and its quantity. A single-price
3
3
  * subscription has exactly one item; a multi-product subscription has several.
4
- * Mirrors Cashier's `subscription_items` table.
5
4
  */
6
5
  import { Model } from "../core/model.js";
7
6
  import type { Casts } from "../core/casts.js";
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * One line of a subscription — a single price and its quantity. A single-price
3
3
  * subscription has exactly one item; a multi-product subscription has several.
4
- * Mirrors Cashier's `subscription_items` table.
5
4
  */
6
5
  import { Model } from "../core/model.js";
7
6
  export class SubscriptionItem extends Model {
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Request decorators — attach named, computed values to the current request,
3
- * resolved lazily and memoized for the life of that request. Inspired by
4
- * Fastify's `decorateRequest`, but without its footguns: you register a resolver
5
- * once, and Keel computes it on first access and caches it per request (keyed off
6
- * the Hono context via a WeakMap, so there's no shared-state leak between
7
- * requests and no V8 shape deopt to design around).
3
+ * resolved lazily and memoized for the life of that request. You register a
4
+ * resolver once, and Keel computes it on first access and caches it per request
5
+ * (keyed off the Hono context via a WeakMap, so there's no shared-state leak
6
+ * between requests and no V8 shape deopt to design around).
8
7
  *
9
8
  * decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
10
9
  *
@@ -18,8 +17,7 @@ import type { Context } from "hono";
18
17
  /** Resolves a decorator's value from the current request context. */
19
18
  export type RequestResolver<T = unknown> = (c: Context) => T | Promise<T>;
20
19
  /**
21
- * Register a request decorator. Throws if `name` is already registered — a
22
- * collision guard, like Fastify's.
20
+ * Register a request decorator. Throws if `name` is already registered.
23
21
  */
24
22
  export declare function decorateRequest<T>(name: string, resolver: RequestResolver<T>): void;
25
23
  /** Whether a request decorator has been registered. */
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Request decorators — attach named, computed values to the current request,
3
- * resolved lazily and memoized for the life of that request. Inspired by
4
- * Fastify's `decorateRequest`, but without its footguns: you register a resolver
5
- * once, and Keel computes it on first access and caches it per request (keyed off
6
- * the Hono context via a WeakMap, so there's no shared-state leak between
7
- * requests and no V8 shape deopt to design around).
3
+ * resolved lazily and memoized for the life of that request. You register a
4
+ * resolver once, and Keel computes it on first access and caches it per request
5
+ * (keyed off the Hono context via a WeakMap, so there's no shared-state leak
6
+ * between requests and no V8 shape deopt to design around).
8
7
  *
9
8
  * decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
10
9
  *
@@ -27,8 +26,7 @@ function bag(c) {
27
26
  return b;
28
27
  }
29
28
  /**
30
- * Register a request decorator. Throws if `name` is already registered — a
31
- * collision guard, like Fastify's.
29
+ * Register a request decorator. Throws if `name` is already registered.
32
30
  */
33
31
  export function decorateRequest(name, resolver) {
34
32
  if (resolvers.has(name)) {
@@ -92,9 +92,8 @@ export declare class ServiceUnavailableException extends HttpException {
92
92
  }
93
93
  /**
94
94
  * Mint a reusable, coded `HttpException` subclass — the ergonomic way to define
95
- * app-specific errors with a stable, machine-readable `code`. Inspired by
96
- * `@fastify/error`. The `message` may carry `%s` placeholders, filled in order
97
- * from the constructor arguments.
95
+ * app-specific errors with a stable, machine-readable `code`. The `message` may
96
+ * carry `%s` placeholders, filled in order from the constructor arguments.
98
97
  *
99
98
  * const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
100
99
  * throw new InsufficientFunds("$40");
@@ -196,9 +196,8 @@ export class ServiceUnavailableException extends HttpException {
196
196
  }
197
197
  /**
198
198
  * Mint a reusable, coded `HttpException` subclass — the ergonomic way to define
199
- * app-specific errors with a stable, machine-readable `code`. Inspired by
200
- * `@fastify/error`. The `message` may carry `%s` placeholders, filled in order
201
- * from the constructor arguments.
199
+ * app-specific errors with a stable, machine-readable `code`. The `message` may
200
+ * carry `%s` placeholders, filled in order from the constructor arguments.
202
201
  *
203
202
  * const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
204
203
  * throw new InsufficientFunds("$40");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A model-aware query builder — what `Model.query()` sugar and eager loading are
3
3
  * built on. It wraps the plain `QueryBuilder`, hydrates rows into models (firing
4
- * `retrieved`), and adds the relationship-aware operations Eloquent has and a raw
4
+ * `retrieved`), and adds the relationship-aware operations a query builder lacks and a raw
5
5
  * builder can't: `with()` (nested eager loading), `withCount()`, and existence
6
6
  * filters `has()` / `whereHas()` / `doesntHave()`.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A model-aware query builder — what `Model.query()` sugar and eager loading are
3
3
  * built on. It wraps the plain `QueryBuilder`, hydrates rows into models (firing
4
- * `retrieved`), and adds the relationship-aware operations Eloquent has and a raw
4
+ * `retrieved`), and adds the relationship-aware operations a query builder lacks and a raw
5
5
  * builder can't: `with()` (nested eager loading), `withCount()`, and existence
6
6
  * filters `has()` / `whereHas()` / `doesntHave()`.
7
7
  *
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Per-request logging. `requestLogger()` middleware binds a child logger to each
3
3
  * request with a generated `reqId`, so every log line within that request
4
- * correlates — Fastify's `request.log`, built on Keel's `Logger.child()`. It can
5
- * also log the request start and completion (method, path, status, duration).
4
+ * correlates — built on Keel's `Logger.child()`. It can also log the request
5
+ * start and completion (method, path, status, duration).
6
6
  *
7
7
  * kernel.use(requestLogger()); // in app/Http/Kernel.ts
8
8
  * requestLog().info("charging card"); // anywhere in the request → carries reqId
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Per-request logging. `requestLogger()` middleware binds a child logger to each
3
3
  * request with a generated `reqId`, so every log line within that request
4
- * correlates — Fastify's `request.log`, built on Keel's `Logger.child()`. It can
5
- * also log the request start and completion (method, path, status, duration).
4
+ * correlates — built on Keel's `Logger.child()`. It can also log the request
5
+ * start and completion (method, path, status, duration).
6
6
  *
7
7
  * kernel.use(requestLogger()); // in app/Http/Kernel.ts
8
8
  * requestLog().info("charging card"); // anywhere in the request → carries reqId
@@ -112,7 +112,7 @@ export function html(body, status) {
112
112
  }
113
113
  export function redirect(location, status) {
114
114
  const c = maybeCtx();
115
- // Koa-style `redirect("back")`: bounce to the Referer, or "/" if there isn't one.
115
+ // `redirect("back")`: bounce to the Referer, or "/" if there isn't one.
116
116
  if (location === "back")
117
117
  location = c?.req.header("referer") ?? "/";
118
118
  return c
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * A test client for Keel apps — inject requests without a live server and assert
3
- * on the response, the way Fastify's `inject()` does. Wraps the app's Hono
4
- * instance (which already does fetch-style request injection), adding verb
5
- * helpers with JSON bodies and fluent response assertions.
3
+ * on the response. Wraps the app's Hono instance (which already does fetch-style
4
+ * request injection), adding verb helpers with JSON bodies and fluent response
5
+ * assertions.
6
6
  *
7
7
  * const client = await testClient(app);
8
8
  * const res = await client.post("/users", { email: "a@b.com" });
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * A test client for Keel apps — inject requests without a live server and assert
3
- * on the response, the way Fastify's `inject()` does. Wraps the app's Hono
4
- * instance (which already does fetch-style request injection), adding verb
5
- * helpers with JSON bodies and fluent response assertions.
3
+ * on the response. Wraps the app's Hono instance (which already does fetch-style
4
+ * request injection), adding verb helpers with JSON bodies and fluent response
5
+ * assertions.
6
6
  *
7
7
  * const client = await testClient(app);
8
8
  * const res = await client.post("/users", { email: "a@b.com" });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaferllc/keel",
3
- "version": "0.83.0",
3
+ "version": "0.83.1",
4
4
  "description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
5
5
  "repo": "https://github.com/shaferllc/keel",
6
6
  "generated": "run `npm run build:ai` to regenerate",
@@ -50,7 +50,7 @@
50
50
  {
51
51
  "slug": "billing",
52
52
  "title": "Billing",
53
- "summary": "Keel Billing is a subscription-billing layer — a port of Laravel Cashier — for charging customers, managing subscriptions, and reconciling gateway state through webhooks.",
53
+ "summary": "Keel Billing is a subscription-billing layer for charging customers, managing subscriptions, and reconciling gateway state through webhooks.",
54
54
  "path": "docs/billing.md",
55
55
  "example": null
56
56
  },
package/docs/billing.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # Billing
2
2
 
3
- Keel Billing is a subscription-billing layer a port of [Laravel
4
- Cashier](https://laravel.com/docs/13.x/billing) for charging customers,
5
- managing subscriptions, and reconciling gateway state through webhooks. It ships
6
- as a Keel [package](./packages.md) and supports two gateways behind one API:
3
+ Keel Billing is a subscription-billing layer for charging customers, managing
4
+ subscriptions, and reconciling gateway state through webhooks. It ships as a
5
+ Keel [package](./packages.md) and supports two gateways behind one API:
7
6
  **Stripe** and **Paddle**.
8
7
 
9
8
  It attaches to a model with a mixin. Your `User` becomes billable, gains a
@@ -221,8 +220,8 @@ resolveBillableUsing(async (customerId) => {
221
220
  The migration is gateway-neutral: `subscriptions` (with `gateway`,
222
221
  `provider_id`, `provider_status`, `provider_price`, trial/grace timestamps),
223
222
  `subscription_items`, and columns on `users` (`billing_gateway`,
224
- `billing_customer_id`, `pm_type`, `pm_last_four`, `trial_ends_at`). Cashier
225
- targets the standard `users` billable table.
223
+ `billing_customer_id`, `pm_type`, `pm_last_four`, `trial_ends_at`). The default
224
+ migration targets the standard `users` billable table.
226
225
 
227
226
  ## Testing
228
227
 
package/docs/changelog.md CHANGED
@@ -4,6 +4,92 @@ All notable changes to Keel are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
5
5
  adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.83.0] — 2026-07-12
8
+
9
+ ### Added
10
+
11
+ - **Starter kits, and a generator that can't fall behind.** Four curated
12
+ applications — `minimal`, `api`, `app` (views, sessions, register/login, password
13
+ reset, two-factor), and `saas` (teams, roles, invitations, billing,
14
+ multi-tenancy):
15
+
16
+ ```bash
17
+ npm create keeljs@latest my-app -- --preset saas
18
+ ```
19
+
20
+ The templates ship **inside this package**, so the version a kit is generated from
21
+ is, by construction, the version it was written for. The old standalone starter
22
+ drifted to five releases behind and nothing noticed; CI now generates all four kits
23
+ on every push and typechecks, migrates, boots, serves a request, bundles the
24
+ Worker, and runs their tests.
25
+
26
+ - **Teams (`@shaferllc/keel/teams`)** — multi-tenancy, membership, roles, and
27
+ invitations. `TenantModel` makes isolation the default rather than a habit: reads
28
+ are constrained by an inherited global scope (so even `find(id)` returns `null` for
29
+ another team's row — not a filter you can forget), and writes are stamped with the
30
+ current team, so a row can't be born ownerless.
31
+
32
+ Outside a team context a tenant query **throws**. A job, console command, or
33
+ webhook has no request and therefore no team; returning everything is how one
34
+ customer's data reaches another, and `team_id = NULL` means jobs quietly do
35
+ nothing. So work says which team it is for — `runForTeam(team, fn)` — or says out
36
+ loud that it spans all of them — `withoutTenant(fn)`. Both are greppable at audit
37
+ time.
38
+
39
+ - **Accounts (`@shaferllc/keel/accounts`)** — password reset, email verification, and
40
+ two-factor, mounted with one provider. A correct password on a 2FA account yields a
41
+ short-lived, single-purpose **challenge, not a session**, so there is no
42
+ half-authenticated state for a route to forget to check. TOTP is RFC 6238, verified
43
+ against the RFC's published vectors, WebCrypto-only, and therefore edge-safe. No
44
+ tokens table: reset links carry their own purpose and expiry and are bound to a
45
+ fingerprint of the current password hash, so spending one kills it.
46
+
47
+ - **D1 over HTTP (`@shaferllc/keel/db/d1-http`).** The D1 binding only exists inside a
48
+ Worker, so `keel migrate` had nowhere to point and you could not create your
49
+ tables. The same `Connection` over D1's HTTP API closes that: migrations run from a
50
+ laptop and from CI. It treats an error in the response *body* as an error even when
51
+ the status is 200 — which is how Cloudflare often reports them, and trusting the
52
+ status would let a failed migration look like it succeeded.
53
+
54
+ - **`Model.withoutGlobalScope(...)` / `withoutGlobalScopes()`** — escaping a scope
55
+ should be typed out and greppable, never something you arrive at by forgetting a
56
+ `where`.
57
+
58
+ ### Fixed
59
+
60
+ - **Global scopes and model hooks now inherit.** Both were keyed by the exact class,
61
+ so a scope or a `creating` hook declared on a *base* class was silently ignored by
62
+ every subclass. The models guide advertises global scopes as "the base for
63
+ multi-tenancy", and that was precisely the case that didn't work: the scope did
64
+ nothing, the query returned every tenant's rows, and nothing complained. It failed
65
+ **open**. Both now walk the prototype chain, and a scope is passed the model it is
66
+ scoping so a base class can read each subclass's own configuration.
67
+
68
+ - **The official `@libsql/client` no longer needs a cast.** Under
69
+ `strictFunctionTypes` the narrower parameter types made the real `Client`
70
+ unassignable to `LibSqlLike`, so wiring libSQL the obvious way required
71
+ `client as unknown as LibSqlLike` — a cast even Keel's own tests carried.
72
+
73
+ - **`createTeam()` could not give two people with the same name a team.** It slugged
74
+ the name into a `UNIQUE` column, so the second "Ada" to sign up got a 500. Looking
75
+ for a free slug first is check-then-act and loses the race anyway; the unique index
76
+ is the only real arbiter, so it now arbitrates and the insert retries.
77
+
78
+ ## [0.82.0] — 2026-07-12
79
+
80
+ ### Added
81
+
82
+ - **Query builder: the methods people actually reach for.** `join`/`leftJoin`,
83
+ `groupBy`/`having`/`distinct`, `whereColumn`/`whereRaw`/`orderByRaw`, `when`,
84
+ `increment`/`decrement`, `upsert`/`insertOrIgnore`, and `chunk`, with docs.
85
+
86
+ ## [0.81.2] — 2026-07-12
87
+
88
+ ### Documentation
89
+
90
+ - **The changelog is published to the docs site** (`docs/changelog.md`), so releases
91
+ are readable at keeljs.com rather than only in the repository.
92
+
7
93
  ## [0.81.1] — 2026-07-12
8
94
 
9
95
  ### Documentation
@@ -25,8 +111,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
25
111
 
26
112
  ### Added
27
113
 
28
- - **ORM: Eloquent-parity features.** The active-record `Model` grows most of the
29
- Eloquent surface people reach for, all backward-compatible and still on the
114
+ - **ORM: the features people reach for.** The active-record `Model` grows the
115
+ surface a real ORM needs, all backward-compatible and still on the
30
116
  driver-agnostic query builder (no JOINs, edge-safe):
31
117
 
32
118
  - **Lifecycle events & observers** — `creating`/`created`, `updating`/
@@ -91,8 +177,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
91
177
 
92
178
  ### Added
93
179
 
94
- - **Billing** — a new `@shaferllc/keel/billing` package: a Cashier-style
95
- subscription layer with **one gateway-neutral API over Stripe and Paddle**
180
+ - **Billing** — a new `@shaferllc/keel/billing` package: a subscription
181
+ layer with **one gateway-neutral API over Stripe and Paddle**
96
182
  (switching gateways is a config change). `class User extends Billable(Model)`
97
183
  gives a gateway customer, subscriptions (create/swap/quantity/trials/cancel/
98
184
  resume + status checks), single charges + refunds, invoices, hosted checkout,
@@ -286,7 +372,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
286
372
 
287
373
  ### Changed
288
374
 
289
- - **Removed the Remult references** from the API-resources guide and source
375
+ - **Removed comparisons to other frameworks** from the API-resources guide and source
290
376
  comments. Keel isn't that, and the docs shouldn't read as a comparison to another
291
377
  framework. The surrounding sentences are rewritten so they still say what the
292
378
  feature does rather than leaving a hole. No behavior change.
@@ -960,7 +1046,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
960
1046
  connection. They honor `X-Forwarded-Proto` / `X-Forwarded-Host` over the raw
961
1047
  URL, so an app behind a TLS-terminating proxy or load balancer sees the
962
1048
  client's real scheme and host — use `origin` to build absolute links and
963
- `secure` to gate insecure requests. (Koa-inspired.)
1049
+ `secure` to gate insecure requests.
964
1050
  - **`response.back(fallback?)` and `redirect("back")`.** Redirect to the
965
1051
  request's `Referer`, falling back to `fallback` (default `"/"`) when it's
966
1052
  absent — the "return where you came from" shortcut for post-form flows.
@@ -1189,7 +1275,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1189
1275
  - **Route config / metadata.** Attach arbitrary data to a route or group with
1190
1276
  `.config({ … })` and read it in the handler or route middleware via
1191
1277
  `request.route.config` — per-route flags like an auth scope, rate tier, or
1192
- layout choice (Fastify's route `config`). Group config merges into every route,
1278
+ layout choice. Group config merges into every route,
1193
1279
  with a route's own keys winning. The matched-route context is now set *before*
1194
1280
  a route's middleware, so route/group middleware can branch on `request.route`.
1195
1281
  See [docs/routing.md](./docs/routing.md#route-config).
@@ -1236,7 +1322,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1236
1322
  - **Response header helpers.** The `response` accessor gains `headers({...})` (set
1237
1323
  several at once), `getHeader(name)`, and `hasHeader(name)` — so middleware can
1238
1324
  inspect and conditionally set what a handler already put on the response (e.g.
1239
- a default `cache-control`). Brings `response` to parity with Fastify's Reply.
1325
+ a default `cache-control`).
1240
1326
  See [docs/request-response.md](./docs/request-response.md).
1241
1327
  - **Design principles** documented in
1242
1328
  [docs/architecture.md](./docs/architecture.md#design-principles) — edge-safe /
@@ -1253,9 +1339,9 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1253
1339
  options at registration: `app.register(RateLimitProvider, { max: 100 })`, typed
1254
1340
  via `ServiceProvider<{ max: number }>` and read as `this.options`. The same
1255
1341
  provider class can register more than once with different options, so a provider
1256
- is now genuinely reusable (Fastify's `register(plugin, options)`). Backward
1257
- compatible — options default to `{}`. (Keel providers stay un-encapsulated by
1258
- design; per-request scoping is [middleware](./docs/middleware.md).) See
1342
+ is now genuinely reusable. Backward compatible options default to `{}`.
1343
+ (Keel providers stay un-encapsulated by design; per-request scoping is
1344
+ [middleware](./docs/middleware.md).) See
1259
1345
  [docs/providers.md](./docs/providers.md#providers-are-keels-plugin-system).
1260
1346
 
1261
1347
  [0.50.0]: https://github.com/shaferllc/keel/releases/tag/v0.50.0
@@ -1350,8 +1436,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1350
1436
  a bad request with a `422` `ValidationException` (errors from every part
1351
1437
  aggregated, keyed `body.field` / `query.field` / `params.field`) so the handler
1352
1438
  only ever sees valid input. `validated(part)` returns the parsed, typed value.
1353
- The declarative counterpart to Fastify's route schemas, built on the same
1354
- schema-agnostic `validate()` engine (bring your own Zod-style schema). See
1439
+ Built on the same schema-agnostic `validate()` engine (bring your own
1440
+ Zod-style schema). See
1355
1441
  [docs/validation.md](./docs/validation.md#declarative-validation-before-the-handler).
1356
1442
 
1357
1443
  [0.44.0]: https://github.com/shaferllc/keel/releases/tag/v0.44.0
@@ -1362,7 +1448,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1362
1448
 
1363
1449
  - **Per-request logging.** `requestLogger()` middleware binds a child logger with
1364
1450
  a generated `reqId` to each request, so every log line within a request
1365
- correlates (Fastify's `request.log`). It logs request start/completion
1451
+ correlates. It logs request start/completion
1366
1452
  (method, path, status, ms) by default; options for `genReqId`, reusing an
1367
1453
  incoming `idHeader` (distributed tracing), and disabling the auto lines.
1368
1454
  `requestLog()` reaches the current request's logger anywhere (falls back to the
@@ -1400,8 +1486,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1400
1486
  through the default path (with `code` in the JSON body) and passes
1401
1487
  `instanceof HttpException`. The built-in exceptions now carry stable codes too
1402
1488
  (`E_NOT_FOUND`, `E_UNAUTHORIZED`, `E_FORBIDDEN`, `E_VALIDATION`), so `code`
1403
- surfaces without any work. Inspired by Fastify's `@fastify/error`. Also
1404
- documented: serving over **HTTP/2** needs no framework code — it's a transport
1489
+ surfaces without any work. Also documented: serving over **HTTP/2** needs no
1490
+ framework code — it's a transport
1405
1491
  concern handled by the edge platform, a reverse proxy, or a `@hono/node-server`
1406
1492
  `node:http2` option. See [docs/errors.md](./docs/errors.md#coded-errors-with-createerror)
1407
1493
  and [docs/hono.md](./docs/hono.md#serving-over-http2).
@@ -1435,8 +1521,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1435
1521
  - **Raw request-body accessors.** `request.text()`, `request.arrayBuffer()`, and
1436
1522
  `request.blob()` read the body for content types `json()` / `all()` don't
1437
1523
  handle — XML, CSV, protobuf, msgpack, or any custom format — which you then
1438
- parse yourself. Keel keeps body parsing explicit (no Fastify-style content-type
1439
- parser registry): you call the accessor you want. See
1524
+ parse yourself. Keel keeps body parsing explicit (no content-type parser
1525
+ registry): you call the accessor you want. See
1440
1526
  [docs/request-response.md](./docs/request-response.md#other-content-types).
1441
1527
 
1442
1528
  [0.40.1]: https://github.com/shaferllc/keel/releases/tag/v0.40.1
@@ -1450,9 +1536,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1450
1536
  life of the request. `decorateRequest(name, resolver)` registers a resolver
1451
1537
  (sync or async), `decorated(name)` reads it (computed once, then cached),
1452
1538
  `setRequestValue(name, value)` sets it imperatively (e.g. from middleware), and
1453
- `hasRequestDecorator(name)` checks. Inspired by Fastify's `decorateRequest`,
1454
- but without the null-placeholder/`onRequest`-hook dance the per-request memo
1455
- is keyed off the context via a WeakMap, so nothing leaks between requests.
1539
+ `hasRequestDecorator(name)` checks. The per-request memo is keyed off the
1540
+ context via a WeakMap, so nothing leaks between requests.
1456
1541
  (Decorating the *app* is already the container's job.) See
1457
1542
  [docs/decorators.md](./docs/decorators.md).
1458
1543
 
@@ -2,10 +2,9 @@
2
2
 
3
3
  Attach named, computed values to the current request — `request.user`,
4
4
  `request.tenant`, `request.locale` — resolved **lazily** and **memoized for the
5
- life of the request**. Inspired by [Fastify's decorators](https://fastify.dev/docs/latest/Reference/Decorators/),
6
- but without the footguns: you register a resolver once, and Keel computes it on
5
+ life of the request**. You register a resolver once, and Keel computes it on
7
6
  first access and caches it per request. No null-placeholder declaration, no
8
- `onRequest` hook to remember, no shared-state leak between requests.
7
+ shared-state leak between requests.
9
8
 
10
9
  > Decorating the **application** is already the [service container's](./container.md)
11
10
  > job — `bind` / `singleton` / `instance` / `make`, with `bound()` as
@@ -53,9 +52,6 @@ setRequestValue("user", theAuthenticatedUser);
53
52
  const user = await decorated("user"); // returns the value set above, no re-lookup
54
53
  ```
55
54
 
56
- This is the clean version of Fastify's "declare a placeholder, set it in an
57
- `onRequest` hook" pattern.
58
-
59
55
  ## Why lazy + memoized
60
56
 
61
57
  Resolving the current user (or tenant, or locale, or a feature-flag set) is the
package/docs/errors.md CHANGED
@@ -185,7 +185,7 @@ subclasses don't define either — they render through the default path.
185
185
 
186
186
  When all you want is a coded error class — a stable `code`, a message, a status —
187
187
  skip the boilerplate and mint one with `createError`. It's the ergonomic shortcut
188
- for the common case, inspired by Fastify's `@fastify/error`:
188
+ for the common case:
189
189
 
190
190
  ```ts
191
191
  import { createError } from "@shaferllc/keel/core";
package/docs/hooks.md CHANGED
@@ -1,9 +1,7 @@
1
1
  # Lifecycle Hooks
2
2
 
3
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.
4
+ on shutdown, and observe route registration.
7
5
 
8
6
  > **Request-lifecycle hooks** (before/after a request, on error) are
9
7
  > [middleware](./middleware.md) in Keel — `HttpKernel.use()`, route/group
package/docs/logger.md CHANGED
@@ -156,8 +156,8 @@ With no options it defaults to `level: "info"`, `pretty: false`, and no bindings
156
156
 
157
157
  `requestLogger()` is a built-in middleware that binds a **child logger with a
158
158
  generated `reqId` to each request**, so every log line within a request
159
- correlates — Fastify's `request.log`. Install it in your HTTP kernel, then reach
160
- the request's logger anywhere with `requestLog()`:
159
+ correlates. Install it in your HTTP kernel, then reach the request's logger
160
+ anywhere with `requestLog()`:
161
161
 
162
162
  ```ts
163
163
  import { requestLogger, requestLog } from "@shaferllc/keel/core";
package/docs/packages.md CHANGED
@@ -7,9 +7,9 @@ adds the conventions a *shippable* package needs so it can carry its own schema
7
7
  and assets instead of asking the app to wire them by hand.
8
8
 
9
9
  [Keel Watch](./watch.md) — the debug dashboard — is a first-party package and the
10
- reference implementation of everything below. [Billing](./billing.md) (a Cashier
11
- port for Stripe and Paddle) is another, and shows a package contributing models,
12
- a schema migration, gateway drivers, and verified webhook routes.
10
+ reference implementation of everything below. [Billing](./billing.md) (Stripe and
11
+ Paddle subscriptions) is another, and shows a package contributing models, a
12
+ schema migration, gateway drivers, and verified webhook routes.
13
13
 
14
14
  ## The shape of a package
15
15
 
package/docs/providers.md CHANGED
@@ -111,12 +111,12 @@ app.register(RateLimitProvider, { max: 100 }); // parameterized, like a plugin
111
111
  The same provider class can be registered more than once with different options.
112
112
  Without options, `this.options` is an empty object.
113
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.
114
+ > Keel providers are **not encapsulated** — bindings, decorators, and routes are
115
+ > registered into the one global container. That's a deliberate simplification:
116
+ > there's a single, predictable scope, and no plugin-boundary rules to reason
117
+ > about. For per-request behavior in the HTTP pipeline (auth, logging, etc.),
118
+ > reach for [middleware](./middleware.md), which *is* scoped to the routes you
119
+ > attach it to.
120
120
 
121
121
  ## Generating a provider
122
122
 
@@ -51,8 +51,8 @@ and host (handy for building absolute links or forcing HTTPS).
51
51
 
52
52
  `all()` understands JSON and form bodies. For anything else — XML, CSV, a binary
53
53
  payload, a custom format — read the raw body and parse it yourself. There's no
54
- content-type parser registry to configure (unlike Fastify): parsing is explicit,
55
- so you call the accessor you want.
54
+ content-type parser registry to configure: parsing is explicit, so you call the
55
+ accessor you want.
56
56
 
57
57
  ```ts
58
58
  await request.text(); // the body as a string (XML, CSV, …)
package/docs/testing.md CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  Test your app by **injecting requests** — no server, no port, no network — and
4
4
  asserting on the response. `testClient()` wraps your app's Hono instance (which
5
- already does fetch-style injection) with verb helpers and fluent assertions, the
6
- way Fastify's `inject()` works.
5
+ already does fetch-style injection) with verb helpers and fluent assertions.
7
6
 
8
7
  ## The client
9
8
 
@@ -112,8 +112,7 @@ router.get("/posts/:id", [Posts, "show"]).middleware([
112
112
 
113
113
  `validated(part)` returns the parsed, typed value for that part (defaults to
114
114
  `"body"`). Coercion (`z.coerce.number()`) is the schema's job — it applies before
115
- your handler sees the value. This is the declarative counterpart to Fastify's
116
- route schemas, on top of the same `validate()` engine.
115
+ your handler sees the value. Built on the same `validate()` engine.
117
116
 
118
117
  ## Body parsing is JSON-only
119
118
 
package/llms-full.txt CHANGED
@@ -1233,12 +1233,12 @@ app.register(RateLimitProvider, { max: 100 }); // parameterized, like a plugin
1233
1233
  The same provider class can be registered more than once with different options.
1234
1234
  Without options, `this.options` is an empty object.
1235
1235
 
1236
- > Unlike Fastify plugins, Keel providers are **not encapsulated** — bindings,
1237
- > decorators, and routes are registered into the one global container. That's a
1238
- > deliberate simplification: there's a single, predictable scope, and no
1239
- > plugin-boundary rules to reason about. For per-request behavior in the HTTP
1240
- > pipeline (auth, logging, etc.), reach for [middleware](./middleware.md), which
1241
- > *is* scoped to the routes you attach it to.
1236
+ > Keel providers are **not encapsulated** — bindings, decorators, and routes are
1237
+ > registered into the one global container. That's a deliberate simplification:
1238
+ > there's a single, predictable scope, and no plugin-boundary rules to reason
1239
+ > about. For per-request behavior in the HTTP pipeline (auth, logging, etc.),
1240
+ > reach for [middleware](./middleware.md), which *is* scoped to the routes you
1241
+ > attach it to.
1242
1242
 
1243
1243
  ## Generating a provider
1244
1244
 
@@ -3102,8 +3102,8 @@ and host (handy for building absolute links or forcing HTTPS).
3102
3102
 
3103
3103
  `all()` understands JSON and form bodies. For anything else — XML, CSV, a binary
3104
3104
  payload, a custom format — read the raw body and parse it yourself. There's no
3105
- content-type parser registry to configure (unlike Fastify): parsing is explicit,
3106
- so you call the accessor you want.
3105
+ content-type parser registry to configure: parsing is explicit, so you call the
3106
+ accessor you want.
3107
3107
 
3108
3108
  ```ts
3109
3109
  await request.text(); // the body as a string (XML, CSV, …)
@@ -5559,10 +5559,9 @@ all gates/policies/hooks (a test helper).
5559
5559
 
5560
5560
  # Billing
5561
5561
 
5562
- Keel Billing is a subscription-billing layer a port of [Laravel
5563
- Cashier](https://laravel.com/docs/13.x/billing) for charging customers,
5564
- managing subscriptions, and reconciling gateway state through webhooks. It ships
5565
- as a Keel [package](./packages.md) and supports two gateways behind one API:
5562
+ Keel Billing is a subscription-billing layer for charging customers, managing
5563
+ subscriptions, and reconciling gateway state through webhooks. It ships as a
5564
+ Keel [package](./packages.md) and supports two gateways behind one API:
5566
5565
  **Stripe** and **Paddle**.
5567
5566
 
5568
5567
  It attaches to a model with a mixin. Your `User` becomes billable, gains a
@@ -5780,8 +5779,8 @@ resolveBillableUsing(async (customerId) => {
5780
5779
  The migration is gateway-neutral: `subscriptions` (with `gateway`,
5781
5780
  `provider_id`, `provider_status`, `provider_price`, trial/grace timestamps),
5782
5781
  `subscription_items`, and columns on `users` (`billing_gateway`,
5783
- `billing_customer_id`, `pm_type`, `pm_last_four`, `trial_ends_at`). Cashier
5784
- targets the standard `users` billable table.
5782
+ `billing_customer_id`, `pm_type`, `pm_last_four`, `trial_ends_at`). The default
5783
+ migration targets the standard `users` billable table.
5785
5784
 
5786
5785
  ## Testing
5787
5786
 
@@ -8162,10 +8161,9 @@ arguments still halts the request and renders an empty page.
8162
8161
 
8163
8162
  Attach named, computed values to the current request — `request.user`,
8164
8163
  `request.tenant`, `request.locale` — resolved **lazily** and **memoized for the
8165
- life of the request**. Inspired by [Fastify's decorators](https://fastify.dev/docs/latest/Reference/Decorators/),
8166
- but without the footguns: you register a resolver once, and Keel computes it on
8164
+ life of the request**. You register a resolver once, and Keel computes it on
8167
8165
  first access and caches it per request. No null-placeholder declaration, no
8168
- `onRequest` hook to remember, no shared-state leak between requests.
8166
+ shared-state leak between requests.
8169
8167
 
8170
8168
  > Decorating the **application** is already the [service container's](./container.md)
8171
8169
  > job — `bind` / `singleton` / `instance` / `make`, with `bound()` as
@@ -8213,9 +8211,6 @@ setRequestValue("user", theAuthenticatedUser);
8213
8211
  const user = await decorated("user"); // returns the value set above, no re-lookup
8214
8212
  ```
8215
8213
 
8216
- This is the clean version of Fastify's "declare a placeholder, set it in an
8217
- `onRequest` hook" pattern.
8218
-
8219
8214
  ## Why lazy + memoized
8220
8215
 
8221
8216
  Resolving the current user (or tenant, or locale, or a feature-flag set) is the
@@ -8479,7 +8474,7 @@ subclasses don't define either — they render through the default path.
8479
8474
 
8480
8475
  When all you want is a coded error class — a stable `code`, a message, a status —
8481
8476
  skip the boilerplate and mint one with `createError`. It's the ergonomic shortcut
8482
- for the common case, inspired by Fastify's `@fastify/error`:
8477
+ for the common case:
8483
8478
 
8484
8479
  ```ts
8485
8480
  import { createError } from "@shaferllc/keel/core";
@@ -10418,9 +10413,7 @@ implementing anything — the same `fetch` handler just gets served over it.
10418
10413
  # Lifecycle Hooks
10419
10414
 
10420
10415
  Tap into the **application lifecycle** — run code once the app is ready, clean up
10421
- on shutdown, and observe route registration. Inspired by
10422
- [Fastify's hooks](https://fastify.dev/docs/latest/Reference/Hooks/), scoped to the
10423
- parts Keel doesn't already cover.
10416
+ on shutdown, and observe route registration.
10424
10417
 
10425
10418
  > **Request-lifecycle hooks** (before/after a request, on error) are
10426
10419
  > [middleware](./middleware.md) in Keel — `HttpKernel.use()`, route/group
@@ -11585,8 +11578,8 @@ With no options it defaults to `level: "info"`, `pretty: false`, and no bindings
11585
11578
 
11586
11579
  `requestLogger()` is a built-in middleware that binds a **child logger with a
11587
11580
  generated `reqId` to each request**, so every log line within a request
11588
- correlates — Fastify's `request.log`. Install it in your HTTP kernel, then reach
11589
- the request's logger anywhere with `requestLog()`:
11581
+ correlates. Install it in your HTTP kernel, then reach the request's logger
11582
+ anywhere with `requestLog()`:
11590
11583
 
11591
11584
  ```ts
11592
11585
  import { requestLogger, requestLog } from "@shaferllc/keel/core";
@@ -14956,9 +14949,9 @@ adds the conventions a *shippable* package needs so it can carry its own schema
14956
14949
  and assets instead of asking the app to wire them by hand.
14957
14950
 
14958
14951
  [Keel Watch](./watch.md) — the debug dashboard — is a first-party package and the
14959
- reference implementation of everything below. [Billing](./billing.md) (a Cashier
14960
- port for Stripe and Paddle) is another, and shows a package contributing models,
14961
- a schema migration, gateway drivers, and verified webhook routes.
14952
+ reference implementation of everything below. [Billing](./billing.md) (Stripe and
14953
+ Paddle subscriptions) is another, and shows a package contributing models, a
14954
+ schema migration, gateway drivers, and verified webhook routes.
14962
14955
 
14963
14956
  ## The shape of a package
14964
14957
 
@@ -19171,8 +19164,7 @@ construct it directly.
19171
19164
 
19172
19165
  Test your app by **injecting requests** — no server, no port, no network — and
19173
19166
  asserting on the response. `testClient()` wraps your app's Hono instance (which
19174
- already does fetch-style injection) with verb helpers and fluent assertions, the
19175
- way Fastify's `inject()` works.
19167
+ already does fetch-style injection) with verb helpers and fluent assertions.
19176
19168
 
19177
19169
  ## The client
19178
19170
 
@@ -20355,8 +20347,7 @@ router.get("/posts/:id", [Posts, "show"]).middleware([
20355
20347
 
20356
20348
  `validated(part)` returns the parsed, typed value for that part (defaults to
20357
20349
  `"body"`). Coercion (`z.coerce.number()`) is the schema's job — it applies before
20358
- your handler sees the value. This is the declarative counterpart to Fastify's
20359
- route schemas, on top of the same `validate()` engine.
20350
+ your handler sees the value. Built on the same `validate()` engine.
20360
20351
 
20361
20352
  ## Body parsing is JSON-only
20362
20353
 
package/llms.txt CHANGED
@@ -15,7 +15,7 @@ Userland imports everything from `@shaferllc/keel/core`.
15
15
  - [Architecture](https://github.com/shaferllc/keel/blob/main/docs/architecture.md): Keel is small on purpose. This page maps the pieces and traces a request from socket to response. Nothing here is magic — every layer is a short, readable file in src/core/, and this guide is mostly a reading order for it.
16
16
  - [Authentication](https://github.com/shaferllc/keel/blob/main/docs/authentication.md): Session-based auth built on the pieces you already have: sessions hold the login, hashing checks passwords.
17
17
  - [Authorization](https://github.com/shaferllc/keel/blob/main/docs/authorization.md): Where authentication answers who you are, authorization answers what you're allowed to do.
18
- - [Billing](https://github.com/shaferllc/keel/blob/main/docs/billing.md): Keel Billing is a subscription-billing layer — a port of Laravel Cashier — for charging customers, managing subscriptions, and reconciling gateway state through webhooks.
18
+ - [Billing](https://github.com/shaferllc/keel/blob/main/docs/billing.md): Keel Billing is a subscription-billing layer for charging customers, managing subscriptions, and reconciling gateway state through webhooks.
19
19
  - [Broadcasting](https://github.com/shaferllc/keel/blob/main/docs/broadcasting.md): Push events to clients in real time over named channels.
20
20
  - [Service Broker](https://github.com/shaferllc/keel/blob/main/docs/broker.md): Structure an application as services that talk to each other by name instead of by import.
21
21
  - [Cache](https://github.com/shaferllc/keel/blob/main/docs/cache.md): A small cache with TTLs and the remember pattern.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaferllc/keel",
3
- "version": "0.83.0",
3
+ "version": "0.83.1",
4
4
  "type": "module",
5
5
  "description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
6
6
  "license": "MIT",