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:
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:
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:
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:
| Source | Runs | Creates | Skippable |
|---|---|---|---|
| App initializer | During tenant installation | The minimum rows the App needs to operate | No |
| Copyable template | When an actor applies it | An editable tenant-owned copy | Yes |
| Demo data | On explicit nexia-demo:seed-demo | Disposable sample records | Always |
These declarations do not persist tenant business rows by themselves:
| Concept | Tenant row effect |
|---|---|
| Descriptor | None — typed capability metadata |
| Registry | None — runtime lookup layer |
| Catalog | None by itself — a code-owned definition |
| Process template descriptor | Changes the editor document only; save/publish persists |
| Template application ledger | Records 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 state | Meaning |
|---|---|
pending | Row written, initializers not yet complete |
initialized | Initializers completed successfully |
failed | An initializer failed; the row keeps the error message |
The command's order is fixed:
- Validate the prerequisite closure, Central publication, and private tenant entitlement, then print the plan.
- Apply pending migrations for the closure in prerequisite order.
- Write
pendinginstallation rows. - Synchronize the tenant's Permission catalog and baseline Core Roles.
- Run each App's initializers, each in its own transaction.
- 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.
InstalledAppResolverexposes 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.
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;
}| Requirement | Reason |
|---|---|
| Idempotent | nexia-apps:repair-tenant-app replays it |
| Creates only minimum operational defaults | It is not a data-loading mechanism |
| Never overwrites operator-edited content | A replay would destroy deliberate changes |
| Never creates demo records | Those belong to demo data |
| Never overwrites copied templates | Those 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 field | Content |
|---|---|
| App key | Owning App |
| Template key and version | The immutable source identity |
| Scope | tenant or legal_entity |
| Legal Entity | Nullable |
| Actor | Who applied it |
| Result status | Success or failure |
| Failure reason | On failure |
| Created resource references | What 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-entitytonexia-apps:apply-templatefails 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.
task artisan -- nexia-demo:seed-demo --tenant="$nexia_tenant"| Property | Behavior |
|---|---|
| Production execution | Rejected unless --force is passed |
| Order | Production baseline first, then Core demo fixtures |
| App installation state | Left unchanged — seeding installs and activates nothing |
| Tenant provisioning | Never invokes it |
| Installation | Never depends on it |
| Missing or inactive App | Its 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
| Symptom | Cause | Resolution |
|---|---|---|
app.installed:{key} refuses a request | Installation is pending or failed, or a prerequisite is not operational | nexia-apps:doctor-package-app, then nexia-apps:repair-tenant-app |
| Installation stops before writing rows | Missing prerequisite, duplicate key, or a cycle | Fix the prerequisite graph; --with-prerequisites for non-interactive install |
| Activation fails on database connection | Manifest sync is strict | Run inside the container with PostgreSQL reachable |
| App installed but invisible to users | No Role carries its permissions | An administrator creates and assigns an App Role |
--legal-entity rejected on nexia-apps:apply-template | The template is tenant-scoped | Omit the option |
| Feature broken on a fresh tenant, fine on a demo tenant | Required rows live in demo data | Move 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.
Related
- Copyable templates — choosing between the three sources
- Artisan commands — signatures and scope for every command above
- App lifecycle — where installation sits in the runtime
- Fix installation errors — diagnosing a failed install