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-nativeskill walks through the conversion.Style exclusively with Tailwind utility classes via
class="..."/:class="...". Never inline CSSstyle="..."or ad-hoc styling props.Use
native:iconfor iconography (SF Symbols on iOS, Material Icons on Android — cross-platform names likehomeresolve 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: ifapp/Icons/doesn't exist yet, runphp artisan native-ui:generate-iconsfirst (safe to run yourself; re-run with--refresh-materialto 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 # or android; compile and launch
php artisan native:run ios --watch # build, deploy, hot reload in one
php artisan native:watch # hot reload only
php artisan native:jump # device dev loop via the Jump app (QR code)
./native run # shortcut wrapper installed by native:install
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,700downloads straight intoresources/fonts/(no API key; libre-licensed, safe to bundle). Files come out as<Family>-<Style>.ttf, ready to use asfonttokens. Safe to run yourself. - App-wide default: set the
font-familytoken inconfig/native-ui.php(e.g.'Inter-Regular';'System'= platform default) — applies to text, buttons, inputs, and navigation chrome.native:font --defaultoffers to set this for you. Per-elementfontandfont-serif/font-monoclasses still win. - Chrome fonts: layouts take a
$fontproperty, bars a->font(), and per-screenNavBarOptions::make()->font(). - Weight gotcha: one font file = one weight. Avoid
font-boldon single-weight custom fonts (Android synthesizes a faux bold, iOS ignores it) — bundle the Bold file and reference it directly (font="Inter-Bold").fontonly changes the typeface; size/weight still come fromtext-*/font-*classes.
Data Binding & Reactivity
native:model="property"two-way binds any input-style element to a public property (the nativewire: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 # once, before first registration
php artisan native:plugin:register vendor/plugin-name # adds it to NativeServiceProvider
php artisan native:plugin:list # verify it shows as registered
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 theplugins/creating-pluginsdocs.
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:iconunless the user explicitly asks for emojis - Using Livewire patterns (
wire:model, Livewire's#[On]) in NativeComponents — usenative:modelandNative\Mobile\Attributes\On - Seeding via
DatabaseSeeder/db:seed— it never runs on device; seed from a migration'sup()instead - Missing
NATIVEPHP_APP_IDin.envbeforenative:install - Suggesting iOS commands on Windows/Linux
- Adding
safe-areaclasses 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 andnative:runwarns "installed but not registered"; always register and verify withnative: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