Apps live on the instance, not in your home

Maintainer, 2026-10-10:

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:

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)

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):

  1. 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 as Space), filtered to content.app == true and bounded by an explicit limit. It goes through IMeshNodeStreamCache. It is the same for every viewer.

  2. 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.

  3. Plus the platform entries every viewer has.

    • Settings (/{user}/Settings) for everyone.
    • Administration (Admin) when IsGlobalAdmin.
    • Inbox.

    These are not records, so SeedDefaultAppsLogonAction, SeedInboxAppLogonAction, InstanceAppTileLogonAction and the icon-adoption logon actions are retired.

  4. 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.

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.

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):

  1. 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.
  2. What you can do: three cards, each a sentence and a deep link.
  3. Ask the agent: two or three example prompts, which start a thread through hub.StartThread.
  4. Sections: for suite apps (Personal, Games, Social Media, Reinsurance, Examples, Learning), a card grid of the hosted packages, from the existing ExtensionShelf query.
  5. 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:

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:

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