Goldie Blocks

Folios Schema

The unified folios and folioVersions tables — one recursive drawer of folios with a kind discriminator, the app's only content tables since the GB-152 Stage B cut-over

Phase scope: [app]. These are Convex tables for the hosted app.

Authority: the settled shape — storage, the twenty-nine binding constraints and the printed About card — is owned by the decision record The Folio Schema (2026-09-12). This page owns the read and write contracts and the executor catalogue.

A folio is one card in one drawer. The three near-identical tiers — documents, modules and blocks, each with its own object table and its own version table — collapsed into one recursive folios table plus one folioVersions table. What tier a card is is a word on the card (kind), not a separate drawer.

In plain terms: the filing cabinet had three drawers holding almost identical cards, so the same furniture — labels, dividers, cross-references — had to be built and maintained three times. The redesign puts every card in one drawer and writes its sort on the card. Adding a new sort later (a project, a workspace) then costs a word and a nesting rule, not two more drawers.

These are the only content tables

Since GB-152 Stage B (2026-09-03) the three-tier tables are deleted and every screen reads and writes through the Folio layer. What is not built on it is listed under Not yet true rather than left for a reader to infer from the tables.

Libraries. The Library's tables and rules are consolidated in Libraries (2026-09-25): an active libraryPins entry is the only membership truth, and a Folio's list of Libraries is printed from it.

Workspace and Project storage

Workspace and Project are kind values in folios, not separate tables. Both carry the normal key, handle, owner and live headVersionKey. Project's parentFolioKey names its Workspace; Document's names its Project. Ordinary folioPins on each container's live version carry the ordered child listing. An optional browserClientId on the Workspace root still finds a browser's Workspace when Home provisions it; it no longer decides access. Access comes from folioUsers: each new folio's owner is the creating browser's anonymous user (GB-309), and the seed actor remains only for the unscoped seed.

folio/home.container reads one explicit container key. home.route resolves an explicit route key and home.parent projects the authorised parent for the Work Bar. Their shared check (browserCanReadFolio, containerAccess.ts) reads Workspace, Project and root Document alike through the browser's folioUsers row on the folio's root; a Document's own edits still bind to its session. Workspace is Home and lists Projects. folioVisits stores browser, Folio key, kind and last-opened time as visit metadata, never containment or a second content list. Home does not display recent Documents. home.visit records opens without editing a Folio or its parent.

The home mutations provision the initial containers, add Projects and edit container Label/guide through this temporary door. Document creation requires an explicit authorised Project and pins the new Document there atomically. Workspace and Project operation-target entries remain undefined; the ordinary operation resolver still refuses them. The exemption registries and tests name the interim door explicitly. The older executor count below predates this extension; GB-10 replaces browser bindings with authenticated authority.

Authority: The Workspace Is Home; the Project Is the Sharing Grain.

Export filename — derived, never entered

Owner ruling, 2026-09-09 (superseding GB-206 of 2026-09-08): a download name is {label} - v{versionNumber}, derived at read time, and there is no way to enter one. The "Set file name" row, the write-file-name operation and its executor are gone; the About card's identity: no longer carries file_name. The folioVersions.file_name column stays declared so rows written before the ruling still validate, but nothing reads it. Renaming the folio renames every edition's file. Amended by owner ruling 2026-09-22 — a sealed edition records the name it was filed under: the name is resolved once, at sealing, into folioVersions.sealedName, and the filename reads that record. Renaming the folio retitles its open draft and every later edition, never an edition already filed. An edition sealed before that ruling carries no sealedName and still derives its filename from the folio's current name. See A sealed edition records its filed name.

In plain terms: the file you download is called what the document was called when you filed it, plus its version number. Rename the document and the files you file from now on follow the new name; the ones already filed keep theirs. There is nothing extra to type before downloading.

What is in the tree

In plain terms: the old cabinet has gone to the skip, both sample documents are filed in the new one, and the clerks who take edits are trained on it. What is left unbuilt is listed after the table, not mixed into it.

The settled statement of this schema as a design is the decision record The Folio Schema. This table is the evidence that the tree matches it — a routing aid, and the first thing to re-run a claim against.

FactEvidence
Six unified tables are the content schema: folios, folioVersions, folioPins, libraries, libraryPins, tags (tags retired 2026-10-04: tags are words on the version; the table stays declared, emptied by migrations:dropTagVocabulary, until a follow-up drops it). The six legacy tables (documents, documentVersions, modules, moduleVersions, blocks, blockVersions) and the interview audit table are deleted, with every handler, adapter, seed and test that read them.convex/schema.ts; GB-152 Stage B, 2026-09-03.
All canvases read the Folio tables. Document Work subscribes to the canvas snapshot and version history by route; Workspace and Project read their pinned children by explicit container key. The page adapts the recursive snapshot to the Stack's module-and-block inputs; Stack v3's anatomy is unchanged.convex/folio/reads.ts (the public reads, each checked by convex/readAuthorityGuard.test.ts), src/pages/document-work/lib/folio-canvas.ts, src/pages/document-work/ui/connected-document-work.tsx, src/pages/folio-container/ui/connected-folio-container.tsx.
Every stored member is typed — no v.any() in the envelope or the payload.convex/folio/validators.ts (folioKindValidator, folioAccessValidator, folioPayloadValidator, folioPayloadValidatorByKind, folioTagTypeValidator). The payload validators are derived from the domain's About-card schemas in packages/domain/src/folio/schema-about-card.ts (GB-182), so the card's type and the column's validator are one statement; convex/derivedValidators.test.ts guards the no-any rule. This is GB-152 AC #3, ticked.
The write invariants are enforced — nesting legality, cycle refusal, payload-agrees-with-kind, sibling handle uniqueness, same-kind derivation, one pin per parent-version-and-child.convex/folio/writeGuards.ts, convex/folio/writes.ts, convex/folio/pinWrites.ts, with .convex.test.ts coverage. Commits 788083cf, 929fd7ae.
The About card round trip — projection out of the stored envelope and write-back into it — is built and tested.convex/folio/aboutCodec.ts (readFolioSchemaAboutCard), convex/folio/schemaAboutWrite.ts (applyAboutCard). Neither is a registered mutation yet — GB-152 AC #4/#5 give them their caller.
Both corpora seed directly into the unified tables — every card with one sealed version 1 and one live head cloned from it, every container edition with its schedule of pins, a route and an authorised session per Document — and a reconciliation report fails on any breach of the invariants below or any difference from the authored corpus.convex/folio/seed.ts (folio/seed:seedServicesAgreement, folio/seed:seedLegalZebraWill, folio/seed:reconcile), the pure compiler seedCompile.ts / seedPayload.ts, the report seedReconcile.ts, and the golden fixtures under convex/folio/golden/. The never-executed three-tier migration runner was deleted with it (GB-152 Stage A, 2026-09-03).
libraries has no product writers; only Goldie writes tags, as words on folioVersions.tags and libraryPins.tags (Goldie Tags by Instruction); libraryPins has only the internal, product-uncalled publishToLibrary primitive. The seed populates none of these tables.Comments on those tables in convex/schema.ts; A Library Is a Shelf, Not a Folio.
Nineteen executors are public, plus one door — rename, delete, restore, New version (write-lock retired with the per-folio lock on 2026-09-11, Maker Fills, Builder Builds), and since Stage D (2026-09-05) Write content, Write Field structure, Write Field values, Add module, Add content block, Add data block, Remove, Move up, Move down, Change numbering, and (2026-09-06, rows 4 and 5) Write About, Write Module About, Write Document guide and Duplicate — each declared through the Folio operation builder so the prologue runs before it; and the root-creation door createDocument (documentBootstrap.ts), the one public write without a session, named as the executor-form test's single exemption (owner ruling 2026-09-06, temporary until sign-in). The card, pin and About primitives are plain ctx helpers an executor composes (GB-180); only the two seed mutations and the root-creation door remain registered outside the builder. The card menus and the canvas bind them.convex/folio/namingWrites.ts, lifecycleWrites.ts, versionWrites.ts, contentWrites.ts, fieldStructureWrites.ts, fieldValueWrites.ts, compositionWrites.ts, arrangementWrites.ts, numberingWrites.ts, aboutWrites.ts, duplicateWrites.ts, documentBootstrap.ts; the helpers in writes.ts, pinWrites.ts, aboutWrite.ts, seed.ts; folioOperation.convex.test.ts walks the modules.
The reads the card menus need are public: the canvas snapshot takes editions — the exact edition to show for any card the walk reaches, the Document included; every card's history comes back in one read keyed by folio. Both are addressed by route. The export input is addressed by the card's own folio key, so any card assembles its own edition.convex/folio/reads.ts (getSnapshotByRouteKey, listTreeVersionsByRouteKey, getExportInput), snapshot.ts (FolioEditionChoices), versionReads.ts (readFolioTreeVersions); treeReads.convex.test.ts; each read checked by readAuthorityGuard.test.ts.
Every read is declared through a Folio read builder (folioRead, folioRouteRead; the internal builder and the two callerless reads it declared were deleted by GB-189), so the read authority runs before the handler by construction, exactly as the operation builder runs the prologue before an executor. A read declares only the arguments beyond its address and receives the resolved authority as ctx.authority; listRecent is the exception, naming no folio.convex/folio/folioRead.ts; convex/folio/reads.ts; the class guard convex/readAuthorityGuard.test.ts, whose postures are the registration forms rather than a scan of each handler.
Read authority reaches the whole subtree. A session bound to a Document reaches any card beneath it by that card's own key, so Download and Versions ▸ work on a Module and a Block as they do on the Document. The reach is by containment — the card's parentFolioKey chain — which is wider than the route-addressed reads' walk of the live schedules: a card carrying a parent key but sitting on no schedule is readable by key and not by route. It stays inside the one bound Document either way, and a card under another Document is refused in the binding rule's one wording. The write authority is the same call with the actor and effective access added.convex/folio/readAuthority.ts (requireFolioReadAuthority, the ancestry walk), convex/folio/writeAuthority.ts; reads.convex.test.ts ("read authority"), treeReads.convex.test.ts ("the export input on any card").
The save lifecycle is built (GB-152 Stage C, 2026-09-04): "Save Version" for every kind seals the open draft as the next edition, descendants first, reuses an unchanged descendant's sealed edition, always appends for the explicitly saved folio, and opens a fresh draft with previousVersionKey set. Every executor is declared through the Folio operation builder.convex/folio/versionWrites.ts (saveFolioVersion), convex/folio/folioOperation.ts; the save-version catalogue row in packages/domain/src/folio/operation-catalog.ts.

Not yet true

Four things this schema supports are not built on it. Each is a scope decision, not a fault, and each is stated so a reader does not infer it from the tables.

  • The Maker interview is post-MVP. Goldie is registered and files proposals — one agent definition per folio (One Goldie Per Folio) — but the collection script that walks a form belongs to the side panel, which is Maker-only and later work (The Side-Panel Goldie Is Maker Only).
  • Three authored facts do not render. A block's flow condition and join_for are post-MVP and deliberately not migrated — seedCompile.ts lists them under "Retired, not migrated" — so a conditional clause renders unconditionally and a joined block shows its raw template. And an unset numbering flag resolves to yes once any flag is authored, so the About card's Display facet prints all four (convex/folio/numberingPlan.ts states that rule once, for both the seed and the executor).
  • libraries and libraryPins have no product path. The tables and the internal primitive exist; nothing a user does reaches them.
  • folio in src/ is UI vocabulary, and the two vocabularies have not met. The Stack renders cards it calls folios, while the page and editor models still speak moduleVersionKey and blockHandle (src/pages/document-work/model/, src/widgets/drafting-editor/model/). src/pages/document-work/lib/folio-canvas.ts is the adapter between them, and it is the seam to read before assuming either name reaches the other.

The six kinds

folios.kind is the sole kind discriminator. The payload on the version row carries no kind marker of its own, and there is no type: content | data switch anywhere — a block's nature is a kind, not a mode. type always means a field's own type.

KindMay containPayload
workspaceprojectnone
projectdocumentnone
documentmodulenone
modulecontent_block · data_blocknone — its dataSchema derives from its pinned data blocks
content_block—{ text, numbering?, repeats? }
data_block—{ fields }

Containers have no payload at all. That is a conditional invariant enforced by the mutations, not by the table layout: Convex cannot make one column's presence depend on another column's value, so convex/folio/writeGuards.ts refuses a container carrying a payload and a block carrying none.

Enforcement clause. No kind-switch outside the keyed tables. Generic code handles a folio without asking its kind; per-kind differences live in one keyed table or union per concern (folioPayloadValidatorByKind, FOLIO_SCHEMA_CONTAINMENT). An if kind anywhere else is the smell that the unification is being undone.

folios — the card in the drawer

One row per folio, versionless. App machinery — keys, pointers, lineage — sits as ordinary top-level columns and never prints on the About card; the authored facts print.

Prop

Type

Indexes on folios

IndexPurpose
by_keyResolve a folio by its stable identity.
by_parentStructural questions only — never a listing index.
by_parent_and_handleSibling handle uniqueness, and reference resolution within a parent.
by_kind_and_handleResolve a handle within one kind.
by_route_keyOpen a folio route.
by_derived_from_folio_key"What was derived from this folio?"
by_owner_and_kind"My working folios" — the working list.

The listing rule. Library lists read libraryPins.by_library; working lists read folios.by_owner_and_kind; ordered contents come from folioPins.by_parent_version. by_parent answers where does this sit, never what should I show in a list.

In plain terms: the shelf catalogue tells you what is on the shelf, and the deed's schedule tells you what that execution comprised. Neither is answered by asking a card who its parent is.

folioUsers — who works on a Folio

Current since GB-309 (2026-09-25). One row is one person's role on one root: a Workspace or a root Document. A Project answers to its Workspace's rows, and nothing beneath a root carries a row of its own.

In plain terms: the register lists who holds each file. A matter file inside a client's cabinet is held by whoever holds the cabinet; there is no separate entry for every page.

Prop

Type

Indexes: by_key, by_folio, by_user, by_folio_and_user. A second row for the same (folioKey, userKey) is refused on write (grantFolioUser, convex/folio/folioUsers.ts).

Who writes rows. Creating a Workspace (home.ensure), a root Document (createDocument), a whole-Document duplicate, and a seed run each write one owner row for the creating browser's anonymous user in the same transaction, and record the same user in folios.owner. An unscoped seed's owner is the seed actor, whom no browser answers to, unless one signed-in account exists: then the seed is that account's, filed in its Workspace (Corpus Seeding). A seed given an explicit createdBy makes that user the owner instead, even with a browserClientId, so the browser does not list it.

What it replaces, and what stays. Access no longer reads folios.browserClientId or the browser's sessions. Both columns stay for now: Home still finds a browser's Workspace by browserClientId, and a Document's edits still bind to its session. Narrowing them is a later task. Folios created before GB-309 have no row and are unreachable. That includes a browser's pre-GB-309 Workspace, which Home still finds by browserClientId, so that browser's Home refuses outright rather than losing only old items. The owner ruled that data disposable (2026-09-25): any deployment holding pre-GB-309 Workspaces is wiped and reseeded before this ships to it, not migrated.

Sign-in. The claim step (GB-10) attaches the anonymous user to the verified account, so its rows come across unchanged. Library access is a separate relationship (Libraries).

folioVersions — one save point of one folio's own content

Versions are explicit save points. Every edit lands on the open working draft (the head version) and mutates it in place; no edit mints a version. Minting is a user act. Saving a container seals every changed descendant in one click, and untouched descendants are re-pinned at their existing editions.

In plain terms: typing into a document does not create a new file every keystroke. It changes the draft you have open. "Save a version" is you deciding this state is worth keeping, the way executing a deed fixes one text rather than every draft that led to it.

Prop

Type

There is no createdAt column. The About card's identity.created_at derives from this row's implicit Convex _creationTime.

Indexes on folioVersions

IndexPurpose
by_keyResolve a save point.
by_folio_and_version_numberOrdered version walk for one folio.
by_folio_and_version_statusFind the open working draft, or walk sealed editions.

Deliberately no interview-status index. Interview progress is derived from the fields that have values, never stored. This is a departure from Status Axes, where interviewStatus is a stored axis on moduleVersions and blockVersions.

Block payloads

A block's material is the only per-kind shape in the schema, and it prints under one wrapper named for its nature — content: on a content block, data: on a data block. The word payload never prints.

content_block

The citation grammar — how text writes an @ reference, its terminator rule, and what it stores — is DSL Spec → References; this page records only where the result is kept.

Prop

Type

join_for — rendering a collection as one grammatical passage ("A and B") — is settled but not MVP, and deliberately has no validator yet. repeats and join_for will be mutually exclusive.

No stored numbering inheritance. A container's numbering control is a bulk-write onto its descendant content blocks, not a stored parent value. The module-tier display / sequence.reset inheritance chain the three-tier schema carried is gone with it.

data_block

The field contract — each declaration's type, required, collection shape and constraints — is DSL Spec → Field Declarations and Standard Types; this page records only how the list is stored.

Prop

Type

The data block is a drawer, never an address. Field keys are unique across the module; references read the fold (@data.name, @personal_details.data.name) and never name the owning block, so moving a field between data blocks breaks nothing. Two fields wanting one bare key — two people each with a name — take a composite or collection field instead.

Position lives on pins, not on the folio

Neither folios nor folioVersions stores where a folio sits among its siblings. Order lives on folioPins.order, a gapped integer (gap 1000) scoped to the parent version.

In plain terms: a save point of a container is its own facts plus a schedule — like a deed's schedule listing which annexures, at which revision, this execution comprises. The annexure does not carry a note saying "I am item 3", because the same annexure is item 3 of one deed and item 5 of another.

One shared sealed edition sits at different positions in different save points of its parent, so order can never live on the child. The reorder operation computes the scalar server-side from a semantic before/after destination; Goldie never reads or writes the raw order value.

Pins point down; children never point up. An unchanged child appears in many save points of its parent with zero new rows.

Derived, never stored

FactDerived from
Children, contents, historical compositionfolioPins.by_parent_version
Agent position context (parent, before, after)the parent version's ordered pins
identity.version and identity.created_atthe selected version's versionNumber and implicit _creationTime
identity.derived_fromderivedFromFolioKey and derivedFromVersionKey, resolved to a readable handle and version
A module's dataSchemathe fields of its pinned data blocks
ContributorscreatedBy across version history — never called authors, because the author tag means attributed source
Library standing (access.library)libraryPins
Who created a folioversion 1's createdBy
Interview progressthe fields getting values are the progress
The download filenamesealedName + " - v" + versionNumber on an edition that recorded its filed name, else label + " - v" + versionNumber at read time; never entered (2026-09-09, amended 2026-09-22)

The About card

The About YAML is the user's authored card and Goldie's read/write surface for facts about the folio. The stored shape and the printed shape were never required to agree: guide is a version column that prints top-level, order lives on the pin and prints nowhere.

Canonical top-level order:

identity:      # folio_kind, handle, label, version, created_at, derived_from
guide:
tags:
access:
logic:         # post-MVP
content:       # content_block — text, numbering, repeats
data:          # data_block — fields

Print rules: sparse (optional and empty members are omitted — absence means "not set", never a manufactured default); booleans print yes/no; children never appear, so there is no modules: or blocks: array; stored kind prints as identity.folio_kind; stored payload prints as content: or data:.

See Folio About Is Schema-Bound YAML for why the card behaves like a schema-bound interface rather than a text editor.

Worked example — a content block

identity:
  folio_kind: content_block
  handle: appointment
  label: Executor Appointment
  version: 1
  created_at: 2026-08-31T10:00:00+10:00
tags:
  owner: Jordan Smith
content:
  text: |
    I appoint @item.name of @item.address as an executor of my will.
  repeats: '@data.executors'

The card stamps once per executor; within each stamping @item.name is that executor's value. No card says where it sits — order lives on the parent's pins.

tags — words Goldie writes

In plain terms: the labels on a file are written by Goldie from the cabinet's note, never chosen from a list and never typed by a person.

tags on the About card is one flat list of words, printed as tags: words: with the owner's name after it. What the words say is set by the Library's guide, whatever the Library is for (Goldie Tags by Instruction). A card states tags read-only; a write that changes them is refused. The server holds every tag list to two rules — at most eight, de-duplicated — in checkFolioTags (packages/domain/src/library/library-guide.ts), the one check Goldie's door runs.

The working revision

In plain terms: the file's edit count. It goes up by one every time anything in the file is actually changed, and a suggestion from Goldie notes the count it was written against, so the clerk can refuse a suggestion the file has moved past.

Since 2026-09-06 the root Document's folios row carries one optional column, workingRevision (absent = 0), owned by convex/folio/documentRevision.ts. It is the concurrency baseline Stage E's proposals are filed and accepted against, because the live head is edited in place and keeps its folioVersions.key between saves — the key alone cannot say whether the prose has changed since a proposal was filed.

The rule, enforced through the operation builder (folio/folioOperation.ts) so no executor can omit it:

RuleWhere it holds
Every material write anywhere in the Document tree — a row of folios, folioVersions or folioPins inserted, patched, replaced or deleted — advances it by exactly one per transactionThe builder hands every handler a counting database writer and settles once after the handler returns
A transaction that writes nothing material leaves it where it wasA restated value stores nothing, so nothing is counted
A refused or failed transaction cannot advance itConvex rolls the whole mutation back, the count included
A multi-operation transaction advances it once for the whole bodyrunFolioTransaction — the seam a proposal acceptance composes primitives inside
A caller that reviewed a revision names it as expectedRevision; the transaction is refused as stale, before any work, when the Document has moved past itopenFolioTransaction
Sealed editions carry nothing of it; only the root Document row countsThe column is written on the Document row alone, and the revision suite proves no other row carries it

Proved by convex/folio/documentRevision.convex.test.ts: every public executor advances it by one; a no-op and a refusal do not; the transaction seam advances once for two operations and refuses a stale expectation with nothing written; and no module under convex/ writes the column but the rule's own. The builder's structural proof is folioOperation.convex.test.ts, which requires every registered folio mutation to be declared through the builder.

Deliberately conservative: Duplicate on a root Document writes the copy's rows through the source's session, so the source's revision advances too; a proposal filed against the source before the copy is refused as stale and re-filed.

Write-time invariants

These are enforced by the mutations in convex/folio/writeGuards.ts and its callers, with observed-failing-then-passing tests. Two of them are invariants the three-tier schema got for free from its table layout, and the unified schema must now state out loud:

InvariantWhy it needs stating
Nesting legality — a kind may only sit inside a kind that may contain itOne recursive table can hold a document inside a block; six tables could not.
Cycle refusal — no folio inside its own descendantOne recursive table can hold this; six tables could not.
Payload agrees with kind — a container given a payload, a block given none, and a block given the other kind's shape are all refusedThe column is optional at the table level; the conditional invariant is code.
Sibling handle uniqueness — checked against by_parent_and_handleConvex does not enforce uniqueness.
Same-kind derivation — lineage may not cross kindsOne drawer means a module could otherwise claim a block as its source.
One pin per (parentVersionKey, childFolioKey) — checked against by_parent_version_and_childConvex does not enforce uniqueness.
Pins stay in the family — every pin's parent version belongs to the child's own parentFolioKeyContainment is single-parent; using a folio under a different parent mints a derived identity, never a cross-parent pin.
The working revision advances once per material transaction — see The working revisionA counter nobody can forget to advance is one the builder advances; an executor that could skip it would let a stale proposal be accepted.

The unified read and write layer

These contracts are built and tested under convex/folio/ and are the facts a caller may rely on. They were settled while the layer was built (2026-09-01 to 2026-09-02); the commits that settled each carry the reasoning.

In plain terms: these are the clerk's standing instructions. Every screen goes through this clerk, so a caller that finds a different answer has found a bug, not an exception.

Reads (convex/folio/reads.ts and the modules beside it):

  • Read authority is the bound Document or anything beneath it — an ancestry walk up the card's parentFolioKey chain, bounded by the same depth the cycle guard uses, admitting the card when a Document on that chain is the one the session binds to. Sessions bind to Documents alone, so a session naming a Module binds nothing. Every failure is refused in one wording, so a caller learns nothing about whether a key exists (readAuthority.ts, documentSessionBinding.ts — GB-110's rule, one home).
  • Children come only through pins, in by_parent_version index order; no client re-sort, and no if kind anywhere in the snapshot (snapshot.ts, schedule.ts). A reader may choose an edition per card (editions): the chosen edition replaces the pinned one for that folio and its subtree is read at it, the Document's own choice included; a chosen edition that is not the folio's is refused, and a choice for a folio the walk never reaches is never consulted, so the map cannot read outside the Document.
  • Every card's history is one read (readFolioTreeVersions), keyed by folio, newest first, following the live schedules — the version menu on every Stack (owner, 2026-09-04). That is its limit: a card rendered from a chosen sealed edition whose folio the draft no longer schedules has no entry, and the screen offers it no version menu. Beneath a sealed edition no editions choice is consulted, so a filed edition always shows the schedule it sealed.
  • dataSchema is a lookup, not an order. Convex sorts a record's keys on the wire; interview order is each pinned data block's own fields list.
  • A headless folio reads as null, exactly as the legacy canvas answers a Document with no head — the read layer does not invent a head.
  • Progress derives from the fields (progress.ts, rules in packages/domain/src/interview/progress.ts). Two absences are kept apart: a folio that asks nothing contributes no status and is dropped from the fold; a status that could not be read counts as not started.
  • The recent-Documents list is scoped by the session binding alone plus an assertion that the folio is a Document; the legacy reuseType === "working" filter has no successor (recentReads.ts).
  • The export input names one exact edition — folioVersionKey is required, never defaulted to the head — and hands the renderer the edition, the resolved display name, the export filename (label ?? humanize(handle) plus - v<N>), a sealed flag and one kind-generic content tree in pin order (exportInput.ts). The guide never reaches a renderer. access, editedAt, createdAt and routeKey are dropped as machinery; the node is a pick of named members, so a member added to the snapshot later does not reach a page by default. sealed is a fact, not a refusal: true only when the selected edition and every edition its schedule reaches are versioned.

Writes (convex/folio/operationPrologue.ts and the executors):

  • Write authority is the read authority plus the actor and the effective access — writeAuthority.ts calls requireFolioReadAuthority rather than asking the binding question a second way, so the rule has one home. The actor proxy is the bound Document's owner.
  • Every executor walks one prologue: authority → open live head → trusted facts read from stored rows → requireFolioWritePolicy. The executor rules settled on the way (a live head is required for every kind, relationship means scheduled under the parent's head, sibling position comes from the parent's head schedule) are recorded in Folio Operations Resolve Once.
  • Rename patches the folios row only, and that is still the single store of the live name — but since the owner ruling of 2026-09-22 it is unsaved work: the save gate compares the folio's resolved name against the last sealed edition's sealedName, so a rename alone offers Save Version at the card and at every container above it. It stays outside Discard Changes. Both uniqueness rules — label and handle — read one sibling population, the other folios sharing this folio's parentFolioKey through by_parent, so they cannot disagree. The handle is prefilled only while the folio wears its minted placeholder, and that flag clears once. Restating the current name writes nothing (namingWrites.ts; rules in packages/domain/src/naming/label-write.ts).
  • Write-lock is retired (owner ruling 2026-09-11, Maker Fills, Builder Builds): a Maker never edits a block, so there is no per-folio lock to write. The access column's normalisation — an empty object is never stored and a role never twice — still lives once in accessColumn.ts, called by the About codec for hidden_for.
  • Delete and restore mark the owned subtree with one deletion timestamp after confirmation. Restore clears only the same deletion cohort; earlier independent deletions remain. Each affected Document advances its working revision (lifecycleWrites.ts, deletion.ts). Delete is a Folio fact, not a placement (owner, 2026-09-04, Delete Is a Folio Fact): the live snapshot omits a deleted child beneath a live draft — from the children and from the module's data fold alike — and omits nothing beneath a sealed edition; the child's live pin is never touched, so Put back returns it to its place with no placement write (snapshot.ts; GB-190).
  • Save Version (save-version, every kind) walks the live schedule bottom-up: a changed descendant is sealed first, an unchanged one contributes its last sealed edition; the folio's own authored state (guide, tags, access, payload) and its schedule of sealed child editions are compared with the head's previousVersionKey edition, and an equal pair reuses the edition for descendants; the explicitly saved folio always appends a checkpoint. The live row becomes the sealed edition, its pins freeze onto the children's sealed keys, and a fresh live head — numbered next, previousVersionKey set, cloned authored state, the machinery members changeNote and editedAt not carried — is opened with pins following the children's current drafts; every live schedule that pinned the sealed draft is repointed at the fresh one. The whole cascade is one transaction (versionWrites.ts).
  • Restore Version appends a saved state to the same linear history and reopens the working draft. restoredFromVersionKey is optional provenance on the new checkpoint; previousVersionKey stays chronological. Deleted children in the saved schedule are reinstated. Names and ownership are unchanged. The complete behaviour and header rule are governed by Saved Versions Are Immutable (GB-196); no fork or new document is created.
  • No Set file name (owner ruling 2026-09-09): an edition's download name derives from the label and version number and is never entered; the former write-file-name executor is gone. The save records descendants whatever their lock state — a lock guards edits, and a save changes none. A deleted child is the one asymmetry (GB-190): the cascade does not seal its draft, drops its pin from the schedule being sealed, and writes the same pin — same order, same live head — onto the fresh head; a baseline that pinned the now-deleted child is a schedule change, so that save mints.
  • Write content (write-edit facet content) patches the live head's payload.text, keeps numbering and repeats, advances editedAt, and writes nothing when the text is restated; on a field-bearing block the prologue refuses it in GB-13's words and the row set is untouched (contentWrites.ts).
  • Write About (write-edit facet about), Write Module About (write-module-about) and Write Document guide (write-document-guide) are the About face's three rows, one per kind, each carrying what the About face a person edits may write and nothing more (owner ruling 2026-09-06, about-face-writes-identity-and-guide.mdx): the block row takes the handle and the guide, the other two the guide alone. A block's name is not an About argument — the face's name row hands off to rename, so label uniqueness has one rule — and a block's material cannot arrive through these arguments at all, which is how the canvas door refuses it. The handle is stored as typed (a value that is not its own normalised form is refused, not rewritten), must be free among siblings (siblingHandleTaken, the one home for that question), and a hand-set handle clears placeholderHandle permanently. The guide lives on the head: writing it advances editedAt, blank clears it, a restated guide writes nothing. Goldie's door to the material stays the About seam (schemaAboutWrite.ts, applyAboutCard), registered with her tools in Stage E (aboutWrites.ts).
  • Duplicate (duplicate) copies a module or a block, whole and fresh, under the owner's rules (2026-09-06, duplicate-copies-the-live-draft.mdx): recursively, every visible descendant beneath the copy and a deleted one left behind; collected answers and skipped marks cleared, everything authored kept — labels, guides, questions, required marks, numbering, repeats, the lock, placeholderHandle — with each copied field minted a fresh fld_ key under its kept handle (duplicatePayload, domain); named the way a new card is — the next free sibling suffix on the handle and the displayed name plus (copy), counting on where a sibling already carries it (duplicateHandle, duplicateLabel, domain); the source is always the live draft and the lineage names it (derivedFromFolioKey, and derivedFromVersionKey the source's head at the moment of copying); every copied folio is one live version 1 pinned into its parent's live schedule, the root copy immediately after its source; one transaction. A root document goes through the creation door (constraint 25, point 6): the copy is filed detached under a minted routeKey with an authorised session for the browserClientId the call names — refused when none is named — owned, with an owner row in folioUsers, by that browser's anonymous user, and the Container Screen lists it for that browser (duplicateWrites.ts).
  • New document (documentBootstrap.ts, createDocument) is the interim browser door (owner ruling 2026-09-06, root-document-creation-interim-browser-door.mdx): for the browserClientId it is handed it files, in one transaction, exactly the rows the seed files for a Document — the folio with a minted routeKey under the placeholder handle document (document_2, … among the root documents), sealed version 1, a live head numbered 2 chained to it, and one authorised session bound to the Document and to the browser — and answers with the folio key, the route key and the session id; the Container Screen then opens the canvas fresh. owner and createdBy name the browser's anonymous user, minted on the browser's first creation, and the same transaction writes that user's owner row in folioUsers (GB-309). Anyone with the app open can create documents — the recent list's own posture — and sign-in (GB-10) deletes this door rather than keeping it.
  • Change numbering (write-display property numbering) patches the live head's payload.numbering and advances editedAt, keeping text, repeats and an existing stored format. The argument is the whole authored display — { start_level?, numbering?: { resets?, headings?, paragraphs?, lists? } } — sparse, and it replaces the stored object as a whole: a member absent from what the card submits is a member removed from the folio, so an empty display clears the numbering rather than leaving the last answer standing. What a blank answer means is stated once, in the pure numberingPlan.ts (resolveNumberingFromDisplay), which the unified seed compiles authored corpus blocks with too: level is start_level, restart is resets ?? false, the three participation flags default to yes, and the whole object is omitted when nothing at all is authored. format has no argument — absence is the app default and the string default is never stored. Numbering is a content block's material, so the resolver hides the row on a fields payload with unsupported-payload exactly as it hides write-edit facet content, and the executor refuses it in its own words with the row set untouched; no container bulk control exists on the canvas, so none is built (numberingWrites.ts, numberingPlan.ts).
  • Write Field structure (write-edit facet fields) and Write Field values (write-field-values) are two rows, two executors and one gate (fieldWrites.ts): the caller states what changes — add a field (a fresh fld_ key and a type-named placeholder handle), rename, retype, move, delete, set required; answer, clear, skip, unskip — and the gate parses every entry of the resulting list with fieldEntrySchema before it patches, so the rules a Convex validator cannot carry (minItems ≤ maxItems; a required field is never skipped, read from either end) are enforced by storage and stated once (GB-172 criterion 5; the authoring parser's own bounds check is gone). Duplicate keys and handles are refused; restating the list writes nothing; answering drops skipped.
  • Add module / Add content block / Add data block (the three insert … source: new rows; compositionWrites.ts) create the child and pin it onto the parent's live head in one mutation, so a child never exists unscheduled. Where the new card lands is one rule, resolveInsertionSite, read by the card menu and the executor alike: inside the subject where it holds the child (append, or after a visible sibling); otherwise directly below the subject on its parent's schedule (a block below a block, a module below a module); otherwise, where the subject closes its run — the last visible card on its schedule — below the subject's own parent (a module below the module whose last block the menu was on). The containment table decides each rung, not the kind; a card in the middle of its run carries no offer upward. The prologue reads the parent's own relationship one tier up for this (relationship.parent). The anchor must be a visible sibling (a deleted one is refused, constraint 23); the parent's head must be live; a new content block seeds numbering from the content block immediately before it; the handle is a minted placeholder, unique among siblings. The primitives are the plain helpers createFolioRow and pinChildRow.
  • Remove (arrangementWrites.ts) deletes the subject's pin from its parent's live head schedule and nothing else — the folio, its versions and its parentFolioKey stay, so it remains findable; it is resolvable only where the folio is shown as a child (constraint 19), and since 2026-09-09 no card menu binds it — the menus offer one Delete. Move up / Move down take an optional before/after anchor naming a visible sibling, else step over the adjacent visible one; only the moved pin's order changes, so a deleted sibling keeps its place; a placement against the row's direction is refused. The prologue counts siblings a person can see, so "Move up" is disabled when the only predecessor is deleted.
  • Refusals are stated once in writeGuards.ts — sealed head, taken handle, a lock naming builder — and shared by the executor (which throws) and the About write (which returns the same words as data).
  • Every executor is declared through folioOperation.ts (GB-173), so the prologue runs before its handler by construction; every one is internal, touches no legacy table, never dual-writes, and its refusal tests assert the whole stored row set is unchanged.

What this schema deliberately does not carry

Each of these was a column on the legacy three-tier tables. None survived, and each has a stated replacement — so re-proposing one is re-opening a settled question, not filling a gap.

  • uiMode — use roles.
  • The generation/payment cluster (requiresPayment, paymentStatus, documentGenerationType, defaults, modes, output) — express payment gating with a data field plus logic.condition.
  • Card-level createdBy — use owner; version authorship remains createdBy.
  • reuseType and its indexes — library standing derives from libraryPins.
  • searchText and its index, writer, backfill and endpoint.
  • display.style and its catalogue operation — styling belongs to export and rendering.
  • documentLifecycle and its machinery — use guide for a non-rendered instruction and logic.condition for conditional inclusion.

On this page