Skip to content

Examples

Create the first App

Generate an App and Resource, install them for one tenant, and verify the terminal and browser results.

Example type Recipe

Overview

First App example

This recipe starts from an empty local environment and follows the complete flow: create the Workshop App and its Note Resource, activate the App, install it for a tenant, and open it in the browser. It focuses on what each command creates and where to confirm success. The Generate a Resource example owns the step-by-step view of the work screens added by the Resource.

Final result

StageWhat it creates or changesWhere to confirm it
Generate the AppPackage metadata, Manifest, Service Provider, Overview, routes, locales, and package documentation and test baselines under packages/workshop/The App generator's CREATE output and package files
Generate the ResourceThe Note model, Policy, Controller, wsp_notes migration, Resource Module, Filament Resource, and list, form, show, and inspector frontend surfacesThe Resource generator's CREATE and UPDATE output
Activate in the hostLocal Composer package registration, a root pnpm-lock.yaml workspace entry, the Installed App map, and descriptor, translation, and permission synchronizationPackage app activation complete. and the package doctor
Install for a tenantThe wsp_notes table and active App installation state in the selected tenantThe development install plan's install action
Verify the screenWorkshop in the App Launcher, Note in the App Menu, and working create, list, and detail screensThe practice tenant in a browser

The App baseline alone has no model, migration, or business screen. Adding the Note Resource still does not expose it to a tenant. Host activation makes the code loadable; tenant installation switches that code on for the selected tenant.

App classes and Core registration flow

The generator's center is not its file count. It is the registration contract between WorkshopServiceProvider and WorkshopAppManifest.

Workshop classExtends or implementsResponsibility
WorkshopServiceProviderExtends Laravel ServiceProviderReads App metadata from composer.json and passes the Manifest to Core's AppRegistrar
WorkshopAppManifestExtends SDK AbstractPackageAppManifest and implements NavigationContributionPublishes stable App identity, contribution discovery roots, migrations and Filament Resources, the Overview surface, and the App Launcher entry
Composer loads WorkshopServiceProvider
  → provider creates AppDefinition through AppPackageMetadataReader
  → creates WorkshopAppManifest(AppDefinition)
  → AppRegistrar::register(manifest)
  → AppRegistry registers the App and passes contributionLocations()
    to NexiaContributionRegistry
  → Core catalogs consume the App Launcher, Overview, migrations, and later Resource contributions
  → tenant installation controls visibility and execution for that tenant

AbstractPackageAppManifest fixes the package-metadata-backed id(), name(), and definition() behavior. Workshop adds its discovery locations and screen entries on top. Copying an App directory alone therefore does not register it: Composer must load the Service Provider, which must pass the Manifest to AppRegistrar.

Before you run it

  • Core's app, vite, postgres, and redis services are running.
  • Core runs with APP_ENV=local; this recipe uses the local-only development installer.
  • You have an empty practice tenant. This recipe does not create a tenant.
  • Your terminal is at the Core repository root.
  • packages/workshop does not exist, so the generator can show its complete CREATE output.

List the tenant IDs:

Code example
Shell
task artisan -- tenants:list --no-ansi

Copy the ID beside the practice domain. For example, this output identifies abc123def456 as the tenant to use:

id: abc123def456 ................................................... acme

Generate, activate, and install

Pass that ID as TENANT:

Code example
Shell
TENANT=abc123def456 sh docs/developers/examples/first-app/commands.sh

The script performs these operations in order:

  1. Preview the App plan with --dry-run, then generate it.
  2. Initialize packages/workshop as an independent Git repository.
  3. Preview the Note Resource plan, then generate it.
  4. Clear the compiled translation cache so it can discover the new catalogs.
  5. Activate the App in the host.
  6. Show the local development installation plan, then migrate, initialize, and install it for the selected tenant.
  7. Restart the App and Vite so both new entry points are loaded.

Read the terminal output

The App dry-run currently shows 19 paths as WOULD. The real run marks the same destinations as CREATE and ends with Package app scaffolded.

The Resource dry-run shows 20 new Resource files plus App-local route, frontend registration, and locale edits. The real run creates a timestamped migration like this one:

packages/workshop/database/migrations/tenant/
  2026_.._.._......_create_wsp_notes_table.php

Host activation ends with:

Package app activation complete.

For a new tenant, the installation dry-run contains one row:

+----------+----------+---------+
| App key  | App name | Action  |
+----------+----------+---------+
| workshop | Workshop | install |
+----------+----------+---------+

If it reports already-active, Workshop is already installed on that tenant. Choose an empty tenant when you want to observe the first installation.

Inspect the generated files

The important paths split ownership as follows:

packages/workshop/
  composer.json                         # App identity and Composer package
  src/WorkshopAppManifest.php           # App contributions exposed to the host
  src/WorkshopServiceProvider.php       # Route and locale registration
  src/Contribution/Resources/NoteModule.php
  src/Models/Note.php
  src/Http/Controllers/NoteController.php
  src/Policies/NotePolicy.php
  database/migrations/tenant/*_create_wsp_notes_table.php
  resources/js/overview/WorkshopOverviewSurface.tsx
  resources/js/resources/notes/         # list, form, show, and inspector
  resources/lang/{en,ko,zh}.json
  tests/Feature/NoteAuthorizationTest.php

vendor/amuzcorp/nexia-workshop is the Composer installation view. Always edit the source under packages/workshop.

The App source and local Composer overlay are ignored, but activation adds a pnpm workspace importer when one is missing, so Core's root pnpm-lock.yaml changes. Do not commit that practice change as though it were a product lock update. For a real App addition, review it deliberately with the Composer graph.

Verify in the browser

Reload the practice tenant and follow this path:

  1. Open Apps in the left App Launcher.
  2. Choose Other apps → Workshop.
  3. Choose Note from the App Menu.
  4. From the empty list, select New Note and save Installation check note.

The detail screen should show the name Installation check note with status Draft, and the list should report one record. Workshop appears only on the tenant selected through TENANT.

Verify from the terminal

Run the verifier with the same tenant ID:

Code example
Shell
TENANT=abc123def456 sh docs/developers/examples/first-app/verify.sh

It checks representative App and Resource files, permanent identity values, the independent Git boundary, Core's ignore boundary, and the Composer installation symlink. It then runs the package doctor and confirms that an installation plan can be calculated for the selected tenant without writing tenant state. The terminal check is complete when it prints:

First App lifecycle verified. Confirm the Note screen in the browser.

Common errors

SymptomCauseFixConfirm
Practice package already exists: packages/workshopThe recipe refuses to overwrite an existing practice packageStart from a clean practice checkout or inspect the existing packages/workshop in placetest ! -e packages/workshop succeeds before rerunning commands.sh
requires translation key [workshop.note.list.title]The compiled translation catalog predates Workshop and does not contain its new keysClear the translation catalog cache and activate the App againActivation prints both translation catalogs validated and Package app activation complete.
Installation succeeds but Workshop is absent from the launcherThe browser is on a different tenant than TENANT, or long-running PHP and Vite processes still hold the pre-package stateOpen the same tenant, run task app:reload, and restart ViteOther apps → Workshop → Note is visible

When activation cannot see the new translation key

Activation can report a generated key as missing:

requires translation key [workshop.note.list.title]

If that key exists in resources/lang/{en,ko,zh}.json, the compiled translation catalog predates the new package. The recipe clears it before activation. If you ran activation separately, recover with:

Code example
Shell
task artisan -- nexia-runtime:clear-translation-catalog-cache --no-ansi
task artisan -- nexia-apps:activate-package-app workshop

The retry must show both translation catalogs validated and Package app activation complete.

What remains a product decision

The generated result is an executable baseline, not a finished business App. Note carries only sample name and status fields, and the generated Policy denies delete by default. A product App must define its domain fields, state transitions, deletion and retention policy, authorization scope, search, and audit requirements before you adapt the generated files.

Included files

Generate, activate, and install

docs/developers/examples/first-app/commands.sh
Code example
Shell
#!/usr/bin/env sh
set -eu

: "${TENANT:?Set TENANT to the practice tenant ID shown by tenants:list.}"

app_root='packages/workshop'

test ! -e "$app_root" || {
  printf 'Practice package already exists: %s\n' "$app_root" >&2
  printf '%s\n' 'Use a clean practice checkout or inspect the existing package instead.' >&2
  exit 1
}

task artisan -- nexia-apps:make-package-app Workshop \
  --family=other \
  --key=workshop \
  --table-prefix=wsp \
  --icon=box \
  --sort=990 \
  --dry-run

task artisan -- nexia-apps:make-package-app Workshop \
  --family=other \
  --key=workshop \
  --table-prefix=wsp \
  --icon=box \
  --sort=990

git -C "$app_root" init -b feat/workshop-app

task artisan -- nexia-apps:make-package-resource workshop Note \
  --record-owner=legal_entity \
  --label-ko=노트 \
  --dry-run

task artisan -- nexia-apps:make-package-resource workshop Note \
  --record-owner=legal_entity \
  --label-ko=노트

# A compiled catalog created before this package cannot contain its new keys.
task artisan -- nexia-runtime:clear-translation-catalog-cache --no-ansi
task artisan -- nexia-apps:activate-package-app workshop

# A new local Workshop is unpublished; development installation migrates and
# initializes it for this explicit practice tenant.
task artisan -- nexia-apps:install-dev-app workshop --tenant="$TENANT" --dry-run
task artisan -- nexia-apps:install-dev-app workshop --tenant="$TENANT" --with-prerequisites

# Long-running PHP and Vite processes started before the new package existed.
task app:reload
docker compose restart vite
docker compose up --wait --no-deps vite

printf '%s\n' 'First App installed. Reload the practice tenant and open Apps > Workshop > Note.'

Verify the App lifecycle

docs/developers/examples/first-app/verify.sh
Code example
Shell
#!/usr/bin/env sh
set -eu

: "${TENANT:?Set TENANT to the same practice tenant ID used during installation.}"

app_root='packages/workshop'

for required in \
  "$app_root/composer.json" \
  "$app_root/src/WorkshopAppManifest.php" \
  "$app_root/src/WorkshopServiceProvider.php" \
  "$app_root/src/Contribution/Resources/NoteModule.php" \
  "$app_root/src/Models/Note.php" \
  "$app_root/src/Http/Controllers/NoteController.php" \
  "$app_root/src/Policies/NotePolicy.php" \
  "$app_root/resources/js/index.ts" \
  "$app_root/resources/js/resources/notes/surface/NoteListSurface.tsx" \
  "$app_root/resources/js/resources/notes/surface/NoteFormSurface.tsx" \
  "$app_root/resources/js/resources/notes/surface/NoteShowSurface.tsx" \
  "$app_root/resources/lang/en.json" \
  "$app_root/resources/lang/ko.json" \
  "$app_root/resources/lang/zh.json" \
  "$app_root/tests/Feature/NoteAuthorizationTest.php" \
  "$app_root/tests/Pest.php" \
  "$app_root/docs/README.md"
do
  test -f "$required" || {
    printf 'Missing generated App or Resource file: %s\n' "$required" >&2
    exit 1
  }
done

set -- "$app_root"/database/migrations/tenant/*_create_wsp_notes_table.php
test "$#" -eq 1 && test -f "$1" || {
  printf '%s\n' 'Expected exactly one wsp_notes tenant migration.' >&2
  exit 1
}

grep -Fq '"app_key": "workshop"' "$app_root/composer.json"
grep -Fq '"app_table_prefix": "wsp"' "$app_root/composer.json"

test "$(git -C "$app_root" rev-parse --show-toplevel)" = "$(cd "$app_root" && pwd)" || {
  printf '%s\n' 'The generated App Package is not an independent Git repository.' >&2
  exit 1
}

git check-ignore -q "$app_root" || {
  printf '%s\n' 'Core does not ignore the generated App Package.' >&2
  exit 1
}

test -L vendor/amuzcorp/nexia-workshop || {
  printf '%s\n' 'The Composer installation view is not linked to the Workshop source.' >&2
  exit 1
}

task artisan -- nexia-apps:doctor-package-app workshop --no-ansi
task artisan -- nexia-apps:install-dev-app workshop --tenant="$TENANT" --dry-run

printf '%s\n' 'First App lifecycle verified. Confirm the Note screen in the browser.'