App Routing Architecture
This document defines the **app-level routing contract** for Prox OS apps hosted by the shell.
Scope
This document defines the app-level routing contract for Prox OS apps hosted by the shell.
It does not redesign OS-level routing. Runtime route groups such as /app, /app-edu, /app-health, /app-user,
/app-connectors, /app-game, /app-admin, /app-dev, /app-iframe, /app-os, and /app-hub stay registry-driven. Future owner-scoped paths such as
/@esmadrider/apps/waitlist-pro and Store listing paths stay governed by
url-and-surface-model.md and
app-namespace-and-domain-strategy.md.
The short rule:
Shell owns app discovery.
App owns internal route.
Shell Wrapper passes route context.
Manifest declares the routing boundary.Route Layers
| Layer | Owner | Current contract |
|---|---|---|
| OS-level route | Shell and OS routing docs | Finds the app surface, view mode, Spaces route, and registry entry. Not redefined by this document. |
| App-level route | App | Lives under manifest.routing.routeBase; represented as AppRuntimeRoute.appPath inside the app. |
| Window route state | Window Manager / runtime host | Stored as WindowInstance.route when a window has a focused app-owned path. |
| External app route | Iframe / remote app | Owned by the external runtime; the shell passes initial context and future bridge messages. |
The shell may parse a URL prefix to find the registered app, but it must not register every app's
internal pages. For example, /app/dreams/entry/rem-city can open the Dreams app and pass
appPath: "/entry/rem-city"; the shell does not need to know what entry/rem-city means.
Local Module App + Shell Wrapper
Local Module Apps are the current default for fast OS app incubation:
| Property | Rule |
|---|---|
| Code location | App code is in the monorepo. |
| Registration | The app is registered through a manifest. |
| Runtime host | The shell opens it inside a managed window. |
| React runtime | The app runs in the same React runtime. |
| Boundary | The app still uses public app contracts instead of shell-private imports. |
apps/os-shell/src/apps/AppRuntime.tsx is the current Shell Wrapper. It stays thin:
| Responsibility | Detail |
|---|---|
| Manifest read | Reads the manifest. |
| Route input | Receives URL-derived or window-derived route state. |
| Route resolution | Resolves AppRuntimeRoute. |
| Runtime context | Passes runtime.route and router-agnostic runtime.navigation hints to local apps. |
| Rendering | Delegates rendering to localAppComponents.tsx or IframeAppHost. |
It does not:
| Non-responsibility | Reason |
|---|---|
| Import app-owned route configs | App internals remain app-owned. |
| Force TanStack Router, React Router, Wouter, or any other app router | App routing stays framework-agnostic at the shell boundary. |
| Keep business data for apps | Remote data belongs in app-local data boundaries or query caches. |
| Rewrite OS route namespaces | Runtime route groups stay registry-driven. |
| Register every app page in the shell router | The shell discovers apps; apps own internal pages. |
Manifest Routing Metadata
Current manifests use routing as the single launch and app-route boundary field:
export type AppRoutingConfig = {
mode: 'local-module' | 'iframe' | 'external-module' | 'web-component' | 'remote-route' | 'system'
owner: 'shell' | 'app'
routeBase: `/${string}`
defaultPath?: `/${string}`
supportsDeepLink?: boolean
preferredUrlMode?: 'path' | 'hash' | 'query' | 'memory'
externalEntry?: string
healthPath?: string
allowedOrigins?: string[]
}routeBase replaces the older top-level routePath manifest field. It is the canonical runtime
launch base. It may differ from the internal appId, such as airtable-viewer at /app/airtable
or os-about at /app-os/about.
Recommended defaults:
| App class | routing.mode | owner | preferredUrlMode |
|---|---|---|---|
| Reusable package app / local UI | local-module | app | path |
| Shell-coupled control surface | system | shell | memory |
| Iframe app | iframe | app | hash or query |
| Future remote route app | remote-route | app | path |
| Future remote module / federation | external-module | app | path or memory |
Runtime Route Context
Apps receive a router-agnostic route context:
export type AppRuntimeRoute = {
appId: string
routeBase: `/${string}`
appPath: `/${string}`
fullPath: string
windowId?: string
params?: Record<string, string>
search?: string
hash?: string
}Current helpers in @prox-os/app-contract:
| Helper | Purpose |
|---|---|
getAppRoutingConfig(manifest) | Read manifest routing metadata. |
getAppRouteBase(manifest) | Resolve the app launch base. |
getAppDefaultPath(manifest) | Resolve the app default path. |
getAppRoutingMode(manifest) | Resolve the app routing mode. |
normalizeAppPath(path) | Normalize app-owned paths. |
buildAppFullPath(input) | Build full shell-visible app paths. |
resolveAppRuntimeRoute(manifest, input) | Resolve the router-agnostic runtime route context. |
Current shell adapter points:
| Adapter | Responsibility |
|---|---|
desktop-shell-utils.ts | Parses browser paths by registered routing.routeBase and extracts appPath. |
WindowInstance.route | Stores app-owned route state for a concrete window. |
AppRuntime.tsx | Resolves runtime.route and runtime.navigation. |
Current Route Schema
The current project does not have a stable public manifest API yet, so this contract chooses a clear current schema instead of parallel route fields.
Migration rules:
| Rule | Detail |
|---|---|
| Single field | Current manifests use routing.routeBase; do not add new top-level route, path, href, or routePath fields for the same value. |
| Shared consumers | Registry, command palette, App Store, route map, and shell links read from routing.routeBase. |
| Centralized math | Helpers centralize route math; apps should not duplicate base-path parsing. |
| Future migration | If a future migration is needed, migrate current manifests in the same change instead of keeping long-term dual schemas. |
App Incubation Guidance
Fast UI incubation does not require a full internal router on day one.
Use the lightest route model that fits:
| Stage | Use when | Pattern |
|---|---|---|
| Static view / tabs | New mock app or single-screen prototype | Ignore runtime.route initially; use local state for filters and tabs. |
appPath based view | A few deep-linkable views | Parse runtime.route.appPath inside the app. |
| App-owned router | Complex app with nested pages or loaders | Mount an app-local router with its own history/base adapter. |
| External app route | Independent site or deployed app | Prefer iframe first; let the external app own its router. |
Examples:
/app/dreams
/app/dreams/entry/:id
/app/dreams/symbols
/app-game
/app-game/snake
/app-os/badges/github-starThese are app-level examples under current runtime route groups. They do not replace future
owner-scoped paths such as /@esmadrider/apps/xxx.
Phase 2: Local App Internal Routing
Phase 2 keeps local apps in the bundle but lets them opt into deep links:
| App complexity | Routing approach |
|---|---|
| Static apps | Remain flat. |
| Apps with a few pages | Parse runtime.route.appPath. |
| Apps with substantial navigation | Mount TanStack Router, React Router, Wouter, or a state machine behind the app component. |
| App-owned routers | Keep history scoped to the app or use a documented base adapter. |
| Shell boundary | Receive only navigation intent, not a concrete router instance. |
Phase 3: Iframe App Routing Protocol
Iframe apps are the preferred early external app boundary because they isolate runtime, dependencies, CSS, and app-owned routing while letting the OS ecosystem grow quickly.
Future shell-to-iframe message:
INIT { appId, routeBase, initialPath, theme, locale, windowId, capabilities }Future iframe-to-shell messages:
READY {}
NAVIGATE { path, replace? }
TITLE_CHANGE { title }
REQUEST_OPEN_APP { appId, path? }
REQUEST_EXTERNAL_OPEN { url }
ERROR { message }Iframe URL modes:
path: https://example.com/prox/entry/123
hash: https://example.com/#/entry/123
query: https://example.com?proxPath=/entry/123routing.preferredUrlMode declares the preferred bridge style. The current shell documents the
direction but does not yet implement the full postMessage route protocol.
Phase 4: External Module And Micro Frontend Routing
External Module Apps are a later integration tier, not the current default.
| Candidate direction | Status |
|---|---|
| Qiankun | Candidate only. |
| single-spa | Candidate only. |
| Module Federation | Candidate only. |
| Web Components | Candidate only. |
| Remote ESM | Candidate only. |
| Sandboxed iframe | Current preferred external boundary. |
| Future Prox OS app package protocol | Later platform direction. |
The invariant remains:
Active route and routeBase let the shell discover and mount the app.
Internal route belongs to the external app.
Capabilities flow through runtime context or a bridge.Do not add those runtimes until the product has a concrete app that needs them.
Non-goals
| Non-goal | Reason |
|---|---|
| Redesign OS-level route namespaces | This document only defines app-level routing contracts. |
Replace /app, /app-edu, /app-health, /app-user, /app-connectors, /app-admin, /app-dev, /app-iframe, /app-os, /app-hub, /website, or /spaces/:spaceId | Current route groups stay registry-driven. |
Treat /spaces/:spaceId as a canonical owner-scoped Studio URL | It remains a historical fullscreen runtime route. |
Migrate /app-os/* or /app-hub/* to owner-scoped paths | Runtime routes and future creator paths stay separate. |
Change @owner roadmap paths such as /@esmadrider/apps/xxx | Owner-scoped routes are governed by URL and namespace docs. |
| Force every app to use TanStack Router | Apps choose the lightest route model that fits. |
| Register every app internal page inside the shell router | Internal pages belong to the app. |
| Implement qiankun, single-spa, Module Federation, or a plugin marketplace runtime now | External module integration needs a concrete product case first. |
Acceptance Checklist
| Check | Expected result |
|---|---|
| Manifest route base | Manifest has routing.routeBase. |
| App lookup | Shell finds the app by routeBase, not by hardcoded app internals. |
| Runtime context | AppRuntimeContext.route includes routeBase, appPath, fullPath, and windowId when available. |
| Window route state | Window state may store WindowInstance.route for the focused app-owned path. |
| Static incubation apps | Static incubation apps may ignore appPath. |
| Deep links | Deep-link apps parse appPath or mount an app-owned router. |
| External apps | External apps keep their own router behind iframe or later bridge contracts. |
App Registry
The **composed** shell registry is the runtime source of truth for route group, window defaults, ownership, permissions, runtime kind, and desktop placement. Pu
App Route Taxonomy
Adding /app-track, /app-connectors, /app-edu, /app-health, /app-sports, /app-game, and /app-hub is a narrow route-group extension. The shell still parses