Apps live on the instance, not in your home
Maintainer, 2026-10-10:
- "Apps should be globally installed, not under user account. User should see apps depending on his roles and functions (access control). For each user, let's store apps in cache for like 5 min sliding scale."
- "remove stuff from home. only actual user data will be stored in home, but not app"
- "remove install in this sense, it is not robust"
- "plan well and migrate across the fleet. see we don't have a gazillion apps appearing. what is settings tabs should be in settings apps, what is examples should be in examples app. make nice app design."
- "user data can still be installed. courses, when we start them."
- On the design (mockups linked from the pull request): "app screen, like it, make search (filter) bigger. personal: like design. we should offer the connect to mail / calendar, home entertainment, ... in setup phase. not just google, but any email, linkedin, ..." and "maps => this must go to instance admin app. in user examples, we should see examples how to use them."
Status: plan, phase 0. This page is the design of record for the migration. It builds on Settings extensions (plans only, Manage apps, three settings homes). It retires the per-user half of Installing and of the home surface. Plan → access, the access-check half, is carried by a parallel change (see §3).
1. What is wrong today — measured
An app is already stored once, as a partition on the instance (Signature/, Crm/, Edu/).
"Install" then copies part of it into every user's home and writes bookkeeping beside the copy.
Measured with get @{user}/* for one long-standing account on the control instance, 2026-10-10:
| In the home | Count | Written by |
|---|---|---|
{user}/{Package} "X — my workspace" roots |
~50 (WebSearch, Mail, Grok, Cursor, Stripe, Hosting, Radzen, …) | Localizer.BuildHomeRoot |
{user}/_App/* tiles (InstalledApp) |
one per app | Localizer.RegisterAppRecord, core SeedDefaultAppsLogonAction + SeedInboxAppLogonAction, InstanceAppTileLogonAction ({user}/_App/Instance), Edu CourseAppTile, SocialMedia ProfileAppPin / ProfileTilesEnsure |
{user}/_Install/* provenance records |
one per package | Localizer.WriteManifest |
{user}/Agent, /Skill, /Harness |
copies of every package's agents, skills, harnesses | installPaths fixed segments |
{user}/{Course}/… |
whole course trees, e.g. 211 nodes of AgenticEngineering | Localizer.Localize via Store/InstallRequest |
{user}/_InstallRequests/* |
one per first-open | Edu EnsureCopy, StartExercise, GoToMyCopy |
every user's AiSettings |
package /Agent + /Skill queries appended |
AiSourcesInstallHook (on every partition install) |
The copy machinery. It has four copy engines: Localizer.Localize, ItemRestore,
StandardPacks.EnsureFor* and the Reinsurance demo importer. About 20 trigger sites call them.
Two of those triggers are page renders: StandardPacksOnboarding.EnsureForViewer on the
catalog/home render, and Edu EnsureCopy on the lesson render. Keeping the copies converging takes
Store/Installer, about 8,900 lines, plus about 320 node-test methods.
Every way it has failed is a property of copying. One per-user state machine per app per home gives one chance per viewer per app to go wrong:
- snapshots that never refresh (AgenticOffice);
- a Reset that deleted nothing because an empty read meant "Anonymous" (211 nodes kept);
- a verify that failed 42 times on
Agent/Voicevsvoice(MeshWeaver#4817); - tile names, icons and groups that drift and need a mesh-wide heal sweep;
AdvancedBusinessRules · 1/17 · 1/14 · 1/14 …;- a free-pack sweep that minted grants for 60+ users;
- unrecorded learner copies nothing can update (#1606).
That is what "not robust" means here, and the fix is to stop copying apps, not to copy more carefully. Only material the user explicitly starts (a course) is still copied, because that copy is the user's own work (§5).
2. The model in one table
| Question | Answer |
|---|---|
| Where does an app live? | Once, on the instance, in its own partition. Add and Remove are the instance admin's acts (SystemInstall, Admin › Manage apps). There is no per-user step. |
| Who sees it? | Whoever the access check lets read it: plan coverage AND the app's optional audience (roles and groups), plus explicit grants (§3) |
| What does the launcher show? | AppDirectory.ForViewer(viewer): the visible apps, computed live and kept warm per viewer for 5 min sliding (§4) |
| What is in my home? | Only what I made, what I started, or what is about me. A started course is mine. No app content, tiles, workspace roots, or agent/skill copies (§5) |
| How many apps? | About a dozen, from 111 packages. Settings go to the settings apps, examples to the Examples app, modules to their suite. A roster gate keeps it that way (§6) |
3. Who sees an app — access, not install
visible(app, viewer) = app.content.app == true ← one of the roster (§6)
AND Read(viewer, app entry point) ← the ONE access check
Read on gated package content =
PlanAccess.Covers(viewer, package) ← effective plan covers package.tier
AND (app._Policy.audience is empty OR viewer ∈ audience) ← roles / functions, transitively via GroupMembership
OR an explicit AccessAssignment / publicRead _Policy ← as today, BUT capped by the audience (below)
- Roles and functions are groups. "Only underwriters see Underwriting" means the admin sets
Underwriting/_Policy.audience: [Underwriters]in Manage apps. The plan still has to cover the tier. With no audience set, everybody the plan covers sees the app. - The access check is the only gate. It answers the launcher, opening by URL, search and agents identically, so a restricted app cannot leak through any of them.
- The plan → access seam is carried by a parallel change on branch
feat/covered-plan-opens-without-get. It providesPlanAccess.Covers/PlanAccess.GrantsinStore/Licensing/Source/PlanAccess.csand folds them into core'sPermissionEvaluator. This plan consumes it and does not touchStore/Licensing,PluginGateor the cover areas. The audience field was agreed with it on 2026-10-10. - The audience is a cap, not one more grant. The gating pass writes a package's audience as
deny rows over everyone outside it, so a legacy package-wide grant cannot reopen an app its
audience excludes. One example is the
PluginGate.Enrollrow that an old Get orSubscriptions.Unlockminted. The parallel change retracts plan-derived legacy grants and keeps deliberate one-time purchases (the eternal entitlement records). - The probe is the app's entry point, not its root, because a gated root is readable by everyone
as the storefront ("root +
_-satellites").
4. The launcher list: live, kept warm 5 minutes
Core gets AppDirectory (in MeshWeaver.Graph, beside UserActivityLayoutAreas.BuildAppsBand,
which reads it instead of path:{owner}/_App):
The app roots. One process-wide query in the established package-root shape:
nodeType:(Space OR Store/Plugin) is:main partitions:all(DataModelExplorerLogic.AllPluginRootsQuery, because a root can still be indexed asSpace), filtered tocontent.app == trueand bounded by an explicitlimit. It goes throughIMeshNodeStreamCache. It is the same for every viewer.Per root,
PermissionEvaluator.HasPermission(viewer, entryPoint, Read). Its inputs are the process-wide$security-*streams, so the result is live: a grant, a membership, a plan change or a revoke re-emits.Plus the platform entries every viewer has.
- Settings (
/{user}/Settings) for everyone. - Administration (
Admin) whenIsGlobalAdmin. - Inbox.
These are not records, so
SeedDefaultAppsLogonAction,SeedInboxAppLogonAction,InstanceAppTileLogonActionand the icon-adoption logon actions are retired.- Settings (
Display (name, icon, description, category) comes straight from the package root, so there is nothing to stamp or heal.
The 5-minute cache. AppDirectoryCache is a singleton keyed by viewer, holding
Replay(1) of the live stream.
- Each subscription resets the clock.
- After the last subscriber leaves, the stream is kept warm for 5 minutes, then disposed.
- A user who comes back within 5 minutes paints instantly. One who is idle longer recomputes once.
| Property | Value |
|---|---|
| Freshness while held | live: revokes and grants re-emit, so there is no stale-but-cached window |
| Lifetime | sliding 5 min after the last subscriber |
| Scope | per process (each replica its own); its own MemoryCache instance, because the distributed host registers no IMemoryCache |
| Not authorization | opening an app re-evaluates access. The list never grants anything |
5. The home holds only the user's own data
Rule for every app author: an app writes into a user's home only in response to that user creating something. Signing in, opening an app, being covered by a plan and rendering a page write nothing.
What remains of "install" is Start: copying user-data templates into the home on an explicit
user act. A package declares them in startPaths (today's installPaths, narrowed). They are
material the user will work in, such as a course's exercises or a template workspace. They are
never the app's own code, agents, skills, guides or examples, which stay in the package partition
and reach the user through access. Start writes the copy and its fingerprint record
({user}/_Install/{package}, kept for refresh and restore). It writes no tile, no workspace
root of its own and no agent/skill copies. The copy engine stays for exactly this:
Localizer.Localize, narrowed to startPaths, with RefreshPlan and ItemRestore.
| Was in the home | Becomes |
|---|---|
{user}/_App/* tiles |
nothing; the launcher reads AppDirectory. The arrangement (group, order) moves into one user-data node, {user}/_Settings/Launcher, written only when the user rearranges. Favorites stay User.PinnedPaths |
{user}/{Package} workspace roots |
nothing; the app's front page is its entry point |
{user}/_Install/*, {user}/_InstallRequests/* |
only for what the user started (a course); nothing for apps |
{user}/Agent, /Skill, /Harness copies |
nothing. The AI pickers (AgentPickerProjection.BuildAgentQuery, BuildSkillQueries, BuildHarnessQuery) query the package partitions of the viewer's visible apps. {user}/Skill and {user}/Agent hold only what the user wrote. AiSourcesInstallHook stops appending to every user's AiSettings |
{user}/{Course}/… course copy |
stays, but only on Start. Starting a course is the learner creating their own work, so the course's working material is copied when they press Start, never on a page render or a first open. Lessons a learner only reads render from {Course}/…. Answers stay in {user}/_Answers/… and progress in {user}/_Progress/…. Refresh-what-you-did-not-edit (RefreshPlan) and per-exercise Restore keep working on this copy |
{user}/AppleMaps/MyMaps key, {user}/AppleWeather connector data |
Maps: nothing (keys are instance-only, Administration › Maps). Weather and the other personal connectors: {user}/_Connections/{provider} (the existing Connections layer), written when the user connects. Existing connector records are moved there by an idempotent migration in phase 4: vault references are carried over, never re-encrypted in the clear; a conflict keeps the newer record and reports the other; it is verified per user before the old readers are retired |
| ReinsuranceDemo submissions | stays: the user pressed Create demo data, so this is user data |
6. The fleet consolidation — 111 packages → about a dozen apps
Scanned 2026-10-10 at origin/main of Plugins (76 roots), SocialMedia (5), Reinsurance (17),
Manufacturing (1), Education (11), Crm (1) and FundReporting (1). Every root lands in exactly one
place:
| Place | Declared by | Surfaces as |
|---|---|---|
| App | app: true, and on the roster |
a launcher tile, subject to §3 |
| Settings section | hostedIn: {user}/Settings · Admin · {app}#settings |
a tab of the user settings app, of Administration, or of the app's own Settings |
| Example | hostedIn: Developer/Home, slot examples · showcases · tools |
a section of the Examples app (already built: Developer/DeveloperApp) |
| Section of an app | hostedIn: {app}, slot |
inside that app (a suite module, a course, a game, a channel) |
| Capability | none of the above | no surface; listed in Manage apps only |
Apps — the whole launcher
| App | Package | Absorbs | Seen by |
|---|---|---|---|
| Threads | AI |
Chat; providers and harnesses in its Settings |
everyone |
| Learning | Edu |
11 Education courses, RiskTransfer, SwissSolvencyTest, ReinsurancePractice, LearningRoadmap, Training tours |
everyone (courses by tier) |
| Feedback | Feedback |
everyone | |
| Examples | Developer (renamed Examples) |
9 galleries, 4 showcases, 3 builder tools (already hosted), plus the Maps galleries | everyone |
| Settings | core person app | §7 | everyone |
| Approvals | Approvals |
pro | |
| Expenses | Expenses |
pro | |
| Signature | Signature |
MySignatures; providers in its Settings; signing authority stays in user Settings |
pro |
| CRM | Crm |
pro | |
| Social Media | SocialMedia |
LinkedIn, X, YouTube, Marketing as channels; LinkedIn profile pins become sections, not _App tiles |
pro |
| Personal (new host) | Personal |
Google, ICloud, HomeAssistant, AppleMusic, AppleWeather as sections, each with its connection in the app's Settings |
free |
| Games (new host) | Games |
Chess, RolePlay, QualityTime / QualityTimeDe (the language twin is picked by the viewer's language) |
personal (maintainer, 2026-10-10: "games are included in personal tier") |
| Administration | core Admin app | Manage apps (+ Add), Operations (Hosting, Governance, BuildServer, AzureCostManagement, Observability), instance settings |
global admins |
| Reinsurance | Reinsurance |
Claims, Underwriting, Pricing, LossModelling, Planning, Ifrs17, SST, ILS, EventMonitoring, Portfolio, PortfolioOptimization, TaskManagement, ReinsuranceDemo, Cornerstone |
enterprise + audience |
| Manufacturing | Manufacturing |
enterprise + audience | |
| Fund reporting | the FundReporting root | enterprise + audience |
A pro user sees 12 tiles, an admin 13, and an enterprise user adds one per vertical. Today the home shows ~50 roots plus a tile per install.
Settings sections — no tile
| Package | User settings | Administration | App's own Settings |
|---|---|---|---|
GoogleMaps, AppleMaps, OpenStreetMap, Maps |
— (no user map keys) | Maps: the instance's keys and default renderer, the only place they are set | — · users learn maps in Examples › Maps |
MyAi |
AI: my agent/skill/model sources | — | — |
Google, ICloud, Mail (personal half), LinkedIn, X, YouTube, HomeAssistant, AppleMusic + new generic IMAP/SMTP mail and CalDAV / ICS calendar connectors |
Connections › Accounts: every account the assistant may use, any provider, several per kind | the OAuth app registrations | the apps read the viewer's connections; Personal and Social Media show Connect in Settings when one is missing |
Providers, Anthropic, OpenAI, AzureFoundry, AppleIntelligence, WebSearch |
— | AI providers: shared keys, tiers, search key | Threads › Models: my own keys; web search default |
ClaudeCode, Codex, Copilot, Cursor, Grok, OpenCode, Antigravity, Acp |
— | CLI availability | Threads › Harnesses: enable + /login |
Teams, WhatsApp, iMessage, Voice |
Channels: link my account / Mac bridge / satellite | Channels: bot / Business API registrations | — |
Mail |
Connections › Accounts (Microsoft 365 consent) | Mail: system mail + intake | — |
Mcp |
Connections › MCP clients | — | — |
Notifications |
Notifications (exists) | — | — |
Stripe |
— | Payments | — |
Store (plans, subscription) |
Plan & billing | Plans & seats, coupons | — |
Hosting.Instance |
— | Registration (exists) | — |
Capabilities — no surface
Essentials, Publish, Import, Indexing, Collaboration, Export (the menu action; its
gallery is in Examples), OgCard (embed; gallery in Examples), RemoteControl, Video,
AzureBlob, Store (the engine). They are listed in Manage apps so an admin can see they are
present.
The roster gate — no gazillion apps
validate-repos.py gets check_app_roster, which holds a literal set of the package ids
allowed to declare app: true. It follows the pattern of check_one_agent_master, and each
satellite carries its own set. A new launcher tile is therefore a deliberate, reviewed one-line
diff. Everything else must declare hostedIn or nothing.
- A Plugins package that sets
app: truewithout being on the roster is red. - A
hostedInnaming a host that does not exist is red.
7. App design — one look for every app
Every surface is built from the platform's controls (Stack, LayoutGrid, Title, Markdown,
Button, DataGrid), never from markup of its own.
The launcher. A large filter field at the top of the home, 64 px tall, full width, with
/ to focus it. It matches app names and what an app does ("sign a contract" finds Signature), and
it filters the one grid in place. Below it is one grid of rounded-square icons, grouped by category. It has the existing ⋯ menu,
Recent / Favorites / All, and Icons / Cards / Rows. With a dozen apps the grouping shrinks to four
headings:
| Daily work | AI & learning | Business | Personal |
|---|---|---|---|
| Approvals · Expenses · Signature · Feedback | Threads · Learning · Examples | CRM · Social Media · Reinsurance · … | Personal · Games · Settings |
Administration sits last, admins only. Icons follow one system: a 48-unit rounded square
(rx=10), one brand colour or a two-stop gradient, and a white glyph, as Developer, Store and
Crm already do. Packages whose icon is an emoji or a currentColor outline (Agent, Skill,
Training, Voice, …) get redrawn in that system.
The app front page (AppFrontPage, one shared builder in Store/Core, used by every app's
entry point):
- Hero: the icon at 64 px, the name, a one-line promise, and one primary button. That button is Open or, for a suite, the first section. A secondary button reads Settings when the app has any.
- What you can do: three cards, each a sentence and a deep link.
- Ask the agent: two or three example prompts, which start a thread through
hub.StartThread. - Sections: for suite apps (Personal, Games, Social Media, Reinsurance, Examples, Learning), a
card grid of the hosted packages, from the existing
ExtensionShelfquery. - Not covered: one line, Included in Pro → see plans, never a buy button.
Setup: connect what you already use. The first sign-in walks through Profile → Connect → Your apps. Connect offers every kind of account at once, by any provider:
- Mail & calendar: Microsoft 365, Google, iCloud, other email (IMAP/SMTP), other calendar (CalDAV or a subscribed link).
- Professional & social: LinkedIn, X, YouTube.
- Messaging: Teams, WhatsApp, iMessage.
- Home & entertainment: Home Assistant, Apple Music, Weather.
Each one is optional, and Skip for now is always there. Every connection is the user's own data
({user}/_Connections/{provider}, secrets encrypted through ConnectionSecrets), so this is exactly the kind of write
§5 allows. The same list is Settings › Connections › Accounts afterwards, and an app with a
missing connection links there.
The Settings app. A left rail grouped into Account (Profile, Account, Preferences, Sharing,
Plan & billing), Personal (AI, Notifications, Signing authority) and Connections (Accounts,
Messaging, MCP clients). There is no Maps tab: map keys are instance configuration. Each tab is a UiContribution context: PersonApp from the
owning package, shown only when the access check passes.
Administration. Manage apps first: a DataGrid of every app on the instance (icon, name,
tier, Who sees it, status), with Add (the registry catalog) and Remove. Each app's row
opens its management page, which holds its instance settings inline and the audience picker. Then
Users & access, Plans & seats, AI providers, Maps, Channels, Mail, Payments, Operations.
App settings. A Settings tab on the host app, fed by a new context: AppSettings
contribution. Threads, Signature, Personal and Social Media use it.
8. Migration — phases, repos, gates
Each phase is its own pull request or set of pull requests. Phases 1–5 are additive or retire code paths. Nothing in them deletes user data. Phase 6 deletes, and runs only on the maintainer's go, per instance, after a dry run.
| # | Repo | Change | Proof |
|---|---|---|---|
| 0 | Plugins | This page | — |
| 1a | core | AppDirectory + AppDirectoryCache (live, 5 min warm). The Apps band reads it behind Home:AppSource (Records | Directory, default Records). {user}/_Settings/Launcher arrangement node, seeded once from the viewer's _App records on first directory render |
AppDirectoryTest: roster × plan × audience × admin matrix; revoke re-emits; warm reuse within 5 min and disposal after; the arrangement carries over |
| 1b | core + Plugins | (parallel change) plan → Read seam with audience | its own tests |
| 2 | Plugins + satellites | Apply §6 to every index.json. app: true only on the roster, hostedIn everywhere else. Developer → Examples. New host packages Personal and Games (this directive is the owner's go). check_app_roster gate |
validate-repos.py; each host renders its sections (render_area) |
| 3 | core + Plugins | Setup › Connect + Connections › Accounts (new generic IMAP/SMTP and CalDAV/ICS connectors beside the existing Microsoft 365, Google, iCloud, LinkedIn, X, YouTube, Home Assistant, Apple Music ones); the launcher's large filter. Settings homes: the Settings app rail + PersonApp tabs (Maps, AI, Channels, Connections, Plan & billing). Administration › Manage apps (+ Add / Remove / audience) and the instance tabs. context: AppSettings + Threads › Models / Harnesses. AppFrontPage |
per-tab Tests areas; the Manage apps audience write; render as free, pro, admin |
| 4 | Plugins + Education + Reinsurance | Stop writing implicitly: StandardPacks per-user sweep + render-path onboarding, Edu EnsureCopy on lesson render, RegisterAppRecord, AppTileRefresh, HostedTilePurgeWatcher, InstanceAppTileLogonAction, AiSourcesInstallHook. Narrow install to Start: installPaths → startPaths (course exercise material only; every Skill/Agent/Guide/Examples/My* entry dropped across the fleet); only the course's Start button and StartExercise copy. Readers switch: AI pickers to visible-app partitions; unstarted lessons render the master; cover CTA → Open. Flip Home:AppSource to Directory |
a fresh user signs in, opens every app, reads a lesson: get @{user}/* lists only _Settings, _Answers, _Progress, _UserActivity and what they created; pressing Start on a course adds that course's material and its _Install record, nothing else |
| 5 | core + Plugins + satellites | Delete what Start does not need: tile and home-root code in Store/Installer (BuildHomeRoot, RegisterAppRecord, AppTileRefresh, the fixed Skill/Agent/Harness segments, StandardPacks per-user), the logon seeders, SocialMedia tile pins, installPaths (now a validate error naming startPaths), core InstalledApp from the launcher. Keep Localize for startPaths, RefreshPlan, ItemRestore, InstallRequest (the Start front door) |
all repos green; the tile/sweep/home-root test methods are retired with their code |
| 6 | ops | Clean the homes (destructive): a Store/Maintenance action, dry run first, report per instance. Scope: legacy APP copies only: workspace roots, copied Agent/Skill/Harness, _App records, and the _Install/_InstallRequests records of packages that are not started-course material. Pristine app copies (fingerprint = record) are deleted; edited ones are kept as the user's content with the app chrome stripped; unrecorded ones are kept and listed. Started courses and their _Install provenance are never touched, pristine or not, because RefreshPlan and per-exercise Restore depend on that record |
report reviewed by the maintainer, then run per instance; afterwards get @{user}/* matches the phase-4 proof |
Order of landing: 1a → 1b → 2 → 3 → 4 → 5 → 6. Phase 2 can run in parallel with phase 3.
Deploy before recycle. A core change (phase 1a's AppDirectory, the AppSettings context) is
process-loaded. It reaches an instance only through the CD roll of a core image that contains it:
confirm the instance's /api/version against the merge first. A module or package change reaches
an instance through the registry publish (after main's settle-locks PR) and the instance's
update. Only once the instance runs the new build does a recycle make sense. Then recycle the
per-node hubs that serve the changed types:
- the person app on a test user, and
Admin; AI/AiThreads;Developer/Home;Store/Catalog;- every new host.
Then verify on the public instance as anonymous, free, pro and admin.
9. Decisions taken here, and the ones left
| Live + warm instead of a snapshot with TTL | The ask was "5 min sliding". A live stream kept warm 5 minutes gives the same cost profile and cannot show a revoked app |
Developer keeps its id and is displayed as "Examples" |
renaming the partition would move every hosted path for no user benefit |
| Map keys are instance-only (maintainer) | this reverses Settings extensions' "every user may enter their own map key". Users learn maps from Examples › Maps. The existing per-user AppleMaps MyMaps key nodes are the user's private material (a .p8), and a global admin has no read on user data. They are never shown to an admin or reused as the instance key. They stay private, unused; their owners are told once that maps now use the instance key and how to delete their old key. The admin configures a separate instance credential |
| Personal and Games become new host apps | they bring the personal-data and game tiles from 9 down to 2 |
| Open: default audience of a newly Added app | Proposed: none, so the plan alone decides; the admin narrows when needed. Today's install-defaults pass keeps adding the platform core set |
| Open: phase 6 | runs only after the dry-run report is reviewed, instance by instance |