Skip to content

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.

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.

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.

The following characteristics apply to both apps (with UI) and extensions (without UI).

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.

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.

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.

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.

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.

The following characteristics apply only to apps with UI, not to extensions.

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).

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.

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 configuration
export 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)
}
];

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)

Apps expose their routes through a constant that the host application imports:

// In my-app/src/lib/lib.routes.ts
export 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)
}

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.

Extensions make their capabilities known to the shell during application initialization.

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();
}
}

The host application imports and registers all app and extension services during bootstrap:

// In host app's app.config.ts
import { 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.

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.

As mentioned above, the primary distinction between apps and extensions is the user interface:

AspectApps (with UI)Extensions (without UI)
User InterfaceFull UI in content areaNo standalone UI
Sidebar NavigationVisible with dedicated buttonNot visible
LoadingLazy loaded on demand when user navigates to appEagerly loaded at Angular application initialization
Primary PurposeDirect user interactionProvide capabilities for apps to use
User AccessUsers navigate to the appUsers access through apps that use the extension
VisibilityHighly visible to usersInvisible 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.

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.

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();
}
}

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)
}));
}
}

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

To create a new app for your client, use the yuuvis® client CLI:

Terminal window
yuv generate app my-custom-app

The 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.

These practices apply to both apps and extensions unless specifically noted.

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”).

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.

Always handle the case where expected capabilities might not be available:

// Get flavors - might be empty if no packages registered any
const flavors = this.#shell.getObjectFlavors();
// Get app route - might be undefined if app not installed
const 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 available
const actions = this.#actionsService.getApplicableActions(selection);
const menuItems = actions.map(action => this.toMenuItem(action));

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-core services
  • Event communication through the shell’s event bus

This ensures consistency and reduces development effort.

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.ts file
  • 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