A node-level permissions protocol with a pluggable storage-layer ACL, so the framework can ask "can this caller do this here?" without baking specific roles into call sites.
STRVCT currently has no first-class concept of who-can-do-what. Permission checks are scattered and ad-hoc: a sync method might consult an isAdmin flag; a drop target might just accept anything; a "save" path might silently swallow writes that the backend will reject anyway. Each new feature reinvents the gate, usually as a role check coupled to whatever auth provider is in play (Firebase custom claims today, something else tomorrow).
A concrete failure mode this produced in one application: a non-admin user dragged a JSON file onto an admin-only catalog. The local model accepted the mutation, the dirty flag fired, a "save" animation ran (because an unrelated dirty subsystem was synced), and the change was silently dropped at the sync layer — the user only discovered the loss on the next reload. Two layers needed gates and neither was generic.
A capability-style protocol on SvNode, asking the write/read/delete question rather than the role question:
callerCanRead() — default truecallerCanWrite() — default truecallerCanDelete() — default callerCanWrite()callerCanShare() — default falseNodes that need gating override the relevant method. Subnode containers gate drops, the inspector hides edit affordances, the sync layer asks the same question before pushing. The model never asks "is the caller admin?" — it asks "can the caller write here?"
The default node implementation walks up the parent chain to find the enclosing scope-root and delegates to a scope-resolver:
scopeRoot.callerHasRole("editor" | "owner" | "member" | "viewer")_members collection) for the current authenticated userThe mapping from "what does write require here?" to "is the caller's role sufficient?" lives once, at the scope level, not at every gate site. Switching a scope from open to admin-only is a one-line policy change, not a hunt-and-replace through gate sites.
The scope-resolver talks to the storage layer through a thin abstraction so the answer is consistent locally and remotely:
SvStorageBackend.callerHasRoleOnScope(scopeRootId, role) — asyncFor backends without real auth (local-only IndexedDB), the implementation returns true for every role check, because there's nothing to gate against.
Failed writes shouldn't be silent. The framework needs one consistent path for "the model rejected this":
SvNotification) like SvPermissionDeniedNote with the action and target, observable by views to surface a toast/panelcallerCanCreate(childClass) as a separate hook from callerCanWrite(), so a node can permit edits to itself while restricting which kinds of children may be added.