Skip to main content

ADR-0008: Frontend Plugin Architecture for Multi-Module Applications

  • Status: Proposed
  • Date: 2026-02-02
  • Deciders: @anshpansuriya14
  • Supersedes: N/A
  • Superseded by: N/A

Context

Framework M supports building modular business applications where different business domains (WMS, Personnel, Finance, Billing) are developed as separate Python apps. Each app has its own backend logic (DocTypes, Controllers, APIs). As applications grow, each module needs its own specialized frontend UI, leading to the question: How do multiple frontend modules integrate into a cohesive user experience?

The Three Levels of Frontend Integration:

  1. Level 1 - Ready Desk: Bundled static UI served from PyPI package (m start)
  2. Level 2 - With Frontend: Scaffolded React app with customization (m start --with-frontend)
  3. Level 3 - External UI: Any framework consuming REST/WebSocket APIs

Problem Statement:

A developer builds multiple Framework M apps:

  • WMS App - Warehouse management with inventory screens
  • Personnel App - HR management with employee screens
  • Finance App - Accounting with ledger screens
  • Billing App - Invoicing with payment screens

Each app needs:

  • Backend modularity - Already solved via ports/adapters, entry points, DI container
  • Frontend modularity - Currently undefined

Forces at Play:

  • Backend Parity: Frontend architecture should mirror backend principles (ports/adapters, DI, composition)
  • Developer Experience: Simple commands like m new:app wms --with-frontend should scaffold everything
  • Build Flexibility: Support both monolithic builds (single artifact) and distributed builds (independent plugins)
  • Runtime Flexibility: Support both bundled plugins and remotely loaded plugins
  • No Lock-in: Developers should be able to eject to custom build systems
  • DevOps Simplicity: Single deployment artifact preferred, but shouldn't preclude microservices

Current State:

  • Monolithic frontend/ directory with all Desk UI code
  • No mechanism to add app-specific routes, menus, or components
  • Customizers must fork and modify the entire frontend
  • No code reuse between custom frontends

Desired Developer Experience:

# Create WMS app with frontend
m new:app wms --with-frontend

# Later, add Personnel app
m new:app personnel --with-frontend

# Both plugins auto-discovered and integrated
cd frontend
pnpm dev # Shows WMS + Personnel menus/routes

# Production build
pnpm build # Single artifact with both plugins

Alternatives Evaluated:

ApproachBuild StrategyProsCons
Monolithic FrontendSingle codebaseSimple, no complexityNo modularity, hard to maintain
Git SubmodulesMerge at build timeSimple buildMerge conflicts, poor DX
Module FederationRemote runtime loadingIndependent deploymentComplex setup, version conflicts
Plugin System (Recommended)Discovery + compositionBest of both worldsRequires framework design
iFrame CompositionSeparate apps in iframesFull isolationPoor UX, no state sharing

Decision

We will implement a Frontend Plugin System that mirrors the backend's port-adapter architecture, using plugin manifests for discovery, dependency injection for service sharing, and composition over inheritance for UI integration. Plugins are discovered at build time via package.json metadata and composed into a single-page application shell.

Package Architecture:

The plugin system extends the existing @framework-m/desk package (from ADR-0007) with new complementary packages:

┌─────────────────────────────────────────────────────────────┐
│ @framework-m/desk (Existing - Core UI Library) │
│ - Refine.dev providers (data, auth, live) │
│ - Base DocType components (List, Form, Show) │
│ - Hooks (useDocType, useMetadata) │
│ - Utilities and types │
└────────────────────────┬────────────────────────────────────┘
│ used by
┌───────────────┴───────────────┐
│ │
┌────────▼──────────────────┐ ┌────────▼──────────────────┐
│ @framework-m/plugin-sdk │ │ App Plugins │
│ (New - Plugin System) │ │ (@my-company/wms, etc) │
│ - PluginRegistry │ │ - plugin.config.ts │
│ - ServiceContainer │ │ - Uses desk + plugin-sdk │
│ - useService, usePlugin │ │ - Domain-specific UI │
│ - FrameworkMPlugin types │ └───────────────────────────┘
└───────────────────────────┘
│ used by
┌────────▼──────────────────┐
│ @framework-m/vite-plugin │
│ (New - Build Tooling) │
│ - Plugin auto-discovery │
│ - Virtual entry generation│
│ - Code splitting config │
└───────────────────────────┘
│ used by
┌────────▼──────────────────┐
│ frontend/ (Shell App) │
│ - Bootstraps plugins │
│ - Renders composed UI │
└───────────────────────────┘

Package Dependency Matrix:

PackageDepends OnUsed ByPurpose
@framework-m/deskRefine.dev, ReactShell, PluginsCore UI library - Existing from ADR-0007
@framework-m/plugin-sdkReactShell, Plugins, Vite PluginPlugin infrastructure - New
@framework-m/vite-pluginplugin-sdkShellBuild tooling - New
Shell Appdesk, plugin-sdk, vite-plugin-Composition root
App Pluginsdesk, plugin-sdkShellDomain UI

Key Design Decision: We keep @framework-m/desk unchanged and add plugin capabilities as separate packages. This means:

  • ✅ Single-app users can use @framework-m/desk alone (no plugin overhead)
  • ✅ Multi-app users add @framework-m/plugin-sdk when needed
  • ✅ No breaking changes to existing desk consumers
  • ✅ Clean separation of concerns

Architecture Principles:

Backend PrincipleFrontend Equivalent
Port-Adapter PatternPlugin Manifest Contract
Dependency InjectionService Registry + React Context
Composition over InheritanceComponent Composition + Hooks
Entry Points (pyproject.toml)Plugin Metadata (package.json)
MetaRegistryPluginRegistry
Adapter SwappingProvider Swapping

System Architecture:

┌──────────────────────────────────────────────────────────────┐
│ Shell Application │
│ (@framework-m/desk - Core) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Plugin Registry │ Route Manager │ Menu Builder │ │
│ │ Service Container │ Theme Engine │ Event Bus │ │
│ └────────────────────────────────────────────────────┘ │
└────────┬──────────────────┬──────────────────┬───────────────┘
│ │ │
┌────▼────┐ ┌──────▼─────┐ ┌─────▼────┐
│ WMS │ │ Personnel │ │ Finance │
│ Plugin │ │ Plugin │ │ Plugin │
└─────────┘ └────────────┘ └──────────┘
│ │ │
Routes Routes Routes
Menus Menus Menus
Components Components Components
Services Services Services
Providers Providers Providers

Plugin Manifest Contract:

// apps/wms/frontend/plugin.config.ts
import { FrameworkMPlugin } from "@framework-m/plugin-sdk";

export default {
name: "wms",
version: "1.0.0",

// Routes - injected into main router
routes: [
{
path: "/wms",
element: () => import("./pages/WarehouseList"),
meta: {
title: "Warehouses",
icon: "warehouse",
permissions: ["wms.read"],
},
},
],

// Menu items - merged into navigation
menu: [
{
label: "WMS",
icon: "warehouse",
children: [
{ label: "Warehouses", to: "/wms" },
{ label: "Inventory", to: "/wms/inventory" },
],
},
],

// Providers - wrapped around app
providers: [{ component: () => import("./providers/WHSProvider") }],

// Services - injectable into components
services: {
warehouseService: () => import("./services/WarehouseService"),
},

// DocType extensions
doctypes: {
Item: {
listColumns: ["sku", "warehouse_location"],
formSections: [
{ label: "Warehouse", fields: ["warehouse", "bin_location"] },
],
},
},

// Dashboard widgets
widgets: [
{
id: "wms-stock-alert",
component: () => import("./widgets/StockAlerts"),
defaultPosition: { x: 0, y: 0, w: 6, h: 4 },
},
],
} satisfies FrameworkMPlugin;

Plugin Discovery (Entry Points):

// apps/wms/frontend/package.json
{
"name": "@my-company/wms", // Workspace-local, NOT published to npm
"version": "1.0.0",
"private": true, // Never published
"framework-m": {
"plugin": "./dist/plugin.config.js",
"type": "frontend-module"
}
}

Important: Plugin packages like @my-company/wms are workspace-local packages only. They:

  • ❌ Are NOT published to npm
  • ✅ Live in your monorepo workspace
  • ✅ Get bundled into the shell app's static assets during build
  • ✅ Are referenced via "workspace:*" protocol in pnpm/yarn

Build Strategies:

workspace/
├── frontend/ # Shell application
│ ├── package.json
│ ├── vite.config.ts # Auto-discovers plugins
│ └── src/
│ ├── App.tsx # Bootstraps PluginRegistry
│ └── core/
│ └── PluginRegistry.ts
├── apps/
│ ├── wms/frontend/
│ │ ├── package.json # {"framework-m": {...}}
│ │ ├── plugin.config.ts
│ │ └── src/
│ ├── personnel/frontend/
│ │ ├── package.json
│ │ ├── plugin.config.ts
│ │ └── src/
│ └── finance/frontend/
│ ├── package.json
│ ├── plugin.config.ts
│ └── src/
└── pnpm-workspace.yaml

Vite Configuration:

// frontend/vite.config.ts
import { defineConfig } from "vite";
import { frameworkMPlugin } from "@framework-m/vite-plugin";

export default defineConfig({
plugins: [
frameworkMPlugin({
// Auto-discovers all workspace packages with framework-m metadata
discoverPlugins: true,

// OR explicit list
plugins: [
"@my-company/wms",
"@my-company/personnel",
"@my-company/finance",
],
}),
],
});

Build Process:

cd frontend
pnpm install # Installs shell + symlinks workspace plugins (no registry fetch)
pnpm build # Single static bundle with code-split chunks per plugin

# Output: frontend/dist/ (static assets - deploy to CDN or serve from Python)
# index.html
# assets/
# main-[hash].js (shell app code)
# wms-[hash].js (WMS plugin - lazy loaded)
# personnel-[hash].js (Personnel plugin - lazy loaded)
# finance-[hash].js (Finance plugin - lazy loaded)
# # All plugins bundled into static files - no runtime fetching needed

Key Point: The build output is pure static assets. No plugin packages need to be published. The shell app imports plugin code at build time via workspace references, and Vite bundles everything into static JS/CSS files.

Strategy 2: Module Federation (Independent Deployment)

For organizations requiring independent plugin deployments:

// Vite/Webpack Module Federation config
federation({
name: "shell",
remotes: {
wms: "wms@https://cdn.example.com/wms/remoteEntry.js",
personnel: "personnel@https://cdn.example.com/personnel/remoteEntry.js",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
"@refinedev/core": { singleton: true },
"@framework-m/desk": { singleton: true },
},
});

Runtime Plugin Loading:

// Shell loads plugins dynamically
const plugins = await loadPlugins([
{ name: "wms", url: "/plugins/wms/plugin.js" },
{ name: "personnel", url: "/plugins/personnel/plugin.js" },
]);

Service Dependency Injection:

// @framework-m/plugin-sdk/src/core/ServiceContainer.ts
class ServiceContainer {
private services = new Map<string, ServiceFactory>();

register(name: string, factory: ServiceFactory) {
this.services.set(name, factory);
}

async get<T>(name: string): Promise<T> {
const factory = this.services.get(name);
if (!factory) throw new Error(`Service ${name} not found`);
return await factory();
}
}

// React Hook
export function useService<T>(name: string): T {
const container = useContext(ServiceContext);
const [service, setService] = useState<T | null>(null);

useEffect(() => {
container.get<T>(name).then(setService);
}, [name]);

return service;
}

// Usage in component
function WarehouseList() {
const warehouseService = useService("warehouseService");
const inventoryService = useService("inventoryService");

// Component logic
}

Plugin Registry Implementation:

// @framework-m/plugin-sdk/src/core/PluginRegistry.ts
import { RouteObject } from "react-router-dom";

interface Plugin {
name: string;
version: string;
routes: RouteObject[];
menu: MenuItem[];
services: Record<string, ServiceFactory>;
providers: Provider[];
doctypes: DocTypeExtension[];
widgets: Widget[];
}

class PluginRegistry {
private plugins = new Map<string, Plugin>();
private serviceContainer = new ServiceContainer();
private routeManager = new RouteManager();
private menuBuilder = new MenuBuilder();

async register(plugin: Plugin) {
// Validate plugin
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin ${plugin.name} already registered`);
}

// Register services
Object.entries(plugin.services).forEach(([name, factory]) => {
this.serviceContainer.register(name, factory);
});

// Register routes
plugin.routes.forEach(route => {
this.routeManager.addRoute(route);
});

// Merge menus
this.menuBuilder.addItems(plugin.menu);

// Store plugin
this.plugins.set(plugin.name, plugin);
}

getRoutes(): RouteObject[] {
return this.routeManager.getRoutes();
}

getMenu(): MenuItem[] {
return this.menuBuilder.build();
}

getProviders(): Provider[] {
return Array.from(this.plugins.values()).flatMap(p => p.providers);
}
}

Shell Bootstrap:

// frontend/src/App.tsx
import { PluginRegistry } from '@framework-m/plugin-sdk';
import { discoverPlugins } from './core/plugin-loader';

async function bootstrap() {
const registry = new PluginRegistry();

// Discover all plugins
const plugins = await discoverPlugins();

// Register each plugin
for (const plugin of plugins) {
await registry.register(plugin);
}

return registry;
}

function App() {
const [registry, setRegistry] = useState<PluginRegistry | null>(null);

useEffect(() => {
bootstrap().then(setRegistry);
}, []);

if (!registry) return <LoadingScreen />;

return (
<ServiceProvider container={registry.serviceContainer}>
<BrowserRouter>
<Layout menu={registry.getMenu()}>
<Routes>
{registry.getRoutes().map(route => (
<Route key={route.path} {...route} />
))}
</Routes>
</Layout>
</BrowserRouter>
</ServiceProvider>
);
}

Consequences

Positive

  • Backend Parity: Frontend mirrors backend architecture (ports/adapters, DI, composition)
  • Excellent DX: m new:app wms --with-frontend scaffolds complete plugin structure
  • Flexible Build: Supports monorepo single build OR distributed Module Federation
  • Service Sharing: Plugins can provide/consume services via DI container
  • Route Composition: Routes automatically merged without conflicts
  • Menu Integration: Navigation automatically built from plugin manifests
  • Type Safety: TypeScript plugin contracts enforce structure
  • Code Reuse: @framework-m/desk components shared across plugins
  • Independent Development: Each plugin can be developed/tested in isolation
  • Progressive Enhancement: Start simple (monorepo), scale to microservices
  • No Breaking Changes: Existing @framework-m/desk users unaffected
  • Opt-in Complexity: Plugin system only needed for multi-app scenarios

Negative

  • Framework Complexity: Plugin system adds abstraction layer to maintain
    • Mitigation: Well-documented contracts, typed interfaces, extensive examples
  • Build Tool Dependency: Requires Vite plugin or Webpack config
    • Mitigation: Provide official plugins for both Vite and Webpack
  • Learning Curve: Developers must understand plugin architecture
    • Mitigation: CLI scaffolds everything, clear documentation, migration guides
  • Version Compatibility: Plugin version conflicts possible in Module Federation
    • Mitigation: Singleton shared dependencies, semantic versioning enforcement

Neutral

  • Bundle Size: Larger initial bundle with all plugins
    • Can be mitigated with lazy loading (already implemented via dynamic imports)
  • Runtime Discovery: Small overhead from plugin discovery/registration at startup
    • Negligible impact (~100ms) for typical app with 5-10 plugins

Implementation Plan

Phase 1: Core Plugin SDK (Week 1-2)

Package: @framework-m/plugin-sdk

// Exports:
export { FrameworkMPlugin } from "./types/plugin";
export { PluginRegistry } from "./core/PluginRegistry";
export { ServiceContainer } from "./core/ServiceContainer";
export { useService, usePlugin } from "./hooks";
export { discoverPlugins } from "./discovery";

Deliverables:

  • Plugin manifest type definitions
  • PluginRegistry class with route/menu/service management
  • ServiceContainer with DI functionality
  • React hooks (useService, usePlugin, usePluginMenu)
  • Plugin discovery utilities
  • Unit tests (100% coverage)

Phase 2: Vite Plugin (Week 2-3)

Package: @framework-m/vite-plugin

// vite-plugin-framework-m/src/index.ts
export function frameworkMPlugin(options: PluginOptions) {
return {
name: "framework-m-plugin",

async config() {
// Discover plugins from package.json dependencies
const plugins = await discoverWorkspacePlugins();

// Generate virtual entry point
return {
resolve: {
alias: {
"~plugins": generatePluginEntry(plugins),
},
},
};
},
};
}

Deliverables:

  • Auto-discovery of workspace plugins
  • Virtual entry point generation
  • Code-splitting configuration
  • HMR support for plugin changes
  • Documentation with examples

Phase 3: CLI Enhancements (Week 3-4)

Command: m new:app <name> --with-frontend

# libs/framework-m-core/src/framework_m_core/cli/new.py

def new_app_command(
name: str,
with_frontend: bool = False,
):
"""Create new app with optional frontend plugin."""

# Create backend structure
create_app_structure(name)

if with_frontend:
# Create frontend plugin structure
frontend_path = Path(name) / "frontend"

scaffold_plugin({
"name": name,
"path": frontend_path,
"template": "plugin-template"
})

# Update workspace package.json
add_to_workspace(f"{name}/frontend")

Plugin Template Structure:

apps/{app-name}/frontend/
├── package.json
├── plugin.config.ts
├── tsconfig.json
├── vite.config.ts # Optional, for standalone dev
├── src/
│ ├── pages/
│ ├── components/
│ ├── services/
│ └── providers/
└── README.md

Deliverables:

  • Enhanced m new:app with --with-frontend flag
  • Plugin template in framework_m/templates/plugin/
  • Automatic workspace configuration
  • Integration tests

Phase 4: Shell Integration (Week 4-5)

Update: frontend/src/App.tsx

// Before: Static routes
const routes = [
{ path: '/', element: <Dashboard /> },
{ path: '/doctypes/:doctype', element: <DocTypeList /> },
];

// After: Dynamic plugin-based routes
const routes = registry.getRoutes();

Deliverables:

  • Bootstrap PluginRegistry in App.tsx
  • Dynamic route rendering
  • Dynamic menu rendering
  • Service context provider
  • Loading states and error boundaries
  • Integration with existing Refine.dev setup

Phase 5: Documentation & Examples (Week 5-6)

New Documentation:

  • docs/developer/frontend-plugins.md - Plugin development guide
  • docs/developer/plugin-api-reference.md - Complete API docs
  • docs/tutorials/building-multi-module-app.md - Step-by-step tutorial
  • docs/examples/wms-plugin/ - Complete working example

Example App:

  • Create sample WMS plugin in examples/wms-plugin/
  • Create sample Personnel plugin in examples/personnel-plugin/
  • Document integration in main shell

Phase 6: Advanced Features (Week 6-8)

Module Federation Support (Optional):

  • Webpack config generator
  • Remote plugin loader
  • Version compatibility checker
  • CDN deployment guide

Plugin Marketplace (Future):

  • Plugin registry API
  • Discovery service
  • Plugin marketplace UI
  • Versioning and updates

Migration Path

For Existing Users

Current State: Monolithic frontend/ with custom routes

Migration Steps:

  1. Create plugin structure:
mkdir -p apps/custom/frontend
mv frontend/src/pages/custom apps/custom/frontend/src/pages
  1. Create plugin.config.ts:
export default {
name: "custom",
routes: [{ path: "/custom", element: () => import("./pages/CustomPage") }],
};
  1. Update workspace:
pnpm install  # Auto-discovers new plugin
pnpm dev # Everything still works

For New Users

Zero Migration: CLI scaffolds plugin structure from day one:

m new:project erp
cd erp
m new:app wms --with-frontend
m new:app personnel --with-frontend

cd frontend
pnpm dev # Both plugins auto-loaded

Alternatives Considered

Alternative 1: Git Submodules

Approach: Each plugin in separate repo, merged via submodules

Rejected Because:

  • Merge conflicts in shared code
  • Complex developer workflow
  • No build-time integration
  • Poor TypeScript support

Alternative 2: Monorepo with Manual Integration

Approach: All plugins in monorepo, manually import/register

Rejected Because:

  • Requires code changes for each plugin
  • No auto-discovery
  • Hard to maintain
  • Doesn't scale

Alternative 3: Pure Module Federation

Approach: All plugins loaded remotely at runtime

Rejected Because:

  • Complex initial setup
  • Requires CDN/hosting for all plugins
  • Version compatibility issues
  • Overkill for most use cases
  • We include this as Strategy 2 (optional) instead
  • ADR-0005: Refine.dev for Frontend - Plugin system builds on Refine's architecture
  • ADR-0007: Bundled Desk Distribution - Plugins extend the bundled Desk
  • ADR-0006: Mx Pattern - Frontend mirrors backend modularity principles

References

Build & Deployment Model

Critical Distinction:

TypePublished to npm?Used How?Example
Framework packages✅ Yes (GitLab npm)Installed via pnpm install@framework-m/desk, @framework-m/plugin-sdk
App pluginsNeverWorkspace symlinks, bundled at build@my-company/wms, @my-company/personnel

Build Output:

frontend/dist/           ← This is what gets deployed
├── index.html
├── assets/
│ ├── main-abc123.js (Shell + framework packages)
│ ├── wms-def456.js (WMS plugin code - bundled in)
│ ├── personnel-ghi789.js (Personnel plugin code - bundled in)
│ └── finance-jkl012.js (Finance plugin code - bundled in)
└── ... (All plugins compiled into static JS)

Deployment Flow:

# 1. Build (CI/CD or local)
cd frontend
pnpm install # Fetches @framework-m/* from registry, symlinks workspace plugins
pnpm build # Vite bundles everything into static assets

# 2. Deploy
# Option A: Serve from Python (like ADR-0007 bundled desk)
m build # Bundles dist/ into Python package wheel
pip install . # Installs with bundled frontend
m start # Serves static files from Python package

# Option B: Deploy to CDN
aws s3 sync frontend/dist s3://my-bucket/
# Python API runs separately, frontend fetches from CDN

# Option C: Container
COPY frontend/dist /app/static
# Nginx serves static files, proxies API to Python backend

No Plugin Registry Needed: Unlike Module Federation (Strategy 2), the recommended monorepo approach means all plugin code is bundled at build time. Users never need to publish or host plugin packages separately.

Package Publishing Strategy

npm Package Locations (Framework Packages Only):

PackageRegistryVisibilityVersion Sync
@framework-m/deskGitLab npmPrivate (future: public)Tied to framework-m PyPI version
@framework-m/plugin-sdkGitLab npmPrivate (future: public)Independent semver
@framework-m/vite-pluginGitLab npmPrivate (future: public)Independent semver

Versioning Strategy:

// @framework-m/desk - Follows framework-m core version
{
"name": "@framework-m/desk",
"version": "1.0.0", // Same as pip install framework-m==1.0.0
"peerDependencies": {
"@framework-m/plugin-sdk": "^1.0.0" // Optional peer dependency
}
}

// @framework-m/plugin-sdk - Independent versioning
{
"name": "@framework-m/plugin-sdk",
"version": "1.2.0", // Can evolve independently
"peerDependencies": {
"@framework-m/desk": "^1.0.0" // Works with desk 1.x
}
}

Migration for Existing Desk Users:

Existing projects using @framework-m/desk continue working without changes:

// Before (still works) - Single app, no plugins
{
"dependencies": {
"@framework-m/desk": "^1.0.0" // From GitLab npm
}
}

// After (when adding plugins) - Multi-app with plugins
{
"dependencies": {
"@framework-m/desk": "^1.0.0", // From GitLab npm
"@framework-m/plugin-sdk": "^1.0.0", // From GitLab npm
"@my-company/wms": "workspace:*" // Workspace-local (NOT npm)
},
"devDependencies": {
"@framework-m/vite-plugin": "^1.0.0" // From GitLab npm
}
}

What Gets Published:

  • @framework-m/desk → GitLab npm registry (consumed by all users)
  • @framework-m/plugin-sdk → GitLab npm registry (consumed by plugin users)
  • @framework-m/vite-plugin → GitLab npm registry (consumed by plugin users)
  • @my-company/wmsNever published (private workspace package)
  • @my-company/personnelNever published (private workspace package)

Build Output:

  • All workspace plugins bundled into frontend/dist/ static assets
  • Deployed as single artifact (no separate plugin hosting needed)
  • Can be served from Python package, CDN, or static file server

Future Considerations

  1. Plugin Marketplace: Public registry for community plugins
  2. Plugin Isolation: Sandboxing for untrusted plugins
  3. Hot Reload: Plugin updates without full page reload
  4. A/B Testing: Different plugin versions for different users
  5. Analytics: Plugin usage tracking and performance metrics
  6. Themes: Plugin-specific theming capabilities
  7. i18n: Plugin-level translations and locale support
  8. Public npm Publishing: Transition from GitLab npm to npmjs.com when ready

Approval: This ADR represents the official frontend architecture for Framework M multi-module applications.