Skip to content

Commit 71bb07f

Browse files
docs(plugin-auth): correct the databaseHooks middleware-bypass claim (#4802) (#4942)
The `databaseHooks` JSDoc justified "use this seam, not an ObjectQL middleware" with a mechanism claim that no longer holds: better-auth's adapter "goes through `dataEngine` directly, bypassing the `ql.registerMiddleware` chain". Re-verified against main: ObjectQLPlugin registers ONE instance under both `objectql` and `data`; AuthPlugin hands that same instance to createObjectQLAdapterFactory; the adapter writes with a plain `dataEngine.insert(objectName, …)` (no bypass option); and `ObjectQL.insert()` wraps its body in `executeWithMiddleware()`, filtered only by object name. The chain and the lifecycle hooks do fire. The rule is unchanged — user-lifecycle invariants belong in `user.create.after` — but the reason is now ADR-0093 D2 (one owner on the one seam every creation path flows through). The narrower surviving fact is written down in place of the false one: adapter writes carry `context.isSystem: true`, so authorization middlewares early-return by design. The stale sentence is refuted in place rather than deleted because it was copied into cloud's agent-facing docs (cloud#1012 / cloud#1022). Comments only; no runtime behaviour change. Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 32a0874 commit 71bb07f

3 files changed

Lines changed: 69 additions & 18 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
---
3+
4+
docs(plugin-auth): the `databaseHooks` doc comment no longer claims better-auth's adapter bypasses the ObjectQL middleware chain (#4802). The option's JSDoc in `auth-manager.ts` (and its two sibling copies — the wiring comment beside `composeDatabaseHooks`, and `AuthPluginOptions.databaseHooks` in `auth-plugin.ts`) justified "use `databaseHooks`, not an ObjectQL middleware" with a mechanism claim that no longer holds: *better-auth's adapter goes through `dataEngine` directly, bypassing the `ql.registerMiddleware` chain*.
5+
6+
Re-verified hop by hop against `main`: `ObjectQLPlugin` registers **one** engine instance under both service names (`registerService('objectql', this.ql)` and `registerService('data', this.ql)`, and nothing else in the repo registers `data`); `AuthPlugin` passes exactly that instance to `createObjectQLAdapterFactory`; the adapter writes with a plain `dataEngine.insert(objectName, …)` — there is no bypass or skip-middleware option to pass; and `ObjectQL.insert()` wraps its body in `executeWithMiddleware()`, whose only filter is the object name. So `ql.registerMiddleware(fn, { object: 'sys_user' })` **does** fire for better-auth's writes, and so do the engine's `beforeInsert`/`afterInsert` hooks — the SCIM identity-source stamp in `auth-plugin.ts` is built on precisely that.
7+
8+
The **rule is unchanged** — user-lifecycle invariants still belong in `user.create.after`, not in a `sys_user` middleware — but the reason is now the one that is actually true: **ADR-0093 D2**, one owner for the invariant on the one seam every creation path already flows through (self-signup, admin create-user, import, SSO JIT). The narrower fact that survives is written down instead of the false one: adapter writes carry `context.isSystem: true` (`withSystemContext`, pinned by `objectql-adapter.test.ts`), so every *authorization* middleware — security, sharing, the ADR-0092 identity write guard — early-returns by design; a middleware that gates on `isSystem` sees nothing, one that does not, runs.
9+
10+
The stale sentence is **refuted in place rather than deleted**, because it had been copied into cloud's agent-facing docs and had already killed the middleware option in two rounds of design work there (cloud#1012, handed over as cloud#1022). A reader arriving from one of those copies needs to see the claim named and corrected; a silent deletion would leave them assuming the framework comment is the stale one.
11+
12+
Comments only — no runtime behaviour changes, nothing released.

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -476,16 +476,47 @@ export interface AuthManagerOptions extends Partial<AuthConfig> {
476476

477477
/**
478478
* Pass-through to better-auth's `databaseHooks` option. better-auth fires
479-
* these around its own adapter writes (e.g. when `genericOAuth` creates
480-
* a JIT user during SSO login), which the kernel-level ObjectQL
481-
* middleware does NOT observe — better-auth's adapter goes through
482-
* `dataEngine` directly, bypassing the `ql.registerMiddleware` chain.
479+
* these around its own adapter writes, including paths that never reach an
480+
* HTTP route of ours (e.g. `genericOAuth` JIT-creating a user during SSO
481+
* login).
483482
*
484-
* The platform uses this to attach a `user.create.after` hook that
485-
* auto-provisions a personal organization for every newly-created user
486-
* (mirroring what SecurityPlugin's middleware does for direct
487-
* ObjectQL inserts) so SSO-arriving users don't land on the empty
488-
* "create organization" screen.
483+
* **Use this seam — not an ObjectQL middleware on `sys_user` — to react to
484+
* user creation.** The reason is ADR-0093 D2: an invariant over the user
485+
* lifecycle gets exactly ONE owner, composed into `user.create.after`
486+
* (`reconcile-membership.ts`), because that is the single seam every
487+
* creation path already flows through (self-signup, admin create-user,
488+
* import, SSO JIT). Re-deriving the invariant per creation path — or in a
489+
* parallel data-layer middleware — is precisely the shape D2 eliminated.
490+
* The platform's own use is a `user.create.after` hook that provisions a
491+
* personal/default organization so SSO-arriving users don't land on the
492+
* empty "create organization" screen.
493+
*
494+
* **Corrected mechanism (#4802).** This comment used to justify the rule by
495+
* asserting that better-auth's adapter "goes through `dataEngine` directly,
496+
* bypassing the `ql.registerMiddleware` chain". That is **not** true, and
497+
* the claim was copied widely enough (framework + cloud) to keep producing
498+
* wrong architectural conclusions, so it is refuted here rather than quietly
499+
* deleted. Verified hop by hop:
500+
*
501+
* - `objectql/src/plugin.ts` registers ONE instance under both names —
502+
* `registerService('objectql', this.ql)` and `registerService('data',
503+
* this.ql)`; nothing else in the repo registers `data`.
504+
* - `auth-plugin.ts` takes `ctx.getService<IDataEngine>('data')` and hands
505+
* that same instance to {@link createObjectQLAdapterFactory}.
506+
* - `objectql-adapter.ts` writes with plain `dataEngine.insert(objectName,
507+
* …)` — there is no bypass/skip-middleware option to pass.
508+
* - `ObjectQL.insert()` (`objectql/src/engine.ts`) wraps its body in
509+
* `executeWithMiddleware()`, whose only filter is the object name.
510+
*
511+
* So `ql.registerMiddleware(fn, { object: 'sys_user' })` **does** fire for
512+
* better-auth's writes, and so do the engine's `beforeInsert`/`afterInsert`
513+
* lifecycle hooks (the SCIM identity-source stamp in `auth-plugin.ts` relies
514+
* on exactly that). What remains true is narrower and worth knowing: adapter
515+
* writes carry `context.isSystem: true` (`withSystemContext`, pinned by
516+
* `objectql-adapter.test.ts`), and every authorization middleware —
517+
* security, sharing, the ADR-0092 identity write guard — early-returns on
518+
* `isSystem` by design. A middleware that gates on `isSystem` therefore sees
519+
* nothing; one that does not, runs.
489520
*/
490521
databaseHooks?: BetterAuthOptions['databaseHooks'];
491522

@@ -1001,11 +1032,14 @@ export class AuthManager {
10011032
// better-auth plugins — registered based on AuthPluginConfig flags
10021033
plugins,
10031034

1004-
// Database hooks (fired by better-auth's adapter writes — these run
1005-
// for SSO JIT-provisioning too, unlike kernel-level ObjectQL
1006-
// middleware which better-auth's adapter bypasses). The framework's
1007-
// identity-source stamp (`account.create.after`) is always composed in,
1008-
// preserving any host-supplied hooks.
1035+
// Database hooks (fired by better-auth's adapter writes — the ONE seam
1036+
// every user-creation path flows through, SSO JIT-provisioning included).
1037+
// ADR-0093 D2 makes that seam the single owner of the user-lifecycle
1038+
// invariants; see the `databaseHooks` option doc for why, and for the
1039+
// #4802 correction of the "adapter bypasses ObjectQL middleware" claim
1040+
// this comment used to carry. The framework's identity-source stamp
1041+
// (`account.create.after`) is always composed in, preserving any
1042+
// host-supplied hooks.
10091043
databaseHooks: this.composeDatabaseHooks(this.config.databaseHooks),
10101044

10111045
// Bootstrap bypass for `disableSignUp`. The first-run owner wizard

packages/plugins/plugin-auth/src/auth-plugin.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,15 @@ export interface AuthPluginOptions extends Partial<AuthConfig> {
161161
* Pass-through to better-auth's `databaseHooks` option. Used by
162162
* platform consumers (objectos kernel) to attach a
163163
* `user.create.after` hook that auto-provisions a personal
164-
* organization for JIT-created SSO users — better-auth's adapter
165-
* bypasses kernel-level ObjectQL middleware, so this is the only
166-
* hook point that fires for every user creation path (email signup,
167-
* social/OIDC sign-in, admin-created accounts).
164+
* organization for JIT-created SSO users.
165+
*
166+
* This is the seam to use because it is the ONE hook point every user
167+
* creation path flows through (email signup, social/OIDC sign-in,
168+
* admin-created accounts, SSO JIT) and ADR-0093 D2 gives such invariants a
169+
* single owner there. It is NOT because better-auth's adapter escapes the
170+
* data layer: it writes through the same ObjectQL instance and its
171+
* middleware/lifecycle-hook chain does run — see
172+
* `AuthManagerOptions.databaseHooks` for the hop-by-hop correction (#4802).
168173
*/
169174
databaseHooks?: BetterAuthOptions['databaseHooks'];
170175
}

0 commit comments

Comments
 (0)