Apps & Extensions
Apps and Extensions are the primary ways to deliver functionality in a yuuvis® MOMENTUM client. Both are self-contained packages that can provide features with or without a dedicated user interface.
Overview
Section titled “Overview”In the yuuvis® MOMENTUM shell, there are two types of packages that provide functionality:
Apps (with UI):
- Provide a complete user interface for a specific domain or workflow
- Appear in the sidebar navigation as a dedicated button
- Occupy the shell’s content area when activated by the user
- Examples: Document management app, workflow inbox, search app
Extensions (without UI):
- Provide capabilities without a user interface of their own
- Do not appear in the sidebar navigation
- Enhance functionality through extension points (Object Flavors, Actions, etc.)
- Examples: PDF processing extension, AI assistant extension
Many architectural principles described on this page apply to both apps and extensions. Both are self-contained, independent packages that communicate through the shell’s registry. The key difference is visibility: apps provide direct user interaction through their own interface, while extensions work behind the scenes to enhance capabilities.
Another important distinction is loading behavior: Apps consist of a UI part (lazy loaded on demand) and an extension service (loaded at Angular application initialization). Standalone extensions have only the extension service part and are always loaded at Angular application initialization to register their capabilities in the shell’s registry.
Fundamental Principles
Section titled “Fundamental Principles”Both apps and extensions share these fundamental characteristics:
- Operate independently from other apps and extensions in the system
- Have no knowledge of other apps or extensions - communicate only through the shell’s registry infrastructure
- Publish extension points that others can discover and consume without coupling
- Consume extension points registered by others without knowing which package provided them
- Are implemented as Angular libraries that can be packaged and published as NPM packages
- Can be versioned, distributed, and installed independently from each other and from the host application
The shell acts as the mediator, providing a registry infrastructure that enables all packages to discover, use and extend capabilities without direct dependencies.
Characteristics for Apps & Extensions
Section titled “Characteristics for Apps & Extensions”The following characteristics apply to both apps (with UI) and extensions (without UI).
Self-Contained Functionality
Section titled “Self-Contained Functionality”Each app and extension encapsulates everything needed for its specific purpose. A document management app contains all the views, components, and logic required for managing documents. A PDF processing extension packages all logic, services, and components needed for PDF handling. This encapsulation ensures that packages can be developed and maintained independently.
Independent Lifecycle
Section titled “Independent Lifecycle”Apps and extensions can be:
- Developed by different teams without coordination
- Tested in isolation with focused test suites
- Deployed separately from other apps and extensions
- Updated on independent schedules
This independence enables parallel development and reduces the risk associated with changes.
Package Independence
Section titled “Package Independence”A critical principle of the architecture is that apps and extensions must not have direct dependencies on each other. An app or extension:
- Has no knowledge of what other app or extension exist in the system
- Cannot import code directly from other app or extension packages
- Does not assume the presence of any specific app or extension
- Has the shell infrastructure libraries (
@yuuvis/client-core,@yuuvis/client-shell-core,@yuuvis/client-framework) as dependencies
This strict independence ensures that packages can be added or removed from a client without breaking other packages. All communication happens exclusively through the shell’s registry infrastructure, consuming capabilities by type rather than by source.
Registry-Based Communication
Section titled “Registry-Based Communication”Apps and extensions interact with each other through the shell’s service registry, not through direct references. When a package needs capabilities provided by others, it queries the shell’s registry:
export class MyAppComponent { private shell = inject(ShellService);
ngOnInit() { // Get all Object Flavors registered by ANY package const flavors = this.shell.getObjectFlavors();
// Get applicable flavors for a specific object const { applied, applicable } = this.shell.getAppliedObjectFlavors(dmsObject);
// Get app routes for navigation without knowing which apps are installed const driveRoute = this.shell.appBaseRoutes['io.yuuvis.app.drive']; }}The package doesn’t know or care which specific package registered these flavors - it simply consumes what’s available in the registry. This loose coupling enables true modularity.
Extension Point Publication
Section titled “Extension Point Publication”Apps and extensions don’t just consume from the registry - they also publish their own extension points for others to use. During initialization, a package registers its capabilities with the shell:
@Injectable()export class MyAppExtension implements ClientShellExtension { #shell = inject(ShellService); #actionsService = inject(ActionsService);
init(): Promise<any> { // Publish Object Flavors for others to discover this.#shell.exposeObjectFlavors([ { id: 'my-app.custom-flavor', label: 'Custom Processing', // ... flavor configuration } ]);
// Register actions that work with objects this.#actionsService.registerActions([ { id: 'my-app.custom-action', action: CustomAction } ]);
return Promise.resolve(); }}Other packages can now discover and use these flavors and actions without any knowledge of which package published them.
While this example shows an app’s extension service, standalone extensions follow the exact same pattern - they implement ClientShellExtension and register their capabilities in the same way.
Characteristics for Apps Only
Section titled “Characteristics for Apps Only”The following characteristics apply only to apps with UI, not to extensions.
Dedicated User Interface
Section titled “Dedicated User Interface”Apps provide their own graphical user interface that occupies the shell’s content area when the app is active. The UI is entirely controlled by the app, giving developers full freedom to implement the interaction patterns best suited for their use case.
Extensions do not provide standalone UI but may provide components that apps can embed (e.g., a flavor edit dialog, a custom form field).
Sidebar Navigation Presence
Section titled “Sidebar Navigation Presence”Every app is represented by a button in the sidebar navigation, making it discoverable and accessible to users. The button typically displays an icon and can show a label, allowing users to quickly identify and switch between available apps.
Extensions are not visible in the sidebar navigation and have no direct user access point.
Lazy Loading
Section titled “Lazy Loading”Apps are loaded on demand using Angular’s lazy loading mechanism. This reduces initial bundle size and improves startup performance:
// In the host application's routing configurationexport const apps: App[] = [ { id: 'io.yuuvis.app.drive', title: 'Drive', path: 'drive', iconName: 'cloud_circle', // Loaded only when user navigates to this app loadChildren: () => import('@yuuvis/app-drive').then(m => m.YuuvisDriveRoutes) }];App Architecture
Section titled “App Architecture”An app consists of two main parts: the UI containing the user interface and business logic, and the extension that registers capabilities with the shell.
my-app/├── src/ # App Library│ ├── lib/│ │ ├── lib.routes.ts # Route definitions│ │ ├── {app}.schema.ts # App types, interfaces, constants│ │ ├── {app}.icons.ts # Icon definitions│ │ ├── components/ # App UI components│ │ ├── services/ # App business logic│ │ └── actions/ # Context actions for objects│ └── public-api.ts # Public exports (routes, services)├── extensions/ # Extensions Library│ └── src/│ └── lib/│ ├── extensions.service.ts # ClientShellExtension implementation│ ├── {app}.schema.ts # Schema definitions for extensions│ ├── {app}.flavor.*.ts # Object and Create Flavor definitions│ ├── actions/ # Extension-specific actions│ └── components/ # Flavor apply/edit components└── package.json # Peer dependencies only (no app dependencies)Routes Configuration
Section titled “Routes Configuration”Apps expose their routes through a constant that the host application imports:
// In my-app/src/lib/lib.routes.tsexport const MyAppRoutes: Route[] = [ { path: '', component: AppFrameComponent, children: [ { path: 'dashboard', component: DashboardComponent }, { path: 'details/:id', component: DetailsComponent }, { path: '', redirectTo: 'dashboard', pathMatch: 'full' } ] }];The host application then references these routes using lazy loading:
// In host app's configuration{ id: 'io.yuuvis.app.myapp', title: 'My App', path: 'myapp', iconName: 'apps', loadChildren: () => import('@yuuvis/app-myapp').then(m => m.MyAppRoutes)}Frame Component
Section titled “Frame Component”The frame component serves as the main container for the app’s user interface. When the app is activated, the shell renders this component in the content area, and Angular’s router handles navigation between the app’s child routes.
Registration
Section titled “Registration”Extensions make their capabilities known to the shell during application initialization.
Extension Service Implementation
Section titled “Extension Service Implementation”The extension service is where an app or extension publishes its capabilities to the shell:
import { Injectable, inject } from '@angular/core';import { ClientShellExtension, ShellService } from '@yuuvis/client-shell-core';import { ActionsService } from '@yuuvis/client-framework';
@Injectable()export class MyAppExtension implements ClientShellExtension { #shell = inject(ShellService); #actionsService = inject(ActionsService);
init(): Promise<any> { // Register Object Flavors that enhance object metadata this.#shell.exposeObjectFlavors([ { id: 'myapp.document-flavor', label: 'Document Processing', description: 'Enhanced document processing capabilities', // ... flavor configuration } ]);
// Register Create Flavors for creating new objects this.#shell.exposeObjectCreateFlavors([ { id: 'myapp.create-document', label: 'Create Document', // ... create flavor configuration } ]);
// Register context actions this.#actionsService.registerActions([ { id: 'myapp.custom-action', action: CustomAction } ]);
return Promise.resolve(); }}Registering with the Host Application
Section titled “Registering with the Host Application”The host application imports and registers all app and extension services during bootstrap:
// In host app's app.config.tsimport { ApplicationConfig } from '@angular/core';import { importShellExtensions } from '@yuuvis/client-shell-core';import { MyAppExtension } from '@yuuvis/app-myapp/extensions';import { DriveExtension } from '@yuuvis/app-drive/extensions';import { PdfExtension } from '@yuuvis/extension-pdf'; // Extension without UI
export const appConfig: ApplicationConfig = { providers: [ // ... other providers importShellExtensions([ // Apps with UI register their extension services { id: 'io.yuuvis.app.myapp.extension', extension: MyAppExtension }, { id: 'io.yuuvis.app.drive.extension', extension: DriveExtension }, // Extensions without UI use the same mechanism { id: 'io.yuuvis.extension.pdf', extension: PdfExtension }, ]), ],};The importShellExtensions function uses Angular’s provideAppInitializer to ensure each extension’s init() method is called during application startup. This guarantees that all extension points are registered before any app is loaded.
Conditional Registration
Section titled “Conditional Registration”Apps can conditionally register extension points based on user permissions or feature flags:
@Injectable()export class MyAppExtension implements ClientShellExtension { #shell = inject(ShellService); #userService = inject(UserService);
init(): Promise<any> { this.#userService.user$.subscribe((user) => { if (user?.authorities.includes('MYAPP_ADVANCED_FEATURES')) { this.#enableAdvancedFeatures(); } });
return Promise.resolve(); }
#enableAdvancedFeatures() { this.#shell.exposeObjectFlavors(ADVANCED_FLAVORS); }}This pattern ensures that extension points are only visible to users with appropriate permissions.
Apps vs Extensions: User Interface
Section titled “Apps vs Extensions: User Interface”As mentioned above, the primary distinction between apps and extensions is the user interface:
| Aspect | Apps (with UI) | Extensions (without UI) |
|---|---|---|
| User Interface | Full UI in content area | No standalone UI |
| Sidebar Navigation | Visible with dedicated button | Not visible |
| Loading | Lazy loaded on demand when user navigates to app | Eagerly loaded at Angular application initialization |
| Primary Purpose | Direct user interaction | Provide capabilities for apps to use |
| User Access | Users navigate to the app | Users access through apps that use the extension |
| Visibility | Highly visible to users | Invisible to users (except through their effects) |
In practice: A document management app (with UI) might use a PDF processing extension (without UI) to enhance its document preview capabilities. Users interact with the app’s UI but benefit from the extension’s functionality without seeing it directly.
Both share the same architectural foundation - they are independent, registry-based packages that communicate through the shell’s extension point system.
Consuming Registry Data
Section titled “Consuming Registry Data”Both apps and extensions consume capabilities registered by other packages through the shell’s registry, without knowing which specific package provided them. This enables loose coupling and true modularity.
The examples below show apps consuming registry data, but extensions follow the exact same patterns when they need to discover and use capabilities from other packages.
Discovering Object Flavors
Section titled “Discovering Object Flavors”An app can query all available Object Flavors to enhance object metadata displays:
@Component({ selector: 'app-object-list', template: ` <div *ngFor="let object of objects"> <app-object-summary [object]="object" [flavors]="flavors" /> </div> `})export class ObjectListComponent implements OnInit { #shell = inject(ShellService);
// Get all flavors from ALL registered apps flavors: ObjectFlavor[] = [];
ngOnInit() { // Query the registry - don't know/care which app registered these this.flavors = this.#shell.getObjectFlavors(); }}Consuming Actions
Section titled “Consuming Actions”Apps can discover and execute actions registered by other apps:
export class ContextMenuComponent { #actionsService = inject(ActionsService);
getContextMenu(selection: DmsObject[]): MenuItem[] { // Get all actions applicable to this selection // Actions could be from ANY app - we don't know which const actions = this.#actionsService.getApplicableActions({ selection, context: 'object-context-menu' });
return actions.map(action => ({ label: action.label, icon: action.icon, execute: () => action.run(selection) })); }}Benefits of Registry-Based Consumption
Section titled “Benefits of Registry-Based Consumption”This pattern enables:
- Feature composition without direct dependencies between apps
- Optional capabilities based on which apps are installed
- Clean separation between provider and consumer
- Runtime flexibility - apps can be added/removed without code changes
- Testing isolation - apps can be tested independently with mock registries
Creating Custom Apps
Section titled “Creating Custom Apps”To create a new app for your client, use the yuuvis® client CLI:
yuv generate app my-custom-appThe CLI generates the complete app structure with:
- Angular module and root component
- Routing configuration
- App manifest
- Default icon
For detailed instructions, see the Using the CLI guide.
Best Practices
Section titled “Best Practices”These practices apply to both apps and extensions unless specifically noted.
Keep Packages Focused
Section titled “Keep Packages Focused”Each app or extension should address a specific domain or workflow. Avoid creating monolithic packages that try to do everything. Instead, create multiple focused packages that work together through the shell.
For apps: Focus on a single user workflow or feature area. For extensions: Provide a single, well-defined capability (e.g., PDF processing, not “document processing + AI + export”).
Design for Complete Independence
Section titled “Design for Complete Independence”Publish Useful Extension Points
Section titled “Publish Useful Extension Points”Make your capabilities discoverable by publishing extension points:
init(): Promise<any> { // Expose Object Flavors that enhance objects this.#shell.exposeObjectFlavors(MY_FLAVORS);
// Register actions that operate on objects this.#actionsService.registerActions(MY_ACTIONS);
// Provide Create Flavors for new object types this.#shell.exposeObjectCreateFlavors(MY_CREATE_FLAVORS);
return Promise.resolve();}This allows other packages to benefit from your functionality without knowing about your package.
Consume Registry Data Defensively
Section titled “Consume Registry Data Defensively”Always handle the case where expected capabilities might not be available:
// Get flavors - might be empty if no packages registered anyconst flavors = this.#shell.getObjectFlavors();
// Get app route - might be undefined if app not installedconst route = this.#shell.appBaseRoutes['some.app.id'];if (route) { // Navigate only if app is available this.#router.navigate([route, 'path']);}
// Get actions - filter and use what's availableconst actions = this.#actionsService.getApplicableActions(selection);const menuItems = actions.map(action => this.toMenuItem(action));Follow Shell Patterns
Section titled “Follow Shell Patterns”Use the shell’s infrastructure for:
- Navigation between views within your app using relative routes (apps only)
- User preferences through the shell’s settings service
- Backend communication through
@yuuvis/client-coreservices - Event communication through the shell’s event bus
This ensures consistency and reduces development effort.
Structure for Maintainability
Section titled “Structure for Maintainability”Organize your package into clear libraries:
For Apps (with UI):
- App Library: User interface, routing, business logic
- Extensions Library: Extension point registration, flavors, actions
- Keep shared types and interfaces in a separate
schema.tsfile - Separate reusable actions from app-specific components
For Extensions (without UI):
- Extension Library: Core functionality, services, extension point registration
- Provide components if needed (flavor dialogs, form fields) but no standalone routes
- Keep schema definitions separate from implementation