Skip to content

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

json
"mobile_app": {
  "enabled": true,
  "icon":    "PackageIcon",
  "list": {
    "card": {
      "icon":           "PackageIcon",
      "titleField":     "name",
      "subtitleFields": ["code"],
      "bodyFields":     ["description"],
      "footerBadge":    "status?.name"
    }
  }
}
KeyTypeDefaultDescription
enabledbooleanfalseSet true to generate NativePHP Mobile pages/components for this module.
modestring"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.
iconstring"LayersIcon"Lucide icon name used in the mobile navigation and list card header.
list.cardobjectauto-resolvedControls 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.

json
"card": {
  "icon":           "PackageIcon",
  "titleField":     "name",
  "subtitleFields": ["sku", "code"],
  "bodyFields":     ["description"],
  "footerBadge":    "status?.name"
}
KeyTypeDescription
iconstringLucide icon for the card header. Falls back to mobile_app.icon.
titleFieldstringColumn name shown as the card title. Falls back to frontend.list.primaryDisplayField then "name".
subtitleFieldsstring[]Column names shown below the title as secondary text. Auto-resolved from frontend.list.fields if omitted.
bodyFieldsstring[]Column names shown in the card body. Auto-resolved: prefers columns matching `description
footerBadgestring|nullJS 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:

  1. iconmobile_app.icon"LayersIcon"
  2. titleFieldfrontend.list.primaryDisplayFieldfrontend.view.primaryDisplayField"name"
  3. subtitleFields → first non-title, non-system, non-action column from frontend.list.fields
  4. bodyFields → first column whose name matches description|body|notes|details|bio|summary|about; empty if none match

Generated Files (when enabled: true)

FilePath
List pageMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}ListPage.vue
Create pageMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}CreatePage.vue
Edit pageMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}EditPage.vue
Delete pageMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DeletePage.vue
View layoutMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DetailsLayout.vue
View overviewMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DetailsOverviewPage.vue
View historyMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/{Module}DetailsHistoryPage.vue
List componentMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}ListComponent.vue
Create formMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}CreateFormComponent.vue
Edit formMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}EditFormComponent.vue
Delete formMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/Components/{Module}DeleteFormComponent.vue
RoutesMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/routes.ts
modules.jsonMOBILE_APP/resources/js/src/modules.json (updated in-place)
menus.jsonMOBILE_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:

FilePath
ModelMOBILE_APP/app/Modules/{Group}/{Module}/{Module}Model.php
ControllerMOBILE_APP/app/Modules/{Group}/{Module}/{Module}Controller.php
RoutesMOBILE_APP/app/Modules/{Group}/{Module}/Routes/api.php
MigrationMOBILE_APP/app/Modules/{Group}/{Module}/Migrations/{date}_create_{table}_table.php
Seeder dataMOBILE_APP/app/Modules/{Group}/{Module}/Seeders/{Module}SeederData.json
List serviceMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}ListService.php
Create serviceMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}CreateService.php
View serviceMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}ViewService.php
Edit serviceMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}EditService.php
Delete serviceMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}DeleteService.php
Delete checkMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}DeleteCheckService.php
Activity listMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}ActivityListService.php
Bulk actionMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}BulkActionService.php
Sync serviceMOBILE_APP/app/Modules/{Group}/{Module}/Services/{Module}SyncService.php (offline/both only)
Sync composableMOBILE_APP/resources/js/src/pages/modules/{group}/{Module}/composables/use{Module}Sync.ts (offline/both only)
RegistryMOBILE_APP/app/Modules/registry.json (updated in-place)

Sync Mode

This package does not gate sync-file generation on a mode key. MobileSyncServiceGenerator and MobileSyncComposableGenerator are plain generators like any other — when a caller invokes generate() on them, they unconditionally write {Module}SyncService.php / use{Module}Sync.ts, regardless of what (if anything) is set at features.mobile_app.mode. There is no code in this package that reads a mode key to decide whether to run them; a grep for mode across src/ turns up nothing related to mobile sync gating.

The table below ("online" / "offline" / "both") describes a consumer-side convention — e.g. how SYSTEM_SHELL's make:module command chooses which mobile generators to invoke based on module.json. If you're driving this package directly (not through make:module), it is entirely your responsibility to read mode (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, mode or no mode.

ValueSyncServiceSyncComposableUse 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):

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 by uuid, sets last_synced_at
  • GET /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 / canSync state
  • push(records) and pull() methods
  • lastSyncedAt persisted in localStorage

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 from SYSTEM_SHELL/BACKEND) or make:mobile-scaffold (runs from inside MOBILE_APP against the local dev SQLite).
  • All mobile backend stubs are SQLite-safe: TEXT instead of JSON columns, LIKE for text search, direct uuid()->primary() syntax, no MySQL-only expressions.

Released under the Apache-2.0 License.