Prox OS Internal Docs
PlatformArchitectureApps

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

LayerOwnerCurrent contract
OS-level routeShell and OS routing docsFinds the app surface, view mode, Spaces route, and registry entry. Not redefined by this document.
App-level routeAppLives under manifest.routing.routeBase; represented as AppRuntimeRoute.appPath inside the app.
Window route stateWindow Manager / runtime hostStored as WindowInstance.route when a window has a focused app-owned path.
External app routeIframe / remote appOwned 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:

PropertyRule
Code locationApp code is in the monorepo.
RegistrationThe app is registered through a manifest.
Runtime hostThe shell opens it inside a managed window.
React runtimeThe app runs in the same React runtime.
BoundaryThe 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:

ResponsibilityDetail
Manifest readReads the manifest.
Route inputReceives URL-derived or window-derived route state.
Route resolutionResolves AppRuntimeRoute.
Runtime contextPasses runtime.route and router-agnostic runtime.navigation hints to local apps.
RenderingDelegates rendering to localAppComponents.tsx or IframeAppHost.

It does not:

Non-responsibilityReason
Import app-owned route configsApp internals remain app-owned.
Force TanStack Router, React Router, Wouter, or any other app routerApp routing stays framework-agnostic at the shell boundary.
Keep business data for appsRemote data belongs in app-local data boundaries or query caches.
Rewrite OS route namespacesRuntime route groups stay registry-driven.
Register every app page in the shell routerThe 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 classrouting.modeownerpreferredUrlMode
Reusable package app / local UIlocal-moduleapppath
Shell-coupled control surfacesystemshellmemory
Iframe appiframeapphash or query
Future remote route appremote-routeapppath
Future remote module / federationexternal-moduleapppath 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:

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

AdapterResponsibility
desktop-shell-utils.tsParses browser paths by registered routing.routeBase and extracts appPath.
WindowInstance.routeStores app-owned route state for a concrete window.
AppRuntime.tsxResolves 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:

RuleDetail
Single fieldCurrent manifests use routing.routeBase; do not add new top-level route, path, href, or routePath fields for the same value.
Shared consumersRegistry, command palette, App Store, route map, and shell links read from routing.routeBase.
Centralized mathHelpers centralize route math; apps should not duplicate base-path parsing.
Future migrationIf 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:

StageUse whenPattern
Static view / tabsNew mock app or single-screen prototypeIgnore runtime.route initially; use local state for filters and tabs.
appPath based viewA few deep-linkable viewsParse runtime.route.appPath inside the app.
App-owned routerComplex app with nested pages or loadersMount an app-local router with its own history/base adapter.
External app routeIndependent site or deployed appPrefer 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-star

These 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 complexityRouting approach
Static appsRemain flat.
Apps with a few pagesParse runtime.route.appPath.
Apps with substantial navigationMount TanStack Router, React Router, Wouter, or a state machine behind the app component.
App-owned routersKeep history scoped to the app or use a documented base adapter.
Shell boundaryReceive 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/123

routing.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 directionStatus
QiankunCandidate only.
single-spaCandidate only.
Module FederationCandidate only.
Web ComponentsCandidate only.
Remote ESMCandidate only.
Sandboxed iframeCurrent preferred external boundary.
Future Prox OS app package protocolLater 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-goalReason
Redesign OS-level route namespacesThis 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/:spaceIdCurrent route groups stay registry-driven.
Treat /spaces/:spaceId as a canonical owner-scoped Studio URLIt remains a historical fullscreen runtime route.
Migrate /app-os/* or /app-hub/* to owner-scoped pathsRuntime routes and future creator paths stay separate.
Change @owner roadmap paths such as /@esmadrider/apps/xxxOwner-scoped routes are governed by URL and namespace docs.
Force every app to use TanStack RouterApps choose the lightest route model that fits.
Register every app internal page inside the shell routerInternal pages belong to the app.
Implement qiankun, single-spa, Module Federation, or a plugin marketplace runtime nowExternal module integration needs a concrete product case first.

Acceptance Checklist

CheckExpected result
Manifest route baseManifest has routing.routeBase.
App lookupShell finds the app by routeBase, not by hardcoded app internals.
Runtime contextAppRuntimeContext.route includes routeBase, appPath, fullPath, and windowId when available.
Window route stateWindow state may store WindowInstance.route for the focused app-owned path.
Static incubation appsStatic incubation apps may ignore appPath.
Deep linksDeep-link apps parse appPath or mount an app-owned router.
External appsExternal apps keep their own router behind iframe or later bridge contracts.

On this page