| name | nativephp-mobile |
| description | Builds fully native iOS and Android apps with PHP & Laravel. Activate when working with SuperNative screens (NativeComponent, Route::native, native:make), EDGE components (native:column, native:button, native:list, bottom-nav, top-bar, and 40+ more), NativeLayout chrome, native:model data binding, #[Computed]/#[Poll]/#[On] attributes, Native::test() component tests, native device APIs (camera, dialog, biometrics, scanner, geolocation, push notifications), NativePHP Artisan commands (native:run, native:install, native:watch, native:jump), deep links, secure storage, or mobile app deployment. |
NativePHP Mobile v4
NativePHP Mobile v4 ("SuperNative") builds fully native apps: SwiftUI on iOS, Jetpack Compose on Android,
driven entirely by PHP. Each screen is a NativeComponent PHP class (think Livewire, but for native views)
rendering EDGE Blade elements. There is no web server and — for native UI screens — no web view.
Native UI First — Non-Negotiable
Every screen you build or scaffold must be native UI: a NativeComponent + EDGE elements, registered with
Route::native().
-
Never create new screens as web views, Livewire pages, or Inertia pages. The web view is a legacy escape hatch
(native:web-view) for embedding web content inside a native screen — never the foundation of one.
-
If the user asks for a webview-based screen, build it natively with EDGE and briefly explain why (performance,
platform look-and-feel, no web-view lifecycle issues). Only fall back if they explicitly insist.
-
When you encounter existing webview/Livewire/Inertia screens in an app, suggest migrating them — the
nativephp-webview-to-native skill walks through the conversion.
-
Style exclusively with Tailwind utility classes via class="..." / :class="...". Never inline CSS
style="..." or ad-hoc styling props.
-
Use native:icon for iconography (SF Symbols on iOS, Material Icons on Android — cross-platform names like
home resolve on both). Never use emoji characters in UI text, labels, or buttons unless the user explicitly
asks for them. Prefer the typed icon enums (App\Icons\Ios, App\Icons\Android, App\Icons\AndroidOutlined,
bound with :ios / :android — they're autocompletable and can't misspell a symbol name. The enums are
generated, not shipped: if app/Icons/ doesn't exist yet, run php artisan native-ui:generate-icons
first (safe to run yourself; re-run with --refresh-material to update). Import them with @use (compiled
views have no namespace), or use fully-qualified cases:
@use('App\Icons\Ios')
@use('App\Icons\Android')
<native:icon :ios="Ios::Gearshape" :android="Android::Settings" :size="28" class="text-theme-primary" />
Documentation
Before implementing any feature, fetch the relevant docs using WebFetch. Find the right URL in
references/available-docs.md.
WebFetch("https://nativephp.com/docs/mobile/4/the-basics/routing", "Explain Route::native, navigation methods, and transitions")
Build Commands — Tell the User, Don't Run
Never auto-run these commands. Always tell the user to run them manually, and always ask which platform
(iOS or Android) first — never assume:
php artisan native:run ios
php artisan native:run ios --watch
php artisan native:watch
php artisan native:jump
./native run
The Vite dev server is opt-in in v4: add --vite to native:run/native:watch only when the app uses
JS/CSS HMR (web-view assets). Native UI screens hot-reload without Vite. npm run build -- --mode=ios|android
is only needed for apps that still ship web-view assets.
Getting Started
New apps: laravel new my-app --using=nativephp/mobile-starter, or composer require nativephp/mobile in an
existing app. Set env vars before php artisan native:install:
NATIVEPHP_APP_ID=com.yourcompany.yourapp
NATIVEPHP_APP_VERSION="DEBUG"
NATIVEPHP_APP_VERSION_CODE="1"
# Optional for iOS:
NATIVEPHP_DEVELOPMENT_TEAM=XXXXXXXXXX
OS support: macOS builds iOS + Android; Windows/Linux build Android only; WSL unsupported.
SuperNative Screens
Scaffold with php artisan native:make Counter (remove with native:rm). Register in routes (a
routes/mobile.php is a clean convention):
Route::native('/', Home::class);
Route::native('/item/{id}', ItemDetail::class);
Inside a NativeComponent: $this->param('id'), $this->data('key', 'default'), $this->navigate('/item/42'),
$this->back(), $this->replace('/login'), $this->exitToWeb('/dashboard'); chain
->transition(Transition::SlideFromBottom) to customize animation. In Blade, @navigate="/path" works on any
element (modifiers: @navigate.back, @navigate.replace.fade, @navigate.slideFromBottom).
Lifecycle hooks: mount() (first push only), onResume() (returning to the screen), onBackPressed()
(Android back button), unmount(), and updated{Property}() when a bound property changes. Mark a component
#[Lazy] to paint a placeholder instantly while a slow mount() runs in the background.
EDGE Elements
Screens are built from native: Blade components (the prefix is optional but preferred for clarity):
<native:column class="w-full h-full p-4 gap-4 bg-theme-background">
<native:text class="text-2xl font-bold">Welcome</native:text>
<native:text-input native:model="name" placeholder="Your name" />
<native:button label="Save" @press="save" />
</native:column>
~40 elements are available — layout (column, row, stack, scroll-view, spacer, pressable), content (text, image,
icon, divider, badge, progress-bar, activity-indicator), forms (button, button-group, text-input, toggle,
checkbox, radio-group, select, slider, chip), navigation (bottom-nav, top-bar, side-nav, tab-row), lists
(list, lazy-grid, carousel, refreshable), overlays (modal, bottom-sheet), and drawing (canvas, shapes). Fetch
the component's doc page before using it — required props are validated at render time.
Custom Fonts
Drop .ttf/.otf/.ttc files into resources/fonts/ — the build bundles them into the native project
automatically (a rebuild via native:run is needed for newly added files; tell the user). Reference a font by
its filename without extension: font="Inter-Bold" on native:text, native:button, and the text inputs
(fluent: ->font('Inter-Bold')).
- Google Fonts:
php artisan native:font Inter --weights=400,700 downloads straight into resources/fonts/
(no API key; libre-licensed, safe to bundle). Files come out as <Family>-<Style>.ttf, ready to use as
font tokens. Safe to run yourself.
- App-wide default: set the
font-family token in config/native-ui.php (e.g. 'Inter-Regular';
'System' = platform default) — applies to text, buttons, inputs, and navigation chrome. native:font --default offers to set this for you. Per-element font and font-serif/font-mono classes still win.
- Chrome fonts: layouts take a
$font property, bars a ->font(), and per-screen
NavBarOptions::make()->font().
- Weight gotcha: one font file = one weight. Avoid
font-bold on single-weight custom fonts (Android
synthesizes a faux bold, iOS ignores it) — bundle the Bold file and reference it directly
(font="Inter-Bold"). font only changes the typeface; size/weight still come from text-*/font-*
classes.
Data Binding & Reactivity
native:model="property" two-way binds any input-style element to a public property (the native wire:model).
Modifiers: .blur/.lazy, .debounce.300ms. updated{Property}() fires on change.
#[Computed] methods are read as properties ($this->total), memoized per frame, invalidated on state change;
#[Computed(persist: true)] survives re-renders until state changes.
#[Poll(5000)] on a method runs it on an interval then re-renders; on a class it just re-renders. In Blade:
native:poll="1s" on an element.
#[On(EventClass::class)] listens for native events (push taps, websocket messages via the Vibe plugin,
bridge completions); parameters bind by name to event properties; listeners auto-teardown on unmount. Use
$this->on(Event::class, $closure) for dynamic registration. (#[OnNative] is the legacy webview/Livewire
equivalent — do not use it in NativeComponents.)
Layouts (Shared Chrome)
A NativeLayout class declares nav bars, tab bars, and drawers once; attach with
Route::native(...)->layout(...) or Route::nativeGroup(TabsLayout::class, fn () => ...). Override navBar()
/ tabBar() using the NavBar, NavAction, TabBar, and Tab fluent builders. Layouts handle safe areas
automatically — never add safe-area classes to screens under a layout (reserve them for chrome-less screens).
Device APIs
Core built-ins (Native\Mobile\Facades): Device, Dialog, File, System — these ship inside
nativephp/mobile in v4. Also System::isIos() / isAndroid() and Blade directives @ios / @android.
Plugins (separate Composer packages): browser, camera, microphone, network, share (free); biometrics,
geolocation, scanner, secure-storage (paid); firebase (push notifications, proprietary); vibe
(websockets/Reverb). v4 conflicts with the old mobile-device/-dialog/-file/-system plugins — remove
them with php artisan native:plugin:uninstall --core-v4 when upgrading.
Installing a plugin is a four-step flow — never stop after composer require (an unregistered plugin does
nothing):
composer require vendor/plugin-name
php artisan vendor:publish --tag=nativephp-plugins-provider
php artisan native:plugin:register vendor/plugin-name
php artisan native:plugin:list
Then tell the user to rebuild with native:run (don't run it yourself). If native:run warns "The following
plugins are installed but not registered", the register step was missed.
Async calls dispatch events (Camera::getPhoto() → PhotoTaken); handle with #[On(PhotoTaken::class)] in a
NativeComponent. Sync calls return directly (SecureStorage::get(), Network::status()).
When a Capability Is Missing
Native functionality or a UI component that core and native-ui don't provide is not a reason to drop to
the web view. Escalate in this order:
- Existing plugin — check the plugin marketplace (
https://plugins.nativephp.com) and the core plugins
list. If a marketplace-lookup MCP tool is available in your session, use it to search.
- Custom plugin — scaffold with
php artisan native:plugin:create. Plugins bundle PHP facades/events,
Swift/Kotlin bridge functions, permissions, native dependencies (Gradle/SPM/CocoaPods), and can ship their
own native EDGE components — so custom native logic and custom native UI both belong in a plugin. See
references/plugin-best-practices.md and the
plugins/creating-plugins docs.
Database & Seeding
SQLite is the only database driver (deliberate — no remote DB credentials in a distributable binary; use an API
backend for sync). NativePHP creates the DB in the app container and runs migrations automatically on every
app start, as needed.
Seeding must go through migrations — there is no db:seed on device. Create a dedicated seed migration
(php artisan make:migration seed_app_settings) with the inserts in up(); migrations give you exactly the
seeding semantics you want (run once per installation, tracked, versioned, reversible). If a Seeder class helps
organize larger datasets, create it — but call it from the migration:
public function up(): void
{
(new \Database\Seeders\CategorySeeder)->run();
}
Test seed migrations for both fresh installs and upgrades of existing user databases — a bad migration on update
can destroy user data.
Testing
Component tests run in-process — no device or simulator. Scaffold with php artisan native:make-test Counter:
Native::test(Counter::class)
->assertSee('Count: 0')
->tap('Increment')
->assertSet('count', 1);
Native::visit('/profile/5') mounts by route; Native::fakeBridge() scripts native responses;
emitNative(Event::class, [...]) delivers device events in tests.
Legacy Web-View Apps (Maintenance Only)
Some existing apps still render in the web view (Livewire or Inertia). When maintaining them: the #nativephp
JS import (import { camera, dialog, on, off, Events } from '#nativephp') exposes device APIs; clean up JS
listeners with off() on unmount; #[OnNative(...)] handles events in Livewire components; the nativephpMobile()
Vite plugin and nativephpHotFile() belong in vite.config.js; add the nativephp-safe-area body class.
Do not extend these apps with new webview screens — build new screens natively and recommend converting the
rest with the nativephp-webview-to-native skill.
Common Pitfalls
- Building a screen in the web view when native UI can do it — always default to EDGE + NativeComponent
- Inline
style="..." or styling props on EDGE elements — Tailwind classes only
- Emoji characters as icons in labels/buttons/text — use
native:icon unless the user explicitly asks for emojis
- Using Livewire patterns (
wire:model, Livewire's #[On]) in NativeComponents — use native:model and
Native\Mobile\Attributes\On
- Seeding via
DatabaseSeeder/db:seed — it never runs on device; seed from a migration's up() instead
- Missing
NATIVEPHP_APP_ID in .env before native:install
- Suggesting iOS commands on Windows/Linux
- Adding
safe-area classes to screens already wrapped by a NativeLayout
- Expecting Vite HMR without passing
--vite (opt-in since v4)
- Installing a plugin with Composer but never running
native:plugin:register — the plugin silently does
nothing and native:run warns "installed but not registered"; always register and verify with
native:plugin:list
- Leaving the four v3 plugins (device/dialog/file/system) installed after upgrading — composer will refuse to
resolve; run
native:plugin:uninstall --core-v4
- Not fetching v4 docs before implementing — use WebFetch with URLs from
references/available-docs.md
For authoring plugins: references/plugin-best-practices.md