Extensions without Get
Maintainer, 2026-11-11, after installing Electronic Signature on a self-hosted instance: "it is absolutely horrific, the install process. lots of blabla get blabla 'start course' wtf?! blabla then somewhere the config. no get, just the settings and finish. no blabla" — and, on the cover: "additional page can do marketing but should never be seen".
The path he had to walk: Settings → Extensions → a shelf card with a long description, which was a link to the package's cover → Get → ▶ Start Course → find the Signing authority tab somewhere else → enter the credentials.
The rule
An in-app extension of the person app (hostedIn: "{user}/Settings") that the viewer is covered
for has no acquisition step and no course button. Its settings tab is simply there, and its one
action, anywhere it appears, is Open settings. The path is settings tab → choose → fill → done.
What does what
| Part | Where | What |
|---|---|---|
| The tab appears without Get | PluginContent.OpensByPlan (Store/Core) |
A PAID person-app extension is plan-gated by construction — no planGated declaration needed. The gating pass writes the tier-group grants (PlanGroups.TierGrant, Store/AppsOnTheInstance §3a), so a viewer whose plan covers the tier reads the package, and the extension's tab (a UiContribution gated requireAddressAccess) shows. An instance plan that covers it opens it to every signed-in viewer, as before. A lapse hides the tab, a renewal restores it. Every former PlanGated read goes through OpensByPlan: PluginGate.SeedGating, PlanAccess.PlanGatedGrantsFrom, Subscriptions.Unlock (mints nothing), the paywall, CoverContract.IsOpenToAll |
| Free extensions | unchanged | The free plan has no tier group (validate-repos.py refuses planGated on a free tier for the same reason), so a free extension keeps its one Get. Making that implicit needs a decision on free access — see Open below |
| Only a course says "Start Course" | CoverContract.StartKindOf |
ONE classification: a person-app extension is Settings; an app: true package is App; a package in the Education category or hosted in Edu/Courses is Course; everything else is App. An unknown category is never a course. The cover's primary button, the catalog card (StoreCatalogLayoutAreas.GetLabel) and the install dialog's way onward (CoverContract.OnwardLabel) all read it. Before, the rule was "an app says Start, everything else is a course" |
| One action: Open settings | PluginLayoutAreas.CoverCtaButtons, InstallLayoutAreas.InstallStepButtons |
A viewer who has the extension gets exactly one button, Open settings →, to /{viewer}/Settings/{settingsTab} (CoverContract.SettingsHref). No Start, no Get / Repair / Uninstall step, no hero links into the cover. A viewer who does not have it keeps the funnel's one plan affordance |
settingsTab |
PluginContent.SettingsTab |
The id of the extension's person-app UiContribution node. Absent → /{viewer}/Settings, where the tab sits in the navigation |
| The cover is never on the path | ExtensionShelf.Shelf |
Shelf cards no longer link to the package's cover (WithDisableNavigation); the action area is the card's only way anywhere. The install dialog, if a legacy path still opens it for an extension, lands on the settings tab (InstallDialog.OnwardHref), never on an entry point or the cover |
| No blabla | ExtensionShelf.PersonAppPage |
The Extensions tab is the shelf alone: no heading repeating the tab's label, no intro explaining a Get that no longer exists |
What this change does NOT do
- No implicit copy into the viewer's space. The task asked whether Get's copies
(
{user}/MySignatures,{user}/Skill/signature) should land implicitly on first open. They should not:Store/AppsOnTheInstance§5 — "Signing in, opening an app, being covered by a plan and rendering a page write nothing"; an app's skills and desks reach the user through access from the package partition. A package whose tab links to a per-user copy should link to the package's own page instead (Signature's tab is changed in its own package). - The package's own tab content (Signature's form-first body) is Signature's, changed there.
- Core's
Doc/Architecture/InAppExtensionsstill describes the shelf's Get as the in-app purchase and the card as a link to the cover. It needs the core change named in the pull request.
Open
- Free person-app extensions (Google, HomeAssistant, ICloud, AppleMusic, AppleWeather, MyAi)
keep one Get. Removing it means deciding that the free plan opens a package to every signed-in
viewer without a per-viewer grant — a reversal of
StandardPacks' "a free plugin is acquired on the viewer's own Get" (2026-09-30), so it is an owner call, not a side effect of this change.
Tests
PluginCoverTests.StartKind_OnlyACourseIsToldToStartACourse,
PluginCoverTests.ExtensionRow_IsOpenSettings_NeverGetOrStartCourse,
InstallDialogTests.WhenFinished_AnExtensionOpensItsSettings_NeverStartCourse,
StoreCatalogTests.PurchasedCard_OnlyACourseSaysStartCourse,
StoreCatalogTests.OpensByPlan_APaidPersonAppExtensionIsPlanGated, and the updated
StoreCatalogTests.Extensions_AnEntryIsTheCardAndThePackagesOwnAction.