Publishing a deck — public read, and handing out what it demos
Making a talk readable by the world is two decisions, not one: who may read the deck, and how the audience reaches the thing the deck demonstrates. They have different answers, and the second one is where the damage is.
Worked on Event/SavGeneralversammlung2026/Talk (the SAV Generalversammlung 2026 talk), 2026-09-09.
Every claim below was measured against the live mesh or the source, not inferred.
1. A public surface needs BOTH grants, or half your readers are refused
Permissions inherit top-down, so one AccessAssignment at the deck root covers every slide — 22
of them here. The node is ordinary:
Event/…/Talk/_Access/Anonymous
nodeType : AccessAssignment
mainNode : Event/…/Talk ← MUST equal the scope the path encodes (AccessAssignmentGuard)
content : { accessObject: "Anonymous", roles: [ { role: "Viewer" } ] }
🚨 Anonymous alone is a trap. Authenticated users inherit Public, never Anonymous —
so a grant to Anonymous alone gives you a deck that a logged-out visitor can read and a
logged-in one cannot. Seed both, which is what Doc already does (core's
Doc/Architecture/AccessControl says so under Anonymous and Public access; it is easy to read the
Anonymous example there and stop).
Siblings are untouched by inheritance, which is the point: the Mail node beside this deck — cover
letter, bio, CV — stays private without a single deny.
2. What Viewer actually permits
Viewer => Permissions = Permission.Read | Permission.Execute | Permission.Api
No Create, Update or Delete. But Execute is in there, and it is the one to think about
before granting a surface that contains anything runnable. On a deck it is inert — a Publish/Deck
plus Publish/Slide nodes and nothing else; verify with a descendants query excluding slides and
expect zero rows. On a package it is not inert: ReinsuranceDemo and Underwriting hold 15 and
30 Code nodes, including a one-click importer (ReinsuranceDemo/Setup). A viewer's own writes
are denied, but a script that elevates to system is not.
The composer does not write where you are standing
A signed-in reader's chat composer is a per-user singleton in their own home —
{user}/_Thread/ThreadComposer (Chat.razor) — so composing from a read-only deck never attempts a
write in the deck's namespace. For an anonymous reader ResolveUserId is empty and the control
resolves to null: no composer is rendered at all.
⚠️ Unverified, and worth checking before you rely on it.
MeshExtensionsdocuments a second chain —Node.MainNode → NavigationContext.PrimaryPath → NavigationService.CurrentNamespace → the chat composer's StartThread namespace. Whether a composer opened as a side panel from a content page takes the current namespace rather than the home path was not traced. If it does, a submit from a read-only deck is a denied write, not a redirect home.
3. 🚨 A link inside a slide does not work — the whole stage is click-to-advance
SlideLayoutAreas.BuildStage attaches .WithClickAction(…) to the entire stage whenever there
is a next slide, and every view passes a real nextHref except the speaker-notes preview
(BuildStage(slide, nextHref: null)). The rendered area confirms it: the stage comes back with
isClickable: true and cursor: pointer.
So an <a href> in slide content competes with a redirect for the same click. Put URLs and codes
on a slide as readable text, not links. That is the right answer anyway — the audience is reading
a projection, or a photo of one, and will type what they see. Real controls belong outside the
stage, which is where the presenter bar's Prev/Next/Deck/Present buttons live and why they work.
4. Reach the demo with a COUPON, not a public grant
The instinct is to make the demo public too. Resist it: the tour's steps target other partitions entirely, and opening them is not a small act.
| The tour's step | Its target |
|---|---|
| 1 · the desk | Underwriting/Desk |
| 2–7 | ReinsuranceDemo/Submissions/MTPL2027 |
Both partitions exceed 200 nodes and hold 26 and 30 GitHubIssue nodes — an internal issue
tracker mirrored into the mesh. Granting the tour node alone is worse than useless: the walkthrough
renders its cards and every step is denied.
A coupon does the whole job with one node, because acquisition cascades.
DependencyInstall.EnsureEntitledTo grants an entitlement on every dependency at amount 0,
attributed dependency of {package} — and DispositionOf auto-grants only unpriced ones.
ReinsuranceDemo and Underwriting both carry no price, so redeeming on ReinsuranceDemo yields
Claims, Ifrs17, LossModelling, Pricing, Reinsurance, Training and Underwriting with it. That is
exactly the tour's surface — capped, windowed, revocable and audited, and with the issue trackers
still private.
Admin/Coupons/SAVDEMO
price 0 · plugins ["ReinsuranceDemo"] · maxRedemptions 200 · validUntil 2026-12-31
What a redeemer can and cannot do
Enrollment writes the plugin-root Viewer grant, so a redeemer gets the same read-only shape as
above. On this demo that means sign-off, sending the enquiry and taking over a claim are denied —
steps 1–4, 6 and 7 read fine and step 5's "reword it live, send" is refused.
That is the correct default, and it is also a product limit worth stating out loud: an interactive
demo for a room full of people needs per-learner copies, because two hundred readers acting on one
shared MTPL2027 submission would trample each other regardless of permissions.
5. Do not publish a code the mesh will refuse
A slide that prints a coupon is a promise. Check the coupon can actually be redeemed on the
deployment the readers will hit, not on main:
CouponContent.Tierbecame required in #1553 — a coupon naming none is refused outright.- A tier granted before that change reaches a portal activates a permanent plan, because the
older build ignores
tierDays.
Both were true at once on 2026-09-09: #1553 was merged while Store/Coupon/Source/CouponContent on
memex still read lastModified 2026-09-02. Merged is not deployed — read that timestamp before
arming a coupon or making the deck that advertises it public.