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
| Stage | What it creates or changes | Where to confirm it |
|---|---|---|
| Generate the App | Package 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 Resource | The Note model, Policy, Controller, wsp_notes migration, Resource Module, Filament Resource, and list, form, show, and inspector frontend surfaces | The Resource generator's CREATE and UPDATE output |
| Activate in the host | Local Composer package registration, a root pnpm-lock.yaml workspace entry, the Installed App map, and descriptor, translation, and permission synchronization | Package app activation complete. and the package doctor |
| Install for a tenant | The wsp_notes table and active App installation state in the selected tenant | The development install plan's install action |
| Verify the screen | Workshop in the App Launcher, Note in the App Menu, and working create, list, and detail screens | The 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 class | Extends or implements | Responsibility |
|---|---|---|
WorkshopServiceProvider | Extends Laravel ServiceProvider | Reads App metadata from composer.json and passes the Manifest to Core's AppRegistrar |
WorkshopAppManifest | Extends SDK AbstractPackageAppManifest and implements NavigationContribution | Publishes 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, andredisservices 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/workshopdoes not exist, so the generator can show its completeCREATEoutput.
List the tenant IDs:
task artisan -- tenants:list --no-ansiCopy 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:
TENANT=abc123def456 sh docs/developers/examples/first-app/commands.shThe script performs these operations in order:
- Preview the App plan with
--dry-run, then generate it. - Initialize
packages/workshopas an independent Git repository. - Preview the
NoteResource plan, then generate it. - Clear the compiled translation cache so it can discover the new catalogs.
- Activate the App in the host.
- Show the local development installation plan, then migrate, initialize, and install it for the selected tenant.
- 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:
- Open Apps in the left App Launcher.
- Choose Other apps → Workshop.
- Choose Note from the App Menu.
- 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:
TENANT=abc123def456 sh docs/developers/examples/first-app/verify.shIt 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
| Symptom | Cause | Fix | Confirm |
|---|---|---|---|
Practice package already exists: packages/workshop | The recipe refuses to overwrite an existing practice package | Start from a clean practice checkout or inspect the existing packages/workshop in place | test ! -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 keys | Clear the translation catalog cache and activate the App again | Activation prints both translation catalogs validated and Package app activation complete. |
| Installation succeeds but Workshop is absent from the launcher | The browser is on a different tenant than TENANT, or long-running PHP and Vite processes still hold the pre-package state | Open the same tenant, run task app:reload, and restart Vite | Other 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:
task artisan -- nexia-runtime:clear-translation-catalog-cache --no-ansi
task artisan -- nexia-apps:activate-package-app workshopThe 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#!/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#!/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.'