Skip to content
Reference

Installation content

The tenant installation lifecycle, its initialization states, and the three content sources that create tenant rows.

Installation Content

Choose installation-required rows, optional copyable content, or disposable demo data before adding tenant content. For an implementation of CopyableTemplateContribution::templates() and apply(), follow Copyable templates. Installation is separate from host activation; the dry-run reports the tenant installation plan without writing rows.

Signature

These commands cover the lifecycle. Host activation and tenant installation are separate operations:

Code example
Shell
task artisan -- nexia-apps:activate-package-app {package} [--build-assets] [--dry-run]
task artisan -- nexia-apps:install-tenant-app {app} [--dry-run] [--with-prerequisites]
task artisan -- nexia-apps:repair-tenant-app {app}
task artisan -- nexia-apps:install-dev-app {app} --tenant= [--dry-run] [--with-prerequisites]

Two more create tenant rows on request:

Code example
Shell
task artisan -- nexia-apps:apply-template {app} {template} --tenant= --actor= [--legal-entity=]
task artisan -- nexia-demo:seed-demo --tenant= [--force]

Minimal example

After host activation, select one local tenant ID from tenants:list. Installation applies pending App migrations before initialization. The development command requires APP_ENV=local and changes neither publication nor readiness:

Code example
Shell
task artisan -- tenants:list
nexia_tenant=REPLACE_WITH_TENANT_ID
task artisan -- nexia-apps:install-dev-app workshop --tenant="$nexia_tenant" --dry-run
task artisan -- nexia-apps:install-dev-app workshop --tenant="$nexia_tenant" --with-prerequisites

--dry-run prints the ordered install / reactivate / already-active plan for the App and its full prerequisite closure, and writes nothing.

Parameters: What creates tenant rows

The installation-content mechanisms have different triggers. Guarded tenant migrations and ordinary App commands can also create rows; they are separate from these mechanisms:

SourceRunsCreatesSkippable
App initializerDuring tenant installationThe minimum rows the App needs to operateNo
Copyable templateWhen an actor applies itAn editable tenant-owned copyYes
Demo dataOn explicit nexia-demo:seed-demoDisposable sample recordsAlways

These declarations do not persist tenant business rows by themselves:

ConceptTenant row effect
DescriptorNone — typed capability metadata
RegistryNone — runtime lookup layer
CatalogNone by itself — a code-owned definition
Process template descriptorChanges the editor document only; save/publish persists
Template application ledgerRecords provenance, never becomes the source

A Catalog creates nothing on its own. A separate materializer may project a code-owned definition into system-owned rows — nexia-access:sync-permissions reads the Permission Catalog and writes Permission rows — but that is the command's effect, not the Catalog's.

Output or return: Installation lifecycle

nexia-apps:install-tenant-app writes a row per App into tenant_app_installations, carrying an operational status plus an initialization state:

Initialization stateMeaning
pendingRow written, initializers not yet complete
initializedInitializers completed successfully
failedAn initializer failed; the row keeps the error message

The command's order is fixed:

  1. Validate the prerequisite closure, Central publication, and private tenant entitlement, then print the plan.
  2. Apply pending migrations for the closure in prerequisite order.
  3. Write pending installation rows.
  4. Synchronize the tenant's Permission catalog and baseline Core Roles.
  5. Run each App's initializers, each in its own transaction.
  6. Mark the plan initialized.

Because each initializer runs in its own transaction, work that already committed is not rolled back. The sequence stops at the failure, and the failing item plus every still-pending item in that plan are marked failed with the error.

InstalledAppResolver exposes an App only when its installation is active, its initialization is complete, and every prerequisite is also operational.

That is the rule behind app.installed:{app_key} failing closed. An App in pending or failed is unavailable to tenant runtime consumers. Its PHP routes may still be registered, but app.installed refuses requests.

nexia-apps:repair-tenant-app {app} replays the installation plan for a failed or outdated App. It is the supported recovery path — not manual row edits.

Normal HTTP, CLI, and signup installation share the distribution guard. Only the local console development command bypasses publication, entitlement, and preview restrictions; it still enforces prerequisites, descriptors, migrations, initialization, and user authorization. Use it again to recover or reactivate unpublished local Apps. A revoked or suspended App stays operational while its existing installation is active, but cannot be newly installed or reactivated. This keeps a temporary Central failure out of normal App requests without allowing lifecycle writes to fail open.

Step 4 synchronizes permission definitions. It does not change User authority. Protected App operations require an applicable Access Grant; installation alone supplies none.

App initializers

This interface is host-only. It lives under App\*, which an App Package may not import. A new package ships required rows in a guarded tenant migration instead. See Required installation content.

Code example
PHP
namespace App\AppRuntime;

interface TenantAppInitializerContribution
{
    public const DEFAULT_VERSION = '1';

    public function appKey(): string;
    public function initialize(TenantAppInitializationContext $context): void;
}

interface VersionedTenantAppInitializerContribution extends TenantAppInitializerContribution
{
    public function version(): string;
}
RequirementReason
Idempotentnexia-apps:repair-tenant-app replays it
Creates only minimum operational defaultsIt is not a data-loading mechanism
Never overwrites operator-edited contentA replay would destroy deliberate changes
Never creates demo recordsThose belong to demo data
Never overwrites copied templatesThose are tenant-owned

For host-maintained initializers, the versioned variant identifies a changed default shape; otherwise the version is '1'.

Copyable templates

CopyableTemplateContributionRegistry discovers code-defined sources — it is not a database catalog. CopyableTemplateFacade combines them with the current ApprovalFormPresetCatalog adapter.

Applying one records provenance in the tenant-local template_applications ledger:

Ledger fieldContent
App keyOwning App
Template key and versionThe immutable source identity
Scopetenant or legal_entity
Legal EntityNullable
ActorWho applied it
Result statusSuccess or failure
Failure reasonOn failure
Created resource referencesWhat the copy produced

The ledger is provenance only. It never becomes the template source catalog, and it does not track later edits to the copies.

Two current constraints worth knowing before you plan around them:

  • Approval form presets are tenant-scoped, so passing --legal-entity to nexia-apps:apply-template fails for them.
  • Process templates do not go through this path at all. Selecting one changes the unsaved BPMN editor document; normal definition save and publish owns persistence and paired-decision deployment.

Apps publish templates through Nexia\Templates\Contracts\CopyableTemplateContribution. Its templates() method declares the sources and apply() receives a TemplateApplicationContext carrying the actor and optional Legal Entity. The host owns discovery, execution, and the provenance ledger.

Demo data

Demo data is an explicit fixture set composed by Core from App-owned Nexia\Fixture\Contracts\FixtureContribution implementations. Core supplies scalar host identities through FixtureContext; Apps write only their own rows and can exchange stable Resource References without importing another App model. This remains disposable sample data, never an installation prerequisite.

Code example
Shell
task artisan -- nexia-demo:seed-demo --tenant="$nexia_tenant"
PropertyBehavior
Production executionRejected unless --force is passed
OrderProduction baseline first, then Core demo fixtures
App installation stateLeft unchanged — seeding installs and activates nothing
Tenant provisioningNever invokes it
InstallationNever depends on it
Missing or inactive AppIts contribution is skipped; other operational Apps continue

If a feature only works after demo seeding, its required rows belong in installation, not in demo data.

Errors

SymptomCauseResolution
app.installed:{key} refuses a requestInstallation is pending or failed, or a prerequisite is not operationalnexia-apps:doctor-package-app, then nexia-apps:repair-tenant-app
Installation stops before writing rowsMissing prerequisite, duplicate key, or a cycleFix the prerequisite graph; --with-prerequisites for non-interactive install
Activation fails on database connectionManifest sync is strictRun inside the container with PostgreSQL reachable
App installed but invisible to usersNo Role carries its permissionsAn administrator creates and assigns an App Role
--legal-entity rejected on nexia-apps:apply-templateThe template is tenant-scopedOmit the option
Feature broken on a fresh tenant, fine on a demo tenantRequired rows live in demo dataMove them into installation

Real usage

For optional content, read an App-owned CopyableTemplateContribution: its declaration lists available templates and apply() creates the editable tenant copy in the supplied context.

packages/payroll and packages/talent implement Nexia\Templates\Contracts\CopyableTemplateContribution directly. Their templates() declarations and idempotent apply() methods are the current App-side examples.

Source of truth: docs/developers/content/en/app-sdk/installation-content.md