Rewritten 2026-07-18 after a blast-radius audit. The original proposal here was to generalize the ADR-0092 engine write guard + apiMethods reconciliation from better-auth to the system/append-only buckets. The audit disproved that as a wholesale change and narrowed the real, safe work to two latent holes (shipped in #3222). This issue now tracks the actual root cause it surfaced: the system bucket conflates two incompatible write policies.
What the audit found
For the better-auth case (#1591/#3213) the guard was clearly right: admin_full_access (wildcard, no RLS) could raw-write identity columns via the generic API. The premise for extending it — "system/append-only also mean no user writes, just enforce it" — turned out to be false in this codebase.
1. The genuinely engine-only tables are already safe. sys_audit_log, sys_activity, sys_automation_run, approval/share/notification/job/settings/secret/metadata-history are all written via isSystem: true or a service engine with no user session (audit-writers.ts:385; approval-service.ts SYSTEM_CTX; messaging/jobs service engines). A user-context guard would reject nothing they do today — pure speculative defense-in-depth. And the key audit tables already expose ['get','list'], so they're locked at the HTTP layer already.
2. append-only immutability already effectively holds. No append-only object receives a user-context UPDATE/DELETE. The only append-only UPDATE is sys_email status, under SYSTEM_CTX.
3. system is overloaded — the real bug. Several system-bucketed objects are user-writable by design, so a blanket affordance-driven guard would break them:
| Object |
Why it takes real user-context writes |
sys_user_position |
delegated admin "add position" — delegation-of-duty.dogfood.test.ts:83 POSTs under a user token; DelegatedAdminGate governs it |
sys_user_permission_set |
delegated manageBindings direct grants — showcase-permission-zoo.dogfood.test.ts:175 |
sys_position_permission_set |
suggested-audience-bindings.ts:371 writes with context: callerCtx (explicitly not isSystem) |
sys_user_preference |
user authors their own prefs (RLS self-grant default-permission-sets.ts:213) |
sys_import_job |
REST import route writes under the user session (rest-server.ts:3727) |
These conflate with the engine-only rows under one bucket. That is the defect: managedBy: 'system' means both "engine-owned, never user-written" and "platform-defined schema, admin-writable data". No single write policy is correct for the bucket.
What shipped instead (the safe slice) — #3222
Only the two objects with a genuine latent hole (write verbs their bucket forbids and no legitimate user-write path):
sys_presence (append-only, even listed update/delete) → ['get','list'] (written over the realtime path, never ObjectQL).
sys_metadata (system) → ['get','list'] (authored via metadata-protocol RPC; confirmed no /data/sys_metadata write in framework or objectui).
The remaining work this issue now tracks
Split the system taxonomy so write policy is expressible and enforceable without breaking admin-writable data:
This is an ADR-scale taxonomy decision (touches delegated administration), not a mechanical guard extension — hence tracked here rather than folded into #3222.
Explicitly out of scope
The writeVia: '<endpoint>' capability for third-party-managed objects — no such object exists today; speculative until a concrete need arises.
Related
Rewritten by Claude Code
What the audit found
For the better-auth case (#1591/#3213) the guard was clearly right:
admin_full_access(wildcard, no RLS) could raw-write identity columns via the generic API. The premise for extending it — "system/append-onlyalso mean no user writes, just enforce it" — turned out to be false in this codebase.1. The genuinely engine-only tables are already safe.
sys_audit_log,sys_activity,sys_automation_run, approval/share/notification/job/settings/secret/metadata-history are all written viaisSystem: trueor a service engine with no user session (audit-writers.ts:385;approval-service.tsSYSTEM_CTX; messaging/jobs service engines). A user-context guard would reject nothing they do today — pure speculative defense-in-depth. And the key audit tables already expose['get','list'], so they're locked at the HTTP layer already.2. append-only immutability already effectively holds. No
append-onlyobject receives a user-context UPDATE/DELETE. The only append-only UPDATE issys_emailstatus, underSYSTEM_CTX.3.
systemis overloaded — the real bug. Severalsystem-bucketed objects are user-writable by design, so a blanket affordance-driven guard would break them:sys_user_positiondelegation-of-duty.dogfood.test.ts:83POSTs under a user token;DelegatedAdminGategoverns itsys_user_permission_setmanageBindingsdirect grants —showcase-permission-zoo.dogfood.test.ts:175sys_position_permission_setsuggested-audience-bindings.ts:371writes withcontext: callerCtx(explicitly notisSystem)sys_user_preferencedefault-permission-sets.ts:213)sys_import_jobrest-server.ts:3727)These conflate with the engine-only rows under one bucket. That is the defect:
managedBy: 'system'means both "engine-owned, never user-written" and "platform-defined schema, admin-writable data". No single write policy is correct for the bucket.What shipped instead (the safe slice) — #3222
Only the two objects with a genuine latent hole (write verbs their bucket forbids and no legitimate user-write path):
sys_presence(append-only, even listedupdate/delete) →['get','list'](written over the realtime path, never ObjectQL).sys_metadata(system) →['get','list'](authored via metadata-protocol RPC; confirmed no/data/sys_metadatawrite in framework or objectui).The remaining work this issue now tracks
Split the
systemtaxonomy so write policy is expressible and enforceable without breaking admin-writable data:sys_automation_run,sys_job,sys_notification) and platform-schema / admin-writable data (e.g. the RBAC/delegation trio,sys_user_preference,sys_import_job). Options: a new bucket value, or an explicituserActionsdeclaration on the writable ones soresolveCrudAffordancesreflects reality.isSystembypass.apiMethodscontradictions on the writable objects by declaring their true policy (not by stripping verbs they legitimately need).This is an ADR-scale taxonomy decision (touches delegated administration), not a mechanical guard extension — hence tracked here rather than folded into #3222.
Explicitly out of scope
The
writeVia: '<endpoint>'capability for third-party-managed objects — no such object exists today; speculative until a concrete need arises.Related
Rewritten by Claude Code