Summary
managedBy: 'better-auth' is documented as "generic CRUD is suppressed — use the action endpoints" but nothing enforces it. The generic data API will happily INSERT/UPDATE/DELETE against an externally-managed table, bypassing the owning subsystem's invariants, hooks, and authz path.
Evidence
packages/platform-objects/src/identity/sys-team.object.ts:
managedBy: 'better-auth',
// "Generic CRUD is suppressed (managedBy: 'better-auth'), so these are
// the canonical entry points for create/update/delete."
...
enable: { apiMethods: ['get','list','create','update','delete'] } // <- contradicts the comment
packages/objectql/src/engine.ts — no managedBy check in the insert/update/delete path (assertWriteAllowed() only gates external datasources).
packages/objectql/src/registry.ts:~189 — managedBy is only used to skip system-field injection (if (schema.managedBy === 'better-auth') return schema), not to block ops.
packages/rest/src/rest-server.ts create handler calls p.createData(...) directly; no metadata/apiMethods gate.
Result: a bare POST /api/v1/data/sys_team runs a generic INSERT. The canonical path is the create_team action → POST /api/v1/auth/organization/create-team, which lets better-auth own id/organizationId, enforce org-membership invariants, and apply its own authz.
Why this matters (not just an error-message bug)
- Integrity: you can create a
sys_team row better-auth doesn't know about, or one that violates its team↔org consistency / cascade-on-delete assumptions.
- Security surface: the action routes through better-auth authz; the generic
/data route uses ObjectQL RLS. These are not guaranteed equivalent.
Proposed change
Make managedBy (external owner) a real capability gate: for objects whose writes are externally managed, the generic data API should refuse create/update/delete with a clear 405/409 that names the canonical action (e.g. "use action create_team"), while leaving read (get/list) open (the UI needs it).
Also reconcile the contradictory metadata: either derive apiMethods from managedBy, or fail object registration when an externally-managed object advertises generic write verbs.
Open design points
- Granularity: refuse all of create/update/delete? (My instinct: yes; reads always open.)
- Generalize
managedBy: 'better-auth' into a broader capability (externallyManaged / writeVia: '<action>') so third-party-managed objects get the same treatment, not just better-auth.
- Enforcement layer: REST router (cheap, per-verb) vs. ObjectQL engine (defense-in-depth, covers non-REST callers). Engine-level is more robust.
Related
- Provenance issue (system-field skip-by-name) — same
managedBy/injection boundary (linked below).
- Error-mapping safety net: branch
fix/rest-map-schema-errors.
Found running cloud LOCAL-E2E-CHECKLIST B7.
Summary
managedBy: 'better-auth'is documented as "generic CRUD is suppressed — use the action endpoints" but nothing enforces it. The generic data API will happilyINSERT/UPDATE/DELETEagainst an externally-managed table, bypassing the owning subsystem's invariants, hooks, and authz path.Evidence
packages/platform-objects/src/identity/sys-team.object.ts:packages/objectql/src/engine.ts— nomanagedBycheck in the insert/update/delete path (assertWriteAllowed()only gates external datasources).packages/objectql/src/registry.ts:~189—managedByis only used to skip system-field injection (if (schema.managedBy === 'better-auth') return schema), not to block ops.packages/rest/src/rest-server.tscreate handler callsp.createData(...)directly; no metadata/apiMethodsgate.Result: a bare
POST /api/v1/data/sys_teamruns a generic INSERT. The canonical path is thecreate_teamaction →POST /api/v1/auth/organization/create-team, which lets better-auth ownid/organizationId, enforce org-membership invariants, and apply its own authz.Why this matters (not just an error-message bug)
sys_teamrow better-auth doesn't know about, or one that violates its team↔org consistency / cascade-on-delete assumptions./dataroute uses ObjectQL RLS. These are not guaranteed equivalent.Proposed change
Make
managedBy(external owner) a real capability gate: for objects whose writes are externally managed, the generic data API should refuse create/update/delete with a clear405/409that names the canonical action (e.g. "use actioncreate_team"), while leaving read (get/list) open (the UI needs it).Also reconcile the contradictory metadata: either derive
apiMethodsfrommanagedBy, or fail object registration when an externally-managed object advertises generic write verbs.Open design points
managedBy: 'better-auth'into a broader capability (externallyManaged/writeVia: '<action>') so third-party-managed objects get the same treatment, not just better-auth.Related
managedBy/injection boundary (linked below).fix/rest-map-schema-errors.Found running cloud
LOCAL-E2E-CHECKLISTB7.