Content Writes Receive One Server-Constructed Authority
Why content mutations resolve their actor and capabilities from a server-held Document session instead of accepting createdBy from the client
Decision
Every mutation that changes an existing Document, Module, or Block constructs
its write authority from a server-held session binding. Ordinary registered
writes enter with authSessionId plus the target documentKey; interview-agent
writes enter with the Agent thread ID and resolve the session and its bound
Document from authSessions.by_agent_thread_id. Both inputs normalize to one
Document-scoped authority. A client-supplied createdBy string never grants
write access and is not copied onto newly written rows.
The edit envelopes and kind-specific executors keep their existing invariants. They receive the constructed authority; they do not each re-read ownership or invent capability conditions. When a mutation corresponds exactly to a Folio operation, server enforcement reconstructs its facts and requires that resolved operation to be available before the executor runs.
Current implementation and boundary
The two server-held session inputs normalize through one authority constructor
to the same DocumentWriteAuthority. withDocumentEdit, withModuleEdit, and
withBlockEdit resolve their target kind and construct trusted Folio facts
before invoking convex/folio/writePolicy.ts. Mutation rules receive the resolved rows
and actor they need, but cannot provide access, lifecycle, relationship,
payload, lock, or availability facts.
The interview Module path additionally proves that its stored thread-bound Document version is the current live head before it constructs the Module edit view. Exact Block-agent writes no longer run a separate capability guard or construct authority twice.
Pre-binding creation from a Library source remains a bootstrap case because the target Document does not exist when its session/source guards run.
Current Builder-first adapter
This seam does not yet mean authenticated multi-user authorization. In the
current Builder-first MVP, authSessions is a server-held Document binding but
the session creation/attachment flow is public, no access record persists the
real role, and the identity-provider claim step has not landed. The adapter
therefore uses the bound Document's stored owner as the current actor proxy and
resolves Builder capabilities.
That is still materially safer and structurally important: knowing an owner key is no longer sufficient to cross a content mutation, a session bound to another Document is refused, and future identity/access work has one adapter to replace. It is not a claim that the permissions gap is closed. The access record and claim step remain owned by Permissions and Locking and the anonymous-drafting plan respectively.
Why not keep createdBy until authentication lands
createdBy is authorship data, not a credential. Accepting it from the client
lets any caller that knows an owner key satisfy the ownership guard, and it
makes every mutation carry a second answer to who is acting. Waiting would make
the access-record migration revisit every write path. Constructing authority
now gives that later work one seam while keeping the present limitation
explicit.
Boundaries
- Session lifecycle, payment simulation, seeding, backfills, and append-only event recording are operational functions rather than Folio content edits.
createdByremains on stored rows as immutable authorship/provenance. The server stamps it from constructed authority when a content mutation creates a new row.- Direct Content/About/Field-structure edits, Module output, Module About, and
lock changes do not yet have exact Folio-operation vocabulary. They still
pass through the authority seam; they must not be mislabelled as
propose-editto manufacture a resolver case.
A Local Test-User Door, Locked Five Ways
Owner ruling 2026-10-04 (option A) — a dev-only /dev/sign-in door signs an agent or the owner in as one of two seeded test users without WorkOS's hosted page. It exists only in a dev build, answers only localhost, mints one-hour tokens, is honoured only by a deployment holding DEV_LOGIN_JWKS, and only for user_dev_ subjects.
Duplicate Copies the Live Draft, Whole and Fresh
Duplicate on any card copies the working draft of that card and everything inside it, clears collected answers and nothing else authored, names the copy with the next free handle and " (copy)", records its lineage, and lands it immediately after the original. A whole document duplicates through the same door that creates one.