Cross-Framework Hosts
Source: docs/guides/creating-an-app.md
Creating an App
This guide covers the full process of adding a new federated app to Korporus.
Overview
A Korporus app is a pnpm workspace package that:
- Exposes required Web Components (
menubar,main) and optionalsettings - Bundles them as a Module Federation remote
- Registers with the shell via a manifest JSON
Step 1: Register Your Port
Before anything else, register your app in the central port registry at packages/platform-config/src/ports.ts:
const PORT_REGISTRY: Record = {
shell: { dev: 3000, preview: 4000 },
"hello-app": { dev: 3001, preview: 4001 },
"docs-app": { dev: 3002, preview: 4002 },
"settings-app": { dev: 3003, preview: 4003 },
"my-app": { dev: 3004, preview: 4004 }, // ← add your app
}; This is the single source of truth for all port assignments. The shell's dev manifest rewrite plugin will automatically discover your app — no manual wiring needed.
Step 2: Create the Package
mkdir -p packages/my-app/src/componentspackage.json
{
"name": "@korporus/app-my-app",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@korporus/web-component-wrapper": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zustand": "^5.0.0"
},
"devDependencies": {
"@korporus/platform-config": "workspace:*",
"@module-federation/enhanced": "^0.7.0",
"@module-federation/vite": "^1.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.0.0",
"typescript": "*",
"vite": "^6.0.0"
}
}Note: no --port flags in dev scripts — port assignment comes from @korporus/platform-config.
Step 3: Configure Vite and Module Federation
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { federation } from "@module-federation/vite";
import { getPortEntry, getDevOrigin } from "@korporus/platform-config";
const APP_ID = "my-app";
const ports = getPortEntry(APP_ID);
export default defineConfig({
plugins: [
react(),
federation({
name: "my_app", // underscores, not hyphens
filename: "remoteEntry.js",
manifest: true,
exposes: {
"./bootstrap": "./src/bootstrap.ts",
},
shared: {
react: { singleton: true, requiredVersion: "^19.0.0" },
"react-dom": { singleton: true, requiredVersion: "^19.0.0" },
zustand: { singleton: true },
},
dts: false,
}),
],
server: {
port: ports.dev,
strictPort: true, // Fail if port taken, don't silently increment
cors: true,
origin: getDevOrigin(APP_ID),
},
preview: {
port: ports.preview,
cors: true,
},
});Step 4: Write Your Components
Create your slot components. They're just React components — no special API:
// src/components/MyMain.tsx
export function MyMain() {
return Hello from my app!;
}Use Zustand for state shared across slots — see State Management.
To consume system-wide appearance settings, use @korporus/system-settings:
import { readAppearance, subscribeAppearance } from "@korporus/system-settings";
const initial = readAppearance();
const unsubscribe = subscribeAppearance((next) => {
// react to global mode/theme/motion changes
});To build standardized settings/help layouts, use @korporus/app-shell-ui:
import { SettingsScaffold, HelpScaffold } from "@korporus/app-shell-ui";Step 5: Register as Web Components
// src/bootstrap.ts
import { registerCustomElement } from "@korporus/web-component-wrapper";
import { MyMenubar } from "./components/MyMenubar";
import { MyMain } from "./components/MyMain";
import { MySettings } from "./components/MySettings";
registerCustomElement("my-app-menubar", MyMenubar);
registerCustomElement("my-app-main", MyMain);
registerCustomElement("my-app-settings", MySettings); // optionalInside custom-element-rendered components, you can access the host with:
import { useHostElement } from "@korporus/web-component-wrapper";Step 6: Register with the Shell
- Create
apps/shell/public/manifests/my-app.json:
{
"id": "my-app",
"name": "My Application",
"icon": "/manifests/my-app-icon.svg",
"version": "1.0.0",
"remoteEntry": "/apps/my-app/remoteEntry.js",
"slots": {
"menubar": "my-app-menubar",
"main": "my-app-main",
"settings": "my-app-settings"
}
}- Add the manifest URL to
apps/shell/src/config/apps.ts
That's it for the shell — the devManifestRewritePlugin automatically discovers all apps registered in @korporus/platform-config, so no manual port wiring is needed.
Settings Save/Cancel Contract
Settings views should buffer edits locally and integrate with shell Save/Cancel actions:
import { useSettingsSessionBridge } from "@korporus/app-shell-ui";
useSettingsSessionBridge({
state: { dirty, valid, saving },
onSave: async () => {
// commit draft -> persisted app state
},
onCancel: () => {
// revert draft to last persisted values
},
});The shell dispatches save/cancel events to your mounted settings custom element and listens for session state updates.
App Help Integration
App-specific help content should live in docs under:
docs/apps//index.md The shell Help menu routes to the centralized docs app with:
/app/docs-app?contextAppId=&entry=app-help If no app-specific help page exists, docs falls back to the general help index.
Step 7: Install and Run
pnpm install
cd packages/my-app && pnpm dev # Terminal 1
cd apps/shell && pnpm dev # Terminal 2Your app should appear on the shell's home screen.