Permissions and Visibility
Roles, capabilities, and the single venue — who may see and change what, and which control carries it.
The URL keeps its old name
The route stays /docs/workflows/permissions-and-surfaces because a large
number of docs, tasks and code comments cite it. The page's subject is roles,
capabilities and visibility; "surfaces" survives in the slug as history, not
as a live concept.
This page is the authoritative model for three questions the app used to conflate: who you are, what you may do, and what you are looking at. The first two are roles and capabilities. The third stopped being a concept when the second venue went — there is one thing to look at, the document. Access Metadata carries the separate sharing, hard-lock and inheritance contract.
Roles
Roles are RBAC, granted by the folio's owner and stored on its access record. They form a seniority ladder:
Reader < Maker < Builder- Reader — reads and comments. Names the activity with no status implication, which matters for a counterparty lawyer or a beneficiary reading a will.
- Maker — completes a document. The person filling it in is making it: the data is what turns a template into their instrument.
- Builder — authors structure. The distinction from Maker is make with vs make the blocks — a Maker makes a document from blocks; a Builder makes the blocks.
Rules:
- Real role comes from the access record; it is never self-selected.
- Preview-as simulates downward only. While previewing, capabilities and module chrome collapse faithfully to the previewed role. The role rail names that effective rung; cycling back to the real-role rung exits preview.
- Higher roles are request targets. The rail keeps all roles visible in seniority order. Activating a higher rung sends the owner an access request naming that role, and the long segment stays on the effective role until access is granted. Access is granted only by the owner; a plan change alone never alters access.
- Request-up requires a grantor. On platform-owned documents the consumer holds Maker and structure is owned by the platform, so higher roles are visible but locked: the ladder stays legible, but no request affordance renders, because there is no owner who could grant it.
- Role names are platform vocabulary, not document secrets — listing higher roles as request targets is fine. What stays hidden is document structure (blocks a role cannot see).
- Builder the role is not a mode. The retired mode system had a Build mode; Builder is a rung on this ladder. Different axes — see Access Metadata.
Built open, locked down later
Settled 2026-07-25, and still the position. The app is built Builder-first with permissions effectively open, and restrictions are added afterwards. This is deliberate, not unfinished: which blocks are load-bearing is discovered by authoring real templates, not by reasoning about roles up front, and all current document data is dummy data, so an open default costs nothing. Preview-as makes it workable — a Builder sees the restricted experience without the restriction being committed.
Reader, Builder and, since 2026-09-11, Maker are all settled (Maker Fills, Builder Builds). The open default is about enforcement, not about which role holds what.
Reader spine interaction
Owner ruling, 2026-09-06. Reader hides structural spines at rest. A hover-capable fine pointer reveals the same resting spine as Maker, on only the innermost hovered item; content and layout never move; touch leaves it hidden. The geometry is the spine's own — see the Stack stories.
The marker is passive: hover and click do not select, open controls or start a drag, and pointer exit leaves no selected marker. Reader canvas selection and its keyboard interception are disabled; native links, scrolling, keyboard focus and text selection remain. There is no scroll-tracking bookmark or stored reading position.
Reader's comment capability is retained, but comment UI is a future feature
and does not currently need canvas selection. The Reader Inspector shell may
open; it has no agent content and no selected-item tool. A future comment
interaction must define its own targeting rather than making this marker
active.
Capabilities
Each role maps to a capability set. The mapping is a single pure module consumed by both UI gating and server-side clamping — there are no per-place edit flags.
| Role | Capabilities |
|---|---|
| Reader | read, comment |
| Maker | read, comment, write-data, use-agent |
| Builder | read, comment, write-data, write-structure, see-code, see-modules, use-agent |
write-data— fill fields, correct data values in line, and Save Version. Since 2026-09-11 it means field values and nothing else: a Maker does not edit a block's text, move, duplicate, remove, delete or restore a block, or change numbering, and there is no per-block switch that lets one (Maker Fills, Builder Builds).write-structure— everything that constructs the document: add / remove / reorder fields, write and arrange content blocks, add and manage modules, classify, insert from the library, edit source.see-code/see-modules— module chrome and the corresponding inspector tabs. Code is an action on a module, not a place: nobody wants to read a whole document as code, they want to see how one module was built.comment— annotate without changing (the Figma pattern: a commercial sender circulates a document; the recipient reads and comments). The UI is a later feature; the capability is recorded now so the Reader contract is stable.use-agent— access to Goldie. Readers are agent-excluded, deliberately: the agent belongs to the working roles, both as product (a Reader's job is reading the document, not working it) and as cost control. A Reader gets no agent panel at all.
Capability names do not follow the role names. write-data /
write-structure describe the mutation, not the person; renaming them to match
roles would re-couple what the model deliberately separates.
The crisp line: filling a field mutates document data; everything else — adding a field, writing or arranging a block — mutates document structure. Every "can role X do Y?" question resolves against that rule, and there is no per-block exception: where a Maker's own words belong, the Builder places a field.
In plain terms: the Maker fills in the blanks. The Builder writes the clauses and decides where the blanks are.
There is no per-block lock (owner ruling 2026-09-11, landed by GB-230). A
Maker never edits a block, so a switch that closes or opens one has nothing to
do. The Document-tier access.locked hard stop is a different mechanism and
stays; whether it is a universal hard override is still open — see
Access Metadata.
Editing is independent of where a thing is drawn: a field is editable wherever it renders if the effective role grants the write capability. A control's presence never grants an authority the role lacks.
Server write authority
Content mutations do not accept createdBy as permission. They receive the
current authSessionId and target documentKey, then construct write authority
on the server from the stored session binding. A session bound to another
Document is refused before the kind-specific edit rule runs. Stored createdBy
remains provenance, stamped by the server when a mutation creates a row.
Current limit: this is the Builder-first adapter, not authenticated multi-user authorization. Session creation is still public and the access record has not landed, so the bound Document's owner and Builder role are the actor and capability proxy. The future claim and access-record work replaces that proxy at this one seam; it does not revisit every mutation. See Content Writes Receive One Server-Constructed Authority.
Every server write re-resolves its operation from stored facts before its kind-specific rule runs. A write a person makes immediately is not a reviewed proposal and does not call itself one: it has its own operation name and its own capability, listed in Folio Operations Resolve Once → Direct writes, which governs. Reading that table is how you answer which capability does this mutation require — it is not restated here.
Authoring a block is not inserting one
Settled 2026-07-25; for a Builder only, since a Maker never writes prose (Maker Fills, Builder Builds). Two actions with different risk profiles:
- Authoring — writing a clause. Ordinary work on one's own document.
- Inserting from the library — drawing on the curated, classified corpus.
Library content carries an authority the person filling the document did not
create and cannot evaluate; inserting a will's attestation clause is a
legally loaded act in a way typing a paragraph is not.
write-structure— Builders only, and it lives on the module menu (see Builder structure actions), never on the block menu.
One venue
Settled by owner ruling, 2026-08-25. A block does not declare where it appears, because there is only one place it can appear: the document. Verbatim: "I want to change the app to only the document surface", and on the one flag that might have survived, "Document yes is not required. All folio items land on documents now."
A flag that is universally true carries no information. display keeps
start_level and numbering; it holds nothing about placement, and kind
(content or data) decides payload, spine and editor — never placement.
In plain terms: every clause used to carry a note saying which of our two views it belonged in. There is one view now, so the note said the same thing on every clause — and a term that is identical in every contract is not a term, it is noise.
The interview stays. Only the venue declaration went. Goldie still collects field values in sequence; that collection is a conversation, not a second place a block opts into. Nothing in the interview ever read the venue flag.
The when axis went too. documentLifecycle was retired on 2026-09-01
(GB-152 Phase 1) and the unified schema has no column for it. A stage
distinction returns, if it is wanted, as a guide for a non-rendered
instruction plus a logic.condition for conditional inclusion — never as a
third filter.
The completed lineage
The venue concept compressed in one direction, four positions, and this is the last of them:
| # | Model | What a block declared | Retired |
|---|---|---|---|
| 1 | layers[] | membership of one of five layers — Guide, Content, Data, Modules, Code | 2026-07-24 |
| 2 | display.targets | four values — surface-form, surface-document, export-form, export-document | 2026-07-30 |
| 3 | display.surfaces | two booleans — {form, document}; export followed screen | 2026-08-25 |
| 4 | nothing | — every folio item lands on the document | current |
No step was ever reversed, and each removed expressive power that authoring
real templates showed nobody needed. Anyone tempted to reintroduce a
placement flag is not opening new ground — they are proposing to undo the
fourth position on a straight line, and that is an owner question, not a schema
convenience. packages/domain/src/visibility/block-visibility.ts carries the
same fact in code, at the rule it simplified.
One open question the owner has not ruled on. Form and Document survive as Goldie's two postures — agent-led collection and user-led editing — carried by navigation rather than by anything stored. Removing the posture too would decide what the composer seeds, which is a product question. Recorded as open, not settled. See The active surface shifts Goldie's posture.
Guidance is a callout, not a layer
Content that must not appear in the instrument is not authored as a block of
the instrument. The retired Guide layer's job was "visible while working,
absent from the deliverable"; with one venue it has no declarative home, and
deliberately so. Builder-facing notes belong in a guide field, which never
renders as document content (see
DSL Syntax); a firm-internal note that must not
reach the instrument is simply not authored into a circulated document.
What survives as a concept is guidance as prose drawn as a callout — an
ordinary block on the one venue, subject to the same filters as any other. It is
not a layer, a kind, a boolean or a placement, and it needs no
Guide-versus-Content boundary to police. A callout renders as a callout, so a
reader cannot mistake commentary for instrument text — which matters in a legal
document. Guide callouts also replace block description fields: because
guidance is real content, it travels with a block into the library, search
indexes it, and picker previews self-document.
Module chrome follows role
Module chrome is not a venue, a layer or a toggle. write-structure holders see
it; other roles do not — chrome is how structure is manipulated, so it follows
the structural capability. Since 2026-09-11 the same gate covers every control
on a block or module: kind marks, block menus, spines that open one, add rows
and fold arrows are structure too.
The one exception is the Document's own card: its cover sheet is drawn for a
Maker, name and guide read-only, and its mark offers the rows write-data earns
(Save Version, Export, and Share once it exists) so they can print or hand the
document on. A Maker's canvas is the paper, its fields, Goldie and that one card
(Maker Fills, Builder Builds).
A Builder who wants a clean view previews down to Maker — chrome follows
write-structure, so no separate control is needed.
The composition rule
There are exactly two visibility filters and they AND. No new ad-hoc visibility mechanism may be added outside this rule.
visible = conditionsMet AND permittedpackages/domain/src/visibility/block-visibility.ts is the rule in code, and
it governs. Two filters, not three: the where axis went with the second venue
on 2026-08-25 and the when axis with documentLifecycle on 2026-09-01,
leaving whether and who.
- conditionsMet —
flow.conditionand data/state logic only. Conditions never encode placement or lifecycle scoping. The legacyuiMode = "fill"pattern was placement masquerading as a condition; the rule survives its example — a condition tests data, and a block that should not be in the instrument is not authored into it. See the migration rule. - permitted — the effective role's capabilities.
Absence means unrestricted: every scoping field defaults to "no restriction" when null or absent. Authors write a scoping field only to narrow or widen a default deliberately.
Ceilings and defaults — which way an inherited rule runs
Settled by owner 2026-08-11. Several authored keys exist at more than one tier of the Folio family (document, module, block). They do not all resolve the same way, and the difference is not arbitrary:
| Runs | Resolves by | Keys | |
|---|---|---|---|
| Ceiling | Top down — the parent constrains | AND / intersection along the chain | flow.condition |
| Default | Bottom up — the child may vary | First setter wins, nearest tier | start_level, numbering |
In plain terms: a ceiling is a "subject to" clause — nobody can grant more than they hold, so a block inside a module that does not apply cannot itself apply, however confidently the block states its own condition. A default is an "unless otherwise specified" clause — the parent supplies a starting position and the child is free to depart from it.
The composition rule above states the filters at one tier. Ceilings extend it down the chain as well as across: a block is visible only where its own filters pass and its module's do and its document's do. Failing higher up is not overridable from below — that is what makes it a ceiling.
The test for any new tiered key, so this is not re-argued each time: does it describe whether the thing exists here, or how the thing looks? Existence is a ceiling; appearance is a default. A key that is really placement wearing a condition's clothes belongs with the ceilings.
Two definitional rules that keep it usable
- Headings are transparent. A heading renders iff its span contains at least one visible block, with transitive membership through nested spans. This is "never render empty" applied one level down, and it means headings need no annotation ever.
- Data blocks are visible when empty. Their fields are their content; values are just the fields' current state. Without this, a fresh document would render blank — every heading suppressed because no block beneath it counted as visible.
Both land in one visibility helper, not two.
The Work Bar's title slot
Versioning is an act on each card, from that card's own mark menu (owner ruling). The Work Bar carries no version chip and no lifecycle stamp. Since 2026-09-10 its title slot holds where you came from — the project's name and kind, as one link back — per Work Bar Shows Where You Stand. The document's name is on its own card, which is the title block.
Draft / Final left versioning entirely and became a payment and export
concept — the watermark on an unpaid Maker's export, over requiresPayment /
paymentStatus / documentGenerationType. Version storage is
Folios → save points and version filenames. A Maker sees
the current form, never the draft history.
The toolbar
The document toolbar replaces the retired mode toggle. It carries, on one row at every width (owner ruling, 2026-08-18):
- Role rail — a neutral three-rung carousel. The countable segments state the ladder's height; the longer segment states the effective rung. It routes through preview-down / request-up semantics (see Roles), and a request or locked rung never appears effective merely because the control was activated. It keeps its words, track width and full hit area at every width; it is never folded away behind a button.
- Overflow (⋮) — a bare vertical glyph rather than an enclosed button, so it cannot be mistaken for a second rail. It carries Outline view (a checkable item — a cheap, reversible view toggle does not earn a permanent slot in the bar), Download…, housekeeping (Rename, Duplicate) and destructive (Delete).
There is no Goldie toggle: the Control Panel opens from the selected block's own trailing magic button (✦), never from a document-wide control.
The dividing rule, worth stating because it will come up again: the title block carries what the document is; the toolbar carries the controls that state and change the effective role. A future read-only status (a lock indicator, a shared badge) joins the title; a future action joins the toolbar.
The test for text versus icon on any future control: what does a wrong guess cost? Where the cost is one obviously reversible click, an icon is enough and a menu item is usually the right home. Where a wrong guess means doing real work in the wrong place, the control carries words — and an icon that needs a tooltip to be legible is a label with extra steps.
The bar's own geometry is measured in
src/entities/document/ui/document-work-bar/; its 30rem container query
measures the bar's own container rather than the viewport, because Canvas can be
narrow beside another panel on a desktop viewport.
Inspect is an action, not a mode
Inspection is reached from the context menu on a block, a module or the document
("Inspect"). The inspector panel opens on the selected entity — it always has a
subject. Tabs within it are capability-filtered: Info for anyone who can see the
entity; Code and Module-boundary tabs require see-code / see-modules.
Builder structure actions
On block and module context menus, visible only with write-structure and
silently absent otherwise. The module chrome menu carries them on the thing they
act on: View Code, Insert Block from Library…, Insert Module from Library…,
Classify, Inspect.
- Classify — place the block or module in the legal taxonomy. Classification is agent-controlled against a predefined taxonomy schema: the agent classifies modules; blocks inherit their module's classification by default, and the agent overrides per block only where it judges the block sits elsewhere. The menu item is a review and correct surface, not from-scratch entry. Classification is what makes blocks reusable and library insertion contextual.
- Insert block… / Insert module… — a source picker with two sources: New (empty) and Search library. Search ships real: it is the product's founding use case — surface existing developed content at the right moment — ranking candidates by taxonomy proximity with guide text as the preview.
- Insertion anchors on the heading. "Add content here" appends into that heading's span, because for a Maker headings are the only visible structure.
Agent turn policy
Goldie reads the same block and field state the visibility model uses and steers each turn by a priority ladder. The ladder is agent-judged, not a state machine: there is no computed "data filled" condition — the brief describes the ladder and the agent reads field state as context.
- Empty data → collect. While required fields are unfilled, collecting is the priority.
- Data filled → offer help. Ask whether the user needs help understanding the document or editing it.
- Editing → assume continuity. After a directed edit (making a clause more pro-buyer, say), assume similar edits are wanted elsewhere and offer them.
For write-structure holders Goldie extends the ladder proactively, assisting
with structure through chat — chat is an authoring command surface, gated
on the capability.
Each turn pre-empts the next suggested prompt as a muted ghost prompt the user accepts with Tab, generated per turn from posture, selection, capabilities and turn-policy state rather than from a static placeholder matrix. The agent's suggestion space is bounded by the user's capabilities — it never offers an action the effective role cannot perform, and it may suggest a change of frame, never make it.
The active surface shifts Goldie's posture
Posture is all that survives of Form and Document
Since 2026-08-25 these are two conversational stances, not two venues.
Nothing is stored on the block, the document or the access record; the posture
is carried by navigation (?surface=) and resolved per turn. Open owner
question: whether the posture should survive at all, and what the composer
seeds if it does not. It was left standing when the venue was removed because
retiring it decides a product question the owner has not been asked.
| Posture | Stance |
|---|---|
| Form | agent-led — Goldie drives: asks in sequence, collects, keeps the collection moving |
| Document | user-led — the user asks; Goldie explains or edits in response |
The posture selects which part of the ladder is live: Form is step 1, Document is steps 2–3. This matters because field state alone gives the wrong answer — a Maker who moves to the Document posture with fields still empty is saying stop collecting, let me look, and a purely state-driven agent would keep pushing collection. The posture is context, not a state machine.
It also decides the side panel's copy: Form hands control to Goldie ("Press play, Goldie helps you fill the form"), Document invites a request ("Ask Goldie to explain and make edits"). Both initiate the same agent; Readers see neither. "Play the form" and "Ask Goldie" are verbs, not places.
The Goldie scope contract
Where you are talking to Goldie determines what she may touch. Scope is not a hint about relevance; it is the boundary of the turn. The live model is One Goldie Per Folio — one agent definition anchored to a folio, briefed on the whole document every turn, with what she may propose enforced at the filing gate on the server, never by the prompt. Reading never widens proposing. In Builder mode she is the in-folio agent on the card; the side panel is Maker only (The Side-Panel Goldie Is Maker Only).
What this page adds is how scope composes with permissions, and it composes one
way only: scope narrows what a turn may reach; capabilities decide whether it
may write at all. A Reader holds no use-agent and so has no scope of any
kind. A Maker's Goldie completes existing field declarations; proposing field
declarations, types, options, guides and DSL-backed rules is Builder work, and
the server validates the proposal against the schema and DSL before it is shown
or accepted. Builder authoring is never free-form field invention.
The UI shows one identity with its current scope path — Goldie · Document,
Goldie · Module label, or Goldie · Module label · Block label. A missing
label falls back to a humanized handle. The path reports scope; Goldie does not
invent a better label.
Deterministic failures use the Block Notification Channel, not Goldie's transcript.
Downloads and monetization (recorded, deferred)
Settled with the toolbar model, implemented later in the pricing pass.
The Final render is the product; the Draft render is the tease. Download…
is available to every role, and there is one artifact — the document — in
two renders:
| Render | What it is | Gate |
|---|---|---|
| Draft | the instrument with real data, DRAFT burned across every page, deliberately incomplete | free |
| Final | the clean, execution-ready instrument | the monetized artifact |
- Draft — free. The document with the user's real data in place ("This is my last Will and…"), DRAFT burned into the render (flattened, never a removable layer), and deliberately incomplete. The gaps are authored teaser blocks naming what is missing, while the corresponding final-only content is silently omitted. Execution blocks are final-only, so the draft carries no signature furniture. It entices precisely because it is real and visibly unfinished.
- Final. Setting a document as Final requires confirmation: it becomes the rendering of record.
Draft and Final are a stage and payment distinction, not a placement one —
which is why removing placement left them untouched. The mechanism is open:
the authored documentLifecycle axis this was first written against was
retired on 2026-09-01, so whoever builds the renders decides how a block is
marked draft-only, and a third visibility filter is not the answer (see
The composition rule).
The printable form was a consequence of the second venue and went with it. The use case was real — a user who wants the questionnaire on paper, to fill in by hand or to collect answers from someone else. If it returns, it returns as an export format decided at download time, never as a venue a block opts into. Recorded so the use case is not lost with the mechanism.
Whose plan unlocks Final follows who the customer is:
- Consumer surface — the user's own purchase unlocks Download Final, with a locked state showing an Upgrade affordance.
- Firm surface — the firm's premium plan covers its audience. Recipients of a premium firm's documents download Final at no charge with no upsell shown to them. A free firm's recipients see Final locked with "contact the document owner": the upgrade pressure belongs on the firm, in the firm's own dashboard, never on the recipient.
Monetization never alters access: a plan change unlocks renders, not roles (roles come only from the owner — see Roles).
Related
- Vocabulary index and ruled-out words: Glossary
- Screen: Document Work Screen