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.
| Fact | Evidence |
|---|---|
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
flowcondition andjoin_forare post-MVP and deliberately not migrated —seedCompile.tslists 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.tsstates that rule once, for both the seed and the executor). librariesandlibraryPinshave no product path. The tables and the internal primitive exist; nothing a user does reaches them.folioinsrc/is UI vocabulary, and the two vocabularies have not met. The Stack renders cards it calls folios, while the page and editor models still speakmoduleVersionKeyandblockHandle(src/pages/document-work/model/,src/widgets/drafting-editor/model/).src/pages/document-work/lib/folio-canvas.tsis 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.
| Kind | May contain | Payload |
|---|---|---|
workspace | project | none |
project | document | none |
document | module | none |
module | content_block · data_block | none — 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
| Index | Purpose |
|---|---|
by_key | Resolve a folio by its stable identity. |
by_parent | Structural questions only — never a listing index. |
by_parent_and_handle | Sibling handle uniqueness, and reference resolution within a parent. |
by_kind_and_handle | Resolve a handle within one kind. |
by_route_key | Open 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
| Index | Purpose |
|---|---|
by_key | Resolve a save point. |
by_folio_and_version_number | Ordered version walk for one folio. |
by_folio_and_version_status | Find 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
| Fact | Derived from |
|---|---|
| Children, contents, historical composition | folioPins.by_parent_version |
Agent position context (parent, before, after) | the parent version's ordered pins |
identity.version and identity.created_at | the selected version's versionNumber and implicit _creationTime |
identity.derived_from | derivedFromFolioKey and derivedFromVersionKey, resolved to a readable handle and version |
A module's dataSchema | the fields of its pinned data blocks |
| Contributors | createdBy across version history — never called authors, because the author tag means attributed source |
Library standing (access.library) | libraryPins |
| Who created a folio | version 1's createdBy |
| Interview progress | the fields getting values are the progress |
| The download filename | sealedName + " - 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 — fieldsPrint 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:
| Rule | Where 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 transaction | The 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 was | A restated value stores nothing, so nothing is counted |
| A refused or failed transaction cannot advance it | Convex rolls the whole mutation back, the count included |
| A multi-operation transaction advances it once for the whole body | runFolioTransaction — 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 it | openFolioTransaction |
| Sealed editions carry nothing of it; only the root Document row counts | The 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:
| Invariant | Why it needs stating |
|---|---|
| Nesting legality — a kind may only sit inside a kind that may contain it | One recursive table can hold a document inside a block; six tables could not. |
| Cycle refusal — no folio inside its own descendant | One 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 refused | The column is optional at the table level; the conditional invariant is code. |
Sibling handle uniqueness — checked against by_parent_and_handle | Convex does not enforce uniqueness. |
| Same-kind derivation — lineage may not cross kinds | One drawer means a module could otherwise claim a block as its source. |
One pin per (parentVersionKey, childFolioKey) — checked against by_parent_version_and_child | Convex does not enforce uniqueness. |
Pins stay in the family — every pin's parent version belongs to the child's own parentFolioKey | Containment 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 revision | A 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
parentFolioKeychain, 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_versionindex order; no client re-sort, and noif kindanywhere 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 noeditionschoice is consulted, so a filed edition always shows the schedule it sealed. dataSchemais a lookup, not an order. Convex sorts a record's keys on the wire; interview order is each pinned data block's ownfieldslist.- 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 inpackages/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 —
folioVersionKeyis 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>), asealedflag and one kind-generic content tree in pin order (exportInput.ts). The guide never reaches a renderer.access,editedAt,createdAtandrouteKeyare 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.sealedis a fact, not a refusal: true only when the selected edition and every edition its schedule reaches areversioned.
Writes (convex/folio/operationPrologue.ts and the executors):
- Write authority is the read authority plus the actor and the effective
access —
writeAuthority.tscallsrequireFolioReadAuthorityrather than asking the binding question a second way, so the rule has one home. The actor proxy is the bound Document'sowner. - 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,relationshipmeans 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
foliosrow 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'ssealedName, 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'sparentFolioKeythroughby_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 inpackages/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
accesscolumn's normalisation — an empty object is never stored and a role never twice — still lives once inaccessColumn.ts, called by the About codec forhidden_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'spreviousVersionKeyedition, 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,previousVersionKeyset, cloned authored state, the machinery memberschangeNoteandeditedAtnot 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.
restoredFromVersionKeyis optional provenance on the new checkpoint;previousVersionKeystays 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-nameexecutor 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-editfacetcontent) patches the live head'spayload.text, keepsnumberingandrepeats, advanceseditedAt, 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-editfacetabout), 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 torename, 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 clearsplaceholderHandlepermanently. The guide lives on the head: writing it advanceseditedAt, 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 andskippedmarks cleared, everything authored kept — labels, guides, questions, required marks, numbering,repeats, the lock,placeholderHandle— with each copied field minted a freshfld_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, andderivedFromVersionKeythe 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 mintedrouteKeywith an authorised session for thebrowserClientIdthe call names — refused when none is named — owned, with anownerrow infolioUsers, 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 thebrowserClientIdit is handed it files, in one transaction, exactly the rows the seed files for a Document — the folio with a mintedrouteKeyunder the placeholder handledocument(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.ownerandcreatedByname the browser's anonymous user, minted on the browser's first creation, and the same transaction writes that user'sownerrow infolioUsers(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-displaypropertynumbering) patches the live head'spayload.numberingand advanceseditedAt, keepingtext,repeatsand an existing storedformat. The argument is the whole authoreddisplay—{ 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 purenumberingPlan.ts(resolveNumberingFromDisplay), which the unified seed compiles authored corpus blocks with too: level isstart_level,restartisresets ?? false, the three participation flags default to yes, and the whole object is omitted when nothing at all is authored.formathas no argument — absence is the app default and the stringdefaultis never stored. Numbering is a content block's material, so the resolver hides the row on a fields payload withunsupported-payloadexactly as it hideswrite-editfacetcontent, 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-editfacetfields) 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 freshfld_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 withfieldEntrySchemabefore 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 dropsskipped. - Add module / Add content block / Add data block (the three
insert … source: newrows;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, oraftera 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 seedsnumberingfrom the content block immediately before it; the handle is a minted placeholder, unique among siblings. The primitives are the plain helperscreateFolioRowandpinChildRow. - Remove (
arrangementWrites.ts) deletes the subject's pin from its parent's live head schedule and nothing else — the folio, its versions and itsparentFolioKeystay, 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 optionalbefore/afteranchor naming a visible sibling, else step over the adjacent visible one; only the moved pin'sorderchanges, 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 namingbuilder— 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 pluslogic.condition. - Card-level
createdBy— useowner; version authorship remainscreatedBy. reuseTypeand its indexes — library standing derives fromlibraryPins.searchTextand its index, writer, backfill and endpoint.display.styleand its catalogue operation — styling belongs to export and rendering.documentLifecycleand its machinery — useguidefor a non-rendered instruction andlogic.conditionfor conditional inclusion.
Related pages
- Schema Overview — the top-level map.
- Proposals — the baseline a proposal records: the Document's working revision.
- A Library Is a Shelf, Not a Folio —
the settled boundary for
librariesandlibraryPins: a pin names an exact sealed edition, and insertion materialises an independent copy. - Folio About Is Schema-Bound YAML
- Folio Operations Resolve Once
- No Folio Card Composer
- Authoring → DSL Spec — the
@reference grammar and the field-type,requiredand collection contract and the field contract the payload must preserve.
Schema Overview
The map — every stored table, what it holds, and which page owns its detail
Libraries
The one consolidated statement of the Library — what a Library is, the tables behind it, how saving, searching, inserting and access work, the menu, and what is still open. The dated decision records are its history.