MemberJunction Open Apps
Open Apps is MemberJunction’s extension and distribution system. An Open App is its manifest (mj-app.json) — the single source of truth for the app — plus whatever it optionally bundles: a database schema, migrations, metadata, server-side logic, and/or client-side UI components. Every one of those is an additive, optional block; an app declares only the blocks it needs (a manifest-only app is valid), and the whole thing installs, upgrades, and removes through the MJ CLI.
Table of Contents
Section titled “Table of Contents”- Overview
- Architecture
- What You Need to Build an Open App
- Repository Structure
- Building Server-Side Packages
- Building Client-Side Packages
- App Manifest Reference
- Installation Lifecycle
- CLI Commands
- How It All Connects
- Configuring Your Project Layout
- Package Manager Support
- Worked Example: Acme CRM
- Key Concepts
- Troubleshooting
Overview
Section titled “Overview”An Open App is a versioned, installable extension for MemberJunction. Its manifest is the source of truth; every capability below is optional and additive — an app uses only what it needs:
- Uses semantic versioning — versions come from GitHub release tags
- Has a full lifecycle — install, upgrade, disable, enable, and remove via the
mjCLI - Optionally owns a dedicated database schema — when it ships database objects, preventing collisions with MJ core (
__mj) or other apps - Optionally manages its own migrations — Skyway (Flyway-compatible) runs SQL migrations scoped to the app’s schema
- Optionally extends shared metadata — entities, actions, prompts, integration catalogs, and UI configurations pushed via
mj-sync(and symmetrically retired on remove) - Optionally ships as npm packages — server packages load into MJAPI, client packages load into MJExplorer
- Optionally depends on other Open Apps — with semver ranges, resolved and installed automatically
- Works with any package manager — npm, pnpm, and yarn are all supported with automatic detection
The “form” of an app is just which optional blocks it declares: manifest-only, metadata-extending (e.g. an integration connector seeding __mj), schema-backed (its own schema + migrations, like bizapps-common), packages-only (code/providers), or any combination.
Architecture
Section titled “Architecture”┌─────────────────────────────────────────────────────────────┐│ GitHub Repository ││ mj-app.json │ migrations/ │ metadata/ │ packages/ │└───────┬─────────────────────────────────────────────────────┘ │ mj app install https://github.com/acme/mj-crm ▼┌─────────────────────────────────────────────────────────────┐│ MJ CLI (Orchestrator) ││ ││ 1. Fetch manifest from GitHub ││ 2. Validate manifest (Zod schema) ││ 3. Check MJ version compatibility ││ 4. Resolve + install dependencies ││ 5. Create database schema ││ 6. Run Skyway migrations ││ 7. Add npm packages to MJAPI/MJExplorer package.json ││ 8. Run package install (npm/pnpm/yarn — auto-detected) ││ 9. Update mj.config.cjs (server dynamic packages) ││ 10. Regenerate open-app-bootstrap.generated.ts (client) ││ 11. Execute lifecycle hooks ││ 12. Record installation in MJ: Open Apps entity │└─────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────┐ ┌─────────────────────────┐│ MJAPI (Server) │ │ MJExplorer (Client) ││ │ │ ││ mj.config.cjs │ │ open-app-bootstrap ││ dynamicPackages: { │ │ .generated.ts ││ server: [ │ │ ││ @acme/mj-crm- │ │ import '@acme/mj-crm- ││ server │ │ ng-bootstrap'; ││ ] │ │ ││ } │ │ // Triggers ││ │ │ // @RegisterClass ││ Loads on startup ──►│ │ // decorators ││ Calls startup export│ │ │└─────────────────────┘ └─────────────────────────┘What You Need to Build an Open App
Section titled “What You Need to Build an Open App”1. The App Manifest (mj-app.json)
Section titled “1. The App Manifest (mj-app.json)”Every Open App has a mj-app.json file at the repository root. This is the single source of truth for the app’s identity, packages, schema, and configuration.
{ "manifestVersion": 1, "name": "acme-crm", "displayName": "Acme CRM", "description": "Customer relationship management for MemberJunction", "version": "1.0.0", "license": "MIT", "icon": "fa-solid fa-handshake", "color": "#2196f3",
"publisher": { "name": "Acme Corporation", "email": "dev@acme.com", "url": "https://acme.com" },
"repository": "https://github.com/acme/mj-crm", "mjVersionRange": ">=5.0.0 <6.0.0",
"schema": { "name": "acme_crm", "createIfNotExists": true },
"migrations": { "directory": "migrations", "engine": "skyway" },
"metadata": { "directory": "metadata" },
"packages": { "server": [ { "name": "@acme/mj-crm-server", "role": "bootstrap", "startupExport": "LoadAcmeCRM" } ], "client": [ { "name": "@acme/mj-crm-ng-bootstrap", "role": "bootstrap", "startupExport": "LoadAcmeCRMComponents" } ], "shared": [ { "name": "@acme/mj-crm-core", "role": "library" } ] },
"dependencies": { "some-other-app": ">=1.0.0" },
"hooks": { "postInstall": "node scripts/post-install.js", "postUpgrade": "node scripts/post-upgrade.js", "preRemove": "node scripts/pre-remove.js" },
"categories": ["crm", "sales"], "tags": ["customer-management", "contacts", "deals"]}Manifest Field Rules
Section titled “Manifest Field Rules”| Field | Required | Description |
|---|---|---|
manifestVersion | Yes | Must be 1 |
name | Yes | Lowercase alphanumeric + hyphens, 3-64 chars (e.g., acme-crm) |
displayName | Yes | Human-readable name, max 200 chars |
description | Yes | 10-500 chars |
version | Yes | Valid semver (e.g., 1.0.0, 2.1.0-beta.1) |
publisher | Yes | Object with name (required), email and url (optional) |
repository | Yes | GitHub URL (e.g., https://github.com/acme/mj-crm) |
mjVersionRange | Yes | Semver range for MJ compatibility (e.g., >=5.0.0 <6.0.0) |
schema | No | Database schema config (name must be alphanumeric + underscores, 3-128 chars) |
migrations | No | Migration config (default engine: skyway, default directory: migrations) |
metadata | No | Dev-time metadata directory (not processed at install) |
packages | Yes | npm packages grouped by server, client, shared |
dependencies | No | Other Open Apps this app depends on (semver ranges) |
hooks | No | Lifecycle shell commands (postInstall, postUpgrade, preRemove) |
categories | No | Up to 5 discovery categories |
tags | No | Up to 20 discovery tags (lowercase + hyphens) |
Package Roles
Section titled “Package Roles”Each package entry has a role that describes its purpose:
| Role | Description | startupExport Required? |
|---|---|---|
bootstrap | Entry point loaded at startup; triggers @RegisterClass decorators | Yes |
actions | MJ Action implementations | No |
engine | Business logic engine classes | No |
provider | Data providers or integrations | No |
module | Angular modules | No |
components | Angular components | No |
library | Shared utilities and types | No |
Important: Packages with
role: "bootstrap"must have astartupExport— the named export that the MJ runtime calls to initialize the package and trigger@RegisterClassdecorators.
2. Database Migrations
Section titled “2. Database Migrations”If your app needs its own database tables, place SQL migration files in the migrations/ directory (or wherever migrations.directory points in your manifest).
Migration File Naming
Section titled “Migration File Naming”Skyway uses Flyway-compatible versioned migration naming:
V1__Initial_schema.sqlV2__Add_contacts_table.sqlV3__Add_deals_pipeline.sqlV1.1__Add_contact_notes.sqlRules:
- Prefix with
Vfollowed by a version number - Double underscore (
__) separates version from description - Underscores in the description replace spaces
- Files are applied in version order
- Once applied, a migration is never re-run (tracked in
flyway_schema_history)
Migration Content
Section titled “Migration Content”All migrations run against the app’s own schema. Use ${flyway:defaultSchema} as a placeholder for portability:
-- V1__Initial_schema.sqlCREATE TABLE ${flyway:defaultSchema}.Contact ( ID UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID(), FirstName NVARCHAR(100) NOT NULL, LastName NVARCHAR(100) NOT NULL, Email NVARCHAR(255), Phone NVARCHAR(50), CompanyName NVARCHAR(200), Status NVARCHAR(20) NOT NULL DEFAULT 'Active', CONSTRAINT PK_Contact PRIMARY KEY (ID), CONSTRAINT CK_Contact_Status CHECK (Status IN ('Active', 'Inactive', 'Lead')));
CREATE TABLE ${flyway:defaultSchema}.Deal ( ID UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID(), Name NVARCHAR(200) NOT NULL, ContactID UNIQUEIDENTIFIER NOT NULL, Amount DECIMAL(18,2), Stage NVARCHAR(50) NOT NULL DEFAULT 'Prospecting', CONSTRAINT PK_Deal PRIMARY KEY (ID), CONSTRAINT FK_Deal_Contact FOREIGN KEY (ContactID) REFERENCES ${flyway:defaultSchema}.Contact(ID));Note: Do NOT include
__mj_CreatedAt/__mj_UpdatedAtcolumns or foreign key indexes — MJ’s CodeGen handles those automatically after entity registration.
3. Metadata (Optional)
Section titled “3. Metadata (Optional)”The metadata/ directory holds declarative JSON files that define MJ entities, actions, prompts, and other records managed by mj-sync. This is a development-time concern — mj-sync push is used during development to push metadata into the database, and the install orchestrator does not process this directory at install time (it relies on migrations for DDL and seed data).
Typical structure:
metadata/├── actions/│ └── .acme-crm-actions.json├── applications/│ └── .acme-crm-application.json├── prompts/│ └── .acme-crm-prompts.json└── queries/ └── .acme-crm-queries.json4. npm Packages
Section titled “4. npm Packages”Your app ships as one or more npm packages. At minimum, you typically need:
Server Bootstrap Package
Section titled “Server Bootstrap Package”This package contains server-side logic (actions, providers, engine classes) and a bootstrap export that triggers @RegisterClass decorators:
import { RegisterClass } from '@memberjunction/global';import { BaseAction } from '@memberjunction/actions';
@RegisterClass(BaseAction, 'Acme CRM: Create Contact')export class CreateContactAction extends BaseAction { // ... implementation}
@RegisterClass(BaseAction, 'Acme CRM: Create Deal')export class CreateDealAction extends BaseAction { // ... implementation}
/** * Bootstrap export called by MJAPI at startup. * Simply importing this module triggers the @RegisterClass decorators above. */export function LoadAcmeCRM(): void { // No-op — the import side-effects register everything}The startupExport in the manifest ("LoadAcmeCRM") tells MJAPI which named export to call after dynamically importing the package.
Client Bootstrap Package
Section titled “Client Bootstrap Package”This package contains Angular components and a bootstrap export:
import { RegisterClass } from '@memberjunction/global';import { BaseResourceComponent } from '@memberjunction/ng-shared';import { AcmeCRMDashboardComponent } from './dashboard/dashboard.component';import { AcmeCRMContactsComponent } from './contacts/contacts.component';
// Register components with MJ's class factory@RegisterClass(BaseResourceComponent, 'AcmeCRMDashboard')export class AcmeCRMDashboardResource extends AcmeCRMDashboardComponent {}
@RegisterClass(BaseResourceComponent, 'AcmeCRMContacts')export class AcmeCRMContactsResource extends AcmeCRMContactsComponent {}
/** * Tree-shaking prevention + bootstrap export. * Called via the static import in open-app-bootstrap.generated.ts. */export function LoadAcmeCRMComponents(): void { // No-op — the import side-effects register everything}Shared Library Package (Optional)
Section titled “Shared Library Package (Optional)”Types, utilities, and business logic shared between server and client:
export interface AcmeContact { ID: string; FirstName: string; LastName: string; Email?: string; Phone?: string; CompanyName?: string; Status: 'Active' | 'Inactive' | 'Lead';}
export interface AcmeDeal { ID: string; Name: string; ContactID: string; Amount?: number; Stage: string;}5. GitHub Repository with Releases
Section titled “5. GitHub Repository with Releases”The CLI fetches your app manifest and migrations from GitHub. Your repository must:
- Have a
mj-app.jsonat the root - Use GitHub Releases with semver tags (e.g.,
v1.0.0,v1.1.0) - Publish npm packages to a registry (npmjs.com or a private registry)
The version in mj-app.json should match the GitHub release tag (minus the v prefix).
Repository Structure
Section titled “Repository Structure”Here’s the recommended directory layout for an Open App repository:
acme-mj-crm/├── mj-app.json # App manifest (REQUIRED)├── package.json # Root package.json for npm workspace├── tsconfig.json # Root TypeScript config│├── migrations/ # Database migrations│ ├── V1__Initial_schema.sql│ ├── V2__Add_notes_table.sql│ └── V3__Add_pipeline_stages.sql│├── metadata/ # MJ metadata (dev-time, mj-sync)│ ├── actions/│ │ └── .acme-crm-actions.json│ ├── applications/│ │ └── .acme-crm-application.json│ └── prompts/│ └── .acme-crm-prompts.json│├── packages/│ ├── core/ # Shared library (@acme/mj-crm-core)│ │ ├── package.json│ │ ├── tsconfig.json│ │ └── src/│ │ └── index.ts│ ││ ├── server/ # Server package (@acme/mj-crm-server)│ │ ├── package.json│ │ ├── tsconfig.json│ │ └── src/│ │ ├── index.ts # Exports LoadAcmeCRM()│ │ └── actions/│ │ ├── create-contact.ts│ │ └── create-deal.ts│ ││ └── ng-bootstrap/ # Client package (@acme/mj-crm-ng-bootstrap)│ ├── package.json│ ├── tsconfig.json│ └── src/│ ├── index.ts # Exports LoadAcmeCRMComponents()│ ├── dashboard/│ │ ├── dashboard.component.ts│ │ └── dashboard.component.html│ └── contacts/│ ├── contacts.component.ts│ └── contacts.component.html│└── scripts/ # Lifecycle hooks (optional) ├── post-install.js └── post-upgrade.jsBuilding Server-Side Packages
Section titled “Building Server-Side Packages”Server packages are loaded by MJAPI at startup through the dynamicPackages.server array in mj.config.cjs. When installed, the CLI adds entries like:
// mj.config.cjs (auto-managed by CLI)module.exports = { // ... existing config ... dynamicPackages: { server: [ { PackageName: '@acme/mj-crm-server', StartupExport: 'LoadAcmeCRM', AppName: 'acme-crm', Enabled: true } ] }};At MJAPI startup:
- MJAPI reads
dynamicPackages.server - For each enabled entry, it calls
import(PackageName) - It then calls the
StartupExportfunction (e.g.,LoadAcmeCRM()) - This triggers all
@RegisterClassdecorators in the package
Server Package Requirements
Section titled “Server Package Requirements”Your server package should:
- Export a named function matching
startupExportin the manifest - Use
@RegisterClassdecorators for all classes that need to be discoverable (actions, providers, etc.) - Depend on
@memberjunction/global(forRegisterClass) and any MJ packages it needs - Be published to npm (or a private registry specified in
packages.registry)
Building Client-Side Packages
Section titled “Building Client-Side Packages”Client packages are loaded by MJExplorer through static imports in the auto-generated open-app-bootstrap.generated.ts file. When installed, the CLI adds:
// AUTO-GENERATED — do not edit
// acme-crm (v1.0.0)import '@acme/mj-crm-ng-bootstrap';This static import ensures ESBuild includes your package in the bundle and triggers @RegisterClass decorators at module evaluation time.
Creating Resource Components
Section titled “Creating Resource Components”Resource components are Angular components that render as tabs within MJ Explorer applications. Each component:
- Extends
BaseResourceComponentfrom@memberjunction/ng-shared - Registers with
@RegisterClassusing a unique driver class name - Implements required abstract methods for display name and icon
import { Component } from '@angular/core';import { RegisterClass } from '@memberjunction/global';import { BaseResourceComponent, ResourceData } from '@memberjunction/ng-shared';
@RegisterClass(BaseResourceComponent, 'AcmeCRMDashboard')@Component({ selector: 'acme-crm-dashboard', template: ` <div class="dashboard-container"> <h2>CRM Dashboard</h2> <!-- Your dashboard content --> </div> `})export class AcmeCRMDashboardComponent extends BaseResourceComponent { async GetResourceDisplayName(data: ResourceData): Promise<string> { return 'CRM Dashboard'; }
async GetResourceIconClass(data: ResourceData): Promise<string> { return 'fa-solid fa-chart-line'; }
ngOnInit(): void { // Access context via this.Data (ResourceData) const config = this.Data.Configuration;
// Load your data, then signal completion this.LoadData().then(() => this.NotifyLoadComplete()); }
private async LoadData(): Promise<void> { // Your data loading logic }}Defining Application Nav Items
Section titled “Defining Application Nav Items”To make your resource components appear in MJ Explorer as navigable tabs, define an application in your metadata:
{ "fields": { "Name": "Acme CRM", "Description": "Customer relationship management", "Icon": "fa-solid fa-handshake", "Color": "#2196f3", "DefaultForNewUser": false, "DefaultSequence": 100, "Status": "Active", "NavigationStyle": "Both", "DefaultNavItems": [ { "Label": "Dashboard", "Icon": "fa-solid fa-chart-line", "ResourceType": "Custom", "DriverClass": "AcmeCRMDashboard", "isDefault": true }, { "Label": "Contacts", "Icon": "fa-solid fa-address-book", "ResourceType": "Custom", "DriverClass": "AcmeCRMContacts", "isDefault": false }, { "Label": "Deals", "Icon": "fa-solid fa-money-bill-trend-up", "ResourceType": "Custom", "DriverClass": "AcmeCRMDeals", "isDefault": false } ] }}The DriverClass value must exactly match the second argument of @RegisterClass(BaseResourceComponent, 'DriverClass').
Nav Item Types
Section titled “Nav Item Types”| Pattern | ResourceType | DriverClass | RecordID | Use Case |
|---|---|---|---|---|
| Custom Component | "Custom" | Required | Optional | Your own Angular component |
| Dashboard | "Dashboards" | Not needed | Required | Load an MJ Dashboard record |
| Route | N/A | N/A | N/A | Navigate to a URL route (use Route field) |
App Manifest Reference
Section titled “App Manifest Reference”See
Engine/manifest.reference.jsoncfor the authoritative, fully-annotated reference manifest — every block documented, with the minimal manifest-only form at the top. It’s kept schema-valid by a unit test, so it never drifts from the validator. The identity fields are the only required ones; every capability block below is optional and additive.
Identity Fields
Section titled “Identity Fields”| Field | Type | Validation |
|---|---|---|
name | string | /^[a-z][a-z0-9-]{1,62}[a-z0-9]$/ (3-64 chars, lowercase + hyphens) |
displayName | string | 1-200 chars |
description | string | 10-500 chars |
version | string | Valid semver (supports pre-release + build metadata) |
icon | string | Font Awesome class (optional) |
color | string | Hex color #RRGGBB (optional) |
Schema Configuration
Section titled “Schema Configuration”{ "schema": { "name": "acme_crm", "createIfNotExists": true }}name: Alphanumeric + underscores, 3-128 chars (/^_{0,2}[a-zA-Z][a-zA-Z0-9_]{1,126}[a-zA-Z0-9]$/)createIfNotExists: Iftrue(default), the CLI creates the schema during install
Package Configuration
Section titled “Package Configuration”{ "packages": { "registry": "https://npm.acme.com", "server": [ { "name": "@acme/mj-crm-server", "role": "bootstrap", "startupExport": "LoadAcmeCRM" } ], "client": [ { "name": "@acme/mj-crm-ng-bootstrap", "role": "bootstrap", "startupExport": "LoadAcmeCRMComponents" } ], "shared": [ { "name": "@acme/mj-crm-core", "role": "library" } ] }}registry: Optional custom npm registry URL (defaults to npmjs.com)server: Packages added to server workspacepackage.jsonclient: Packages added to client workspacepackage.jsonshared: Packages added to both server and client workspaces
Dependencies
Section titled “Dependencies”Dependencies can be a simple semver range string or an object with version and repository:
{ "dependencies": { "some-app": ">=1.0.0", "another-app": { "version": ">=2.0.0", "repository": "https://github.com/org/another-app" } }}When the dependency includes a repository, the CLI will automatically install it if it’s not already present.
Installation Lifecycle
Section titled “Installation Lifecycle”Install Flow (mj app install)
Section titled “Install Flow (mj app install)”| Step | Phase | What Happens |
|---|---|---|
| 1 | Fetch | Download mj-app.json from GitHub at the specified (or latest) tag |
| 2 | Validate | Parse and validate manifest against Zod schema |
| 3 | Compatibility | Check mjVersionRange against the running MJ version |
| 4 | Dependencies | Resolve dependency graph with topological sort |
| 5 | Dependencies | Recursively install any missing dependency apps |
| 6 | Schema | Check for schema name collisions |
| 7 | Schema | Create the database schema (CREATE SCHEMA acme_crm) |
| 8 | Migrations | Download and run Skyway migrations against the app’s schema |
| 9 | Record | Create MJ: Open Apps record with status Installing |
| 10 | Packages | Add npm packages to server/client workspace package.json files |
| 11 | Packages | Run package install (auto-detects npm/pnpm/yarn from lockfile) |
| 12 | Config | Add entries to dynamicPackages.server in mj.config.cjs |
| 13 | Config | Regenerate open-app-bootstrap.generated.ts for client imports |
| 14 | Hooks | Execute postInstall hook (if defined) |
| 15 | Finalize | Set app status to Active and record install history |
If any step fails, the orchestrator attempts compensating actions (e.g., dropping a newly created schema) and records the failure in the install history.
Upgrade Flow (mj app upgrade)
Section titled “Upgrade Flow (mj app upgrade)”Similar to install, but:
- Validates that the target version is newer than the installed version
- Reuses the existing schema (Skyway only applies new migrations)
- Updates the existing
MJ: Open Appsrecord - Runs
postUpgradehook instead ofpostInstall
Remove Flow (mj app remove)
Section titled “Remove Flow (mj app remove)”- Check for dependent apps (fails unless
--force) - Run
preRemovehook - Remove config entries, client bootstrap imports, and npm package references (in parallel)
- Run package install to clean up
- Remove entity metadata for the app’s schema
- Drop the database schema (unless
--keep-data) - Set app status to
Removed
CLI Commands
Section titled “CLI Commands”# Install an app from GitHubmj app install https://github.com/acme/mj-crmmj app install https://github.com/acme/mj-crm --version 1.2.0
# Upgrade an installed appmj app upgrade acme-crmmj app upgrade acme-crm --version 2.0.0
# Remove an installed appmj app remove acme-crmmj app remove acme-crm --keep-data # Preserve database schemamj app remove acme-crm --force # Remove even if other apps depend on it
# List installed appsmj app list
# Show app detailsmj app info acme-crm
# Disable/enable without removingmj app disable acme-crmmj app enable acme-crm
# Check for available updatesmj app check-updatesAfter install/upgrade/remove, you must:
- Restart MJAPI to pick up server-side package changes
- Rebuild MJExplorer to bundle the new client-side imports
How It All Connects
Section titled “How It All Connects”Server Side
Section titled “Server Side”mj.config.cjsgets a new entry indynamicPackages.server:{ PackageName: '@acme/mj-crm-server', StartupExport: 'LoadAcmeCRM', AppName: 'acme-crm', Enabled: true }- MJAPI startup reads this config and dynamically imports the package
- The import triggers
@RegisterClassdecorators, registering your actions/providers with MJ’sClassFactory - MJ’s runtime can now discover and execute your registered classes
Client Side
Section titled “Client Side”open-app-bootstrap.generated.tsgets a static import:import '@acme/mj-crm-ng-bootstrap';- ESBuild includes your package in the MJExplorer bundle
- The import triggers
@RegisterClass(BaseResourceComponent, 'AcmeCRMDashboard')decorators - When a user navigates to your app, MJ Explorer:
- Reads the application’s
DefaultNavItemsJSON - Finds
DriverClass: "AcmeCRMDashboard" - Looks up the component via
ClassFactory.GetRegistration(BaseResourceComponent, 'AcmeCRMDashboard') - Instantiates and renders your component in a tab
- Reads the application’s
Database
Section titled “Database”- Your app’s schema (e.g.,
acme_crm) is created automatically - Skyway runs your migrations against that schema
- A
flyway_schema_historytable tracks applied migrations within the schema - MJ CodeGen can register your tables as MJ entities for full CRUD support
Configuring Your Project Layout
Section titled “Configuring Your Project Layout”By default, the engine expects the standard MJ monorepo layout:
- Server workspace:
packages/MJAPI - Client workspace:
packages/MJExplorer
If your project uses a different layout (e.g., an apps/ directory), configure the openApps section in mj.config.cjs:
module.exports = { // ... database config ...
openApps: { // Custom workspace paths (relative to repo root) serverPackagePath: 'apps/MJAPI', clientPackagePath: 'apps/MJExplorer',
// Override auto-detected package manager packageManager: 'pnpm', // 'npm' | 'pnpm' | 'yarn'
// Version strategy for writing dependencies versionStrategy: 'catalog', // 'semver' | 'catalog' | 'workspace' | 'auto'
// Custom bootstrap file location within client workspace clientBootstrapSubpath: 'src/app/generated/open-app-bootstrap.generated.ts',
// Additional workspace targets (for multi-app monorepos) additionalTargets: [ { Path: 'apps/AdminPortal', Role: 'client' }, { Path: 'services/Background', Role: 'server' } ],
// GitHub token for private app repos github: { token: process.env.GITHUB_TOKEN } }};Configuration Fields
Section titled “Configuration Fields”| Field | Default | Description |
|---|---|---|
serverPackagePath | 'packages/MJAPI' | Path to server workspace relative to repo root |
clientPackagePath | 'packages/MJExplorer' | Path to client workspace relative to repo root |
packageManager | Auto-detected | Force a specific package manager ('npm', 'pnpm', or 'yarn') |
versionStrategy | 'auto' | How dependency versions are written (see below) |
clientBootstrapSubpath | 'src/app/generated/open-app-bootstrap.generated.ts' | Path to bootstrap file within client workspace |
additionalTargets | [] | Extra workspaces for packages to be installed into |
github.token | $GITHUB_TOKEN | GitHub token for fetching manifests from private repos |
Package Manager Support
Section titled “Package Manager Support”The engine auto-detects which package manager to use by checking for lockfiles in this order:
| Lockfile | Package Manager |
|---|---|
pnpm-lock.yaml | pnpm |
yarn.lock | yarn |
package-lock.json (or none) | npm |
You can override auto-detection by setting packageManager in mj.config.cjs (see above).
Version Strategies
Section titled “Version Strategies”When the engine adds a dependency to package.json, it uses a version strategy to determine the version string:
| Strategy | Version Written | When to Use |
|---|---|---|
semver | ^1.2.0 | Standard semver range (works with any package manager) |
catalog | catalog: | pnpm catalog protocol — versions managed centrally in pnpm-workspace.yaml |
workspace | workspace:* | pnpm/yarn workspace protocol — for local packages |
auto (default) | Depends | Uses catalog: if pnpm + pnpm-workspace.yaml has a catalog: section; otherwise ^version |
pnpm Catalog Example
Section titled “pnpm Catalog Example”If your project uses pnpm with a catalog, the engine writes catalog: as the version and you manage the actual version centrally:
packages: - 'apps/*' - 'packages/*'
catalog: '@acme/mj-crm-server': '^1.0.0' '@acme/mj-crm-ng-bootstrap': '^1.0.0' '@acme/mj-crm-core': '^1.0.0'// apps/MJAPI/package.json (auto-managed by CLI){ "dependencies": { "@acme/mj-crm-server": "catalog:", "@acme/mj-crm-core": "catalog:" }}yarn Example
Section titled “yarn Example”For yarn workspaces, the engine runs yarn install instead of npm install. No special configuration is needed beyond having a yarn.lock in your repo root.
Azure SQL Auto-Detection
Section titled “Azure SQL Auto-Detection”When connecting to Azure SQL Database (host ending in .database.windows.net), the engine automatically enables encrypt: true on the database connection. No manual configuration is needed.
MJ Version Detection
Section titled “MJ Version Detection”The engine detects the running MJ version for compatibility checking in this order:
@memberjunction/corepackage.json (works for all project layouts)packages/MJGlobal/package.json(fallback for the MJ monorepo itself)
Worked Example: Acme CRM
Section titled “Worked Example: Acme CRM”Step 1: Create the manifest
Section titled “Step 1: Create the manifest”{ "manifestVersion": 1, "name": "acme-crm", "displayName": "Acme CRM", "description": "Simple CRM with contacts and deals management", "version": "1.0.0", "license": "MIT", "icon": "fa-solid fa-handshake", "color": "#4caf50", "publisher": { "name": "Acme Corp" }, "repository": "https://github.com/acme/mj-crm", "mjVersionRange": ">=5.0.0 <6.0.0", "schema": { "name": "acme_crm" }, "migrations": { "directory": "migrations" }, "packages": { "server": [ { "name": "@acme/mj-crm-server", "role": "bootstrap", "startupExport": "LoadAcmeCRM" } ], "client": [ { "name": "@acme/mj-crm-ng-bootstrap", "role": "bootstrap", "startupExport": "LoadAcmeCRMComponents" } ] }}Step 2: Write migrations
Section titled “Step 2: Write migrations”-- migrations/V1__Initial_schema.sqlCREATE TABLE ${flyway:defaultSchema}.Contact ( ID UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID(), FirstName NVARCHAR(100) NOT NULL, LastName NVARCHAR(100) NOT NULL, Email NVARCHAR(255), CompanyName NVARCHAR(200), CONSTRAINT PK_Contact PRIMARY KEY (ID));
CREATE TABLE ${flyway:defaultSchema}.Deal ( ID UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID(), Name NVARCHAR(200) NOT NULL, ContactID UNIQUEIDENTIFIER NOT NULL, Amount DECIMAL(18,2), Stage NVARCHAR(50) NOT NULL DEFAULT 'Prospecting', CONSTRAINT PK_Deal PRIMARY KEY (ID), CONSTRAINT FK_Deal_Contact FOREIGN KEY (ContactID) REFERENCES ${flyway:defaultSchema}.Contact(ID));Step 3: Build server package
Section titled “Step 3: Build server package”import { RegisterClass } from '@memberjunction/global';import { BaseAction, RunActionParams } from '@memberjunction/actions';import { UserInfo } from '@memberjunction/core';
@RegisterClass(BaseAction, 'Acme CRM: Create Contact')export class CreateContactAction extends BaseAction { async Run(params: RunActionParams, contextUser: UserInfo): Promise<void> { // Implementation }}
export function LoadAcmeCRM(): void { /* triggers @RegisterClass */ }Step 4: Build client package
Section titled “Step 4: Build client package”import { Component } from '@angular/core';import { RegisterClass } from '@memberjunction/global';import { BaseResourceComponent, ResourceData } from '@memberjunction/ng-shared';
@RegisterClass(BaseResourceComponent, 'AcmeCRMDashboard')@Component({ selector: 'acme-crm-dashboard', template: '<div><h2>CRM Dashboard</h2><p>Welcome to Acme CRM</p></div>'})export class AcmeCRMDashboardComponent extends BaseResourceComponent { async GetResourceDisplayName(data: ResourceData): Promise<string> { return 'Dashboard'; } async GetResourceIconClass(data: ResourceData): Promise<string> { return 'fa-solid fa-chart-line'; }}
export function LoadAcmeCRMComponents(): void { /* triggers @RegisterClass */ }Step 5: Publish and install
Section titled “Step 5: Publish and install”# Publish packages to npmcd packages/server && npm publishcd packages/ng-bootstrap && npm publish
# Create a GitHub release tagged v1.0.0
# Install into a MemberJunction instancemj app install https://github.com/acme/mj-crm
# Restart MJAPI and rebuild MJExplorerKey Concepts
Section titled “Key Concepts”Schema Isolation
Section titled “Schema Isolation”An app that ships database objects owns a dedicated SQL Server schema (e.g., acme_crm); apps that extend MJ purely via metadata or code declare no schema. When a schema is present, this ensures:
- No table name collisions between apps or with MJ core (
__mjschema) - Clean removal —
DROP SCHEMA CASCADEremoves all app tables (metadata-extending apps are retired symmetrically via mj-sync deletion instead) - Clear ownership — every table belongs to exactly one app
Dependency Resolution
Section titled “Dependency Resolution”When app A depends on app B:
- The CLI performs a topological sort of the dependency graph
- Dependencies are installed depth-first before the dependent app
- Dependency versions are checked with semver satisfaction
- Removal of a depended-upon app is blocked unless
--forceis used
Version Compatibility
Section titled “Version Compatibility”mjVersionRangeuses standard semver range syntax (e.g.,>=5.0.0 <6.0.0)- The CLI checks this against the running MJ version before installation
- Upgrades must target a version newer than the currently installed version
Dynamic Package Loading
Section titled “Dynamic Package Loading”Server side: MJAPI reads mj.config.cjs → dynamicPackages.server[] and dynamically imports each package, calling its StartupExport function.
Client side: Static imports in open-app-bootstrap.generated.ts are resolved by ESBuild at build time. The file is regenerated whenever apps are installed/removed/toggled.
App Status Lifecycle
Section titled “App Status Lifecycle” ┌──────────┐ install │Installing│ ────────► └────┬─────┘ │ success ▼ ┌──────────┐ disable ┌──────────┐ │ Active │ ──────────────► │ Disabled │ └────┬─────┘ ◄────────────── └──────────┘ │ enable upgrade│ ▼ ┌──────────┐ │Upgrading │ └────┬─────┘ │ success ▼ ┌──────────┐ │ Active │ └────┬─────┘ │ remove ▼ ┌──────────┐ │ Removing │ └────┬─────┘ │ success ▼ ┌──────────┐ │ Removed │ └──────────┘
Any phase failure ──────► ┌──────────┐ │ Error │ └──────────┘Troubleshooting
Section titled “Troubleshooting””Schema already exists”
Section titled “”Schema already exists””The app’s schema name collides with an existing schema. Either:
- The app was previously installed and removed with
--keep-data - Another app or manual process created the schema
To reinstall over a kept schema, the CLI automatically detects and reuses it.
”Incompatible MJ version”
Section titled “”Incompatible MJ version””Your mjVersionRange doesn’t match the running MJ version. Update the manifest to support the installed MJ version, or upgrade MJ.
”Dependency not installed and no repository URL”
Section titled “”Dependency not installed and no repository URL””A dependency is declared as a simple version string (e.g., "some-app": ">=1.0.0") but is not already installed. Either:
- Install the dependency first:
mj app install https://github.com/org/some-app - Use the object form with a repository URL so the CLI can auto-install it:
{ "some-app": { "version": ">=1.0.0", "repository": "https://github.com/org/some-app" } }
Components not appearing after install
Section titled “Components not appearing after install”- Verify you restarted MJAPI and rebuilt MJExplorer
- Check
open-app-bootstrap.generated.tscontains your import (not commented out) - Check
mj.config.cjshas your server package entry withEnabled: true - Verify your
@RegisterClassdecorator names match theDriverClassvalues in your application’sDefaultNavItems
Migration failures
Section titled “Migration failures”- Check that migration files follow Flyway naming conventions (
V{version}__{description}.sql) - Verify SQL uses
${flyway:defaultSchema}instead of hardcoded schema names - Check the Skyway
flyway_schema_historytable in your app’s schema for applied migration records
pnpm catalog issues
Section titled “pnpm catalog issues”If you see ERR_PNPM_CATALOG_ENTRY_NOT_FOUND after installing an app:
- The engine wrote
catalog:as the version but the package isn’t in yourpnpm-workspace.yamlcatalog section - Add the package to your catalog manually, or set
versionStrategy: 'semver'inmj.config.cjsto use standard version ranges instead
yarn install failures
Section titled “yarn install failures”If yarn install fails after app installation:
- Ensure your
yarn.lockis committed and up to date - Try running
yarn installmanually from the repo root to see detailed errors - Check that the app’s packages are published to the registry configured in
.yarnrc.yml