Mobile App Config Reference
The features.mobile_app key controls NativePHP Mobile frontend generation. It is a sub-key of the module config's features object.
Shape
"mobile_app": {
"enabled": true,
"icon": "PackageIcon",
"list": {
"card": {
"icon": "PackageIcon",
"titleField": "name",
"subtitleFields": ["code"],
"bodyFields": ["description"],
"footerBadge": "status?.name"
}
}
}| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Set true to generate NativePHP Mobile pages/components for this module. |
mode | string | "online" | Not read by this package. A consumer-side convention (e.g. SYSTEM_SHELL's make:module) for deciding whether it invokes the sync generators. See Sync Mode below. |
icon | string | "LayersIcon" | Lucide icon name used in the mobile navigation and list card header. |
list.card | object | auto-resolved | Controls the mobile list card layout — see below. |
list.card Object
The mobile list displays records as cards. This object controls what appears in each card slot.
"card": {
"icon": "PackageIcon",
"titleField": "name",
"subtitleFields": ["sku", "code"],
"bodyFields": ["description"],
"footerBadge": "status?.name"
}| Key | Type | Description |
|---|---|---|
icon | string | Lucide icon for the card header. Falls back to mobile_app.icon. |
titleField | string | Column name shown as the card title. Falls back to frontend.list.primaryDisplayField then "name". |
subtitleFields | string[] | Column names shown below the title as secondary text. Auto-resolved from frontend.list.fields if omitted. |
bodyFields | string[] | Column names shown in the card body. Auto-resolved: prefers columns matching `description |
footerBadge | string|null | JS accessor path for a colored badge in the card footer (e.g. "status?.name"). null to hide. |
Auto-resolution Fallback Chain
If card is empty or partial, MobileAppConfigResolver fills gaps:
- icon →
mobile_app.icon→"LayersIcon" - titleField →
frontend.list.primaryDisplayField→frontend.view.primaryDisplayField→"name" - subtitleFields → first non-title, non-system, non-action column from
frontend.list.fields - bodyFields → first column whose name matches
description|body|notes|details|bio|summary|about; empty if none match
Generated Files (when enabled: true)
| File | Path |
|---|---|
| List page | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}ListPage.vue |
| Create page | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}CreatePage.vue |
| Edit page | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}EditPage.vue |
| Delete page | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DeletePage.vue |
| View layout | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DetailsLayout.vue |
| View overview | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DetailsOverviewPage.vue |
| View history | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DetailsHistoryPage.vue |
| List component | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}ListComponent.vue |
| Create form | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}CreateFormComponent.vue |
| Edit form | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}EditFormComponent.vue |
| Delete form | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}DeleteFormComponent.vue |
| Routes | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/routes.ts |
modules.json | MOBILE_APP/resources/js/src/modules.json (updated in-place) |
menus.json | MOBILE_APP/resources/js/src/menus.json (updated in-place) |
Mobile Backend Files (always generated by make:mobile-modules / make:mobile-scaffold)
These are generated regardless of mobile_app.enabled when running a mobile scaffold command:
| File | Path |
|---|---|
| Model | MOBILE_APP/app/Modules/{Group}/{Module}/{Module}Model.php |
| Controller | MOBILE_APP/app/Modules/{Group}/{Module}/{Module}Controller.php |
| Routes | MOBILE_APP/app/Modules/{Group}/{Module}/Routes/api.php |
| Migration | MOBILE_APP/app/Modules/{Group}/{Module}/Migrations/{date}_create_{table}_table.php |
| Seeder data | MOBILE_APP/app/Modules/{Group}/{Module}/Seeders/{Module}SeederData.json |
| List service | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}ListService.php |
| Create service | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}CreateService.php |
| View service | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}ViewService.php |
| Edit service | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}EditService.php |
| Delete service | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}DeleteService.php |
| Delete check | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}DeleteCheckService.php |
| Activity list | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}ActivityListService.php |
| Bulk action | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}BulkActionService.php |
| Sync service | MOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}SyncService.php (offline/both only) |
| Sync composable | MOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/composables/use{Module}Sync.ts (offline/both only) |
| Registry | MOBILE_APP/app/Modules/registry.json (updated in-place) |
Sync Mode
This package does not gate sync-file generation on a
modekey.MobileSyncServiceGeneratorandMobileSyncComposableGeneratorare plain generators like any other — when a caller invokesgenerate()on them, they unconditionally write{Module}SyncService.php/use{Module}Sync.ts, regardless of what (if anything) is set atfeatures.mobile_app.mode. There is no code in this package that reads amodekey to decide whether to run them; agrepformodeacrosssrc/turns up nothing related to mobile sync gating.The table below (
"online"/"offline"/"both") describes a consumer-side convention — e.g. howSYSTEM_SHELL'smake:modulecommand chooses which mobile generators to invoke based onmodule.json. If you're driving this package directly (not throughmake:module), it is entirely your responsibility to readmode(or any other flag) from your own config and decide whether to call the sync generators at all — the engine will happily generate sync files for every mobile module you point it at,modeor nomode.
| Value | SyncService | SyncComposable | Use when |
|---|---|---|---|
"online" (default) | ✗ | ✗ | Module data is always fetched live from the API — no local storage needed. |
"offline" | ✅ | ✅ | Module works entirely offline; data is synced to/from the device on demand. |
"both" | ✅ | ✅ | Module supports both live API access and offline sync. |
How to set it (consumer convention, not enforced by this package)
Edit the module's module.json (at BACKEND/app/Project/Modules/{Group}/{Module}/module.json):
"features": {
"mobile_app": {
"enabled": true,
"mode": "offline"
}
}Then re-run php artisan make:module {Group}/{Module} --force. The mode value persists across re-generations. This flow only works if the calling application's make:module command actually reads mode and conditionally invokes the sync generators — this package itself has no opinion on the key.
What the sync files do
{Module}SyncService.php — two API endpoints:
POST /api/{route-prefix}/sync/push— accepts records from the device, upserts byuuid, setslast_synced_atGET /api/{route-prefix}/sync/pull?since=2026-01-01T00:00:00Z— returns records updated since a given timestamp
use{Module}Sync.ts — Vue composable that wraps both endpoints with:
- Reactive
isOnline/isSyncing/canSyncstate push(records)andpull()methodslastSyncedAtpersisted inlocalStorage
Notes
- NativePHP Mobile embeds a full Laravel app on the device. The Vue SPA calls
http://localhost/api/...locally. - The device SQLite cannot be introspected during development. Use
make:mobile-modules --blueprint=file.json(runs fromSYSTEM_SHELL/BACKEND) ormake:mobile-scaffold(runs from insideMOBILE_APPagainst the local dev SQLite). - All mobile backend stubs are SQLite-safe:
TEXTinstead ofJSONcolumns,LIKEfor text search, directuuid()->primary()syntax, no MySQL-only expressions.