nativephp/mobile
NativePHP for Mobile lets Laravel developers build native iOS and Android apps using PHP. It embeds a PHP runtime and bridges Laravel with device APIs like camera, notifications, and filesystem, enabling rapid cross-platform mobile development without leaving the Laravel ecosystem.
NativePHP for Mobile compiles a Laravel app into a native iOS/Android app: SwiftUI and Jetpack Compose rendering EDGE Blade elements, driven by an on-device PHP runtime. There is no web server in front of native screens. Treat it as a screen framework, not a renderer — that framing answers most "why doesn't this work like Blade on the web?" questions before they arise.
The shortest path from zero to a buildable app:
composer create-project nativephp/mobile-starter my-app --no-interaction
cd my-app
php artisan native:install --force --no-interaction
Before you write a line of UI, do three things that are cheap now and expensive later:
NATIVEPHP_DEEPLINK_SCHEME=myapp to .env. It is baked in at build time, so adding it after you have a working build means a full rebuild. You will want it for native:open-url navigation testing almost immediately.column and text come from nativephp/mobile-ui. Run php artisan native:plugin:list and confirm it's registered. A screen that renders as nothing is almost always this. 4.6.0 surfaces plugin hook failures at build rather than shipping a broken app, but you still want to catch it here.php artisan vendor:publish --tag=native-ui-config
php artisan native-ui:generate-icons
Fill config/native-ui.php's theme block with your palette and fonts with semantic aliases ('headline' => 'ArchivoNarrow-Bold'). The token map is open-ended — add success, outline-variant, etc. and the matching bg-theme-* / text-theme-* / border-theme-* classes exist.Then get a baseline build on a simulator:
php artisan native:devices --json # prefer one already "booted"
php artisan native:run ios <udid> --build=debug --no-tty --json
If you're working in a scripted or agent-driven loop, write nativephp/agent-device.json = {"platform":"ios","udid":"<UDID>"} so every --json command resolves the target without repeating the UDID. Allow ~10 minutes for the first build.
First use case: a list → detail flow. Write one NativeComponent with a render() returning EDGE Blade, register it with Route::native(), and put a native:icon plus a handful of text/column elements on it. That single screen exercises plugin registration, icon generation, theme tokens, routing, and the build/verify loop — the cheapest possible proof the project is viable. Screenshot it with php artisan native:screenshot <os> --json and judge the PNG.
Where to look first when stuck: config/native-ui.php for tokens, app/Icons/ for symbol names, native:plugin:list for a missing renderer, and native:tail <os> --fatals --json for a boot failure. The docs at nativephp.com/docs/mobile/4/ cover routing, elements, and events — read them before inventing a pattern.
Native UI first is architecture, not style. Every screen is a NativeComponent registered with Route::native() and styled entirely with Tailwind utilities in class="..." / :class="...". No inline style, no ad-hoc styling props. The web view (native:web-view) is an escape hatch for embedding external content inside a native screen — never the foundation of one. Existing Livewire/Inertia screens should be migrated off, not extended. A useful scoping rule: anything that only works as a web view is not ready for the mobile roadmap.
Gate mobile routes by context. Put mobile screens in routes/mobile.php behind a native-context check so the web deployment never exposes them. This is the most common shape for the package — one Laravel app serving both surfaces — and it keeps routing logic in one place.
Design tokens, not literals. Define the palette once in config/native-ui.php and style with semantic theme classes throughout. Arbitrary bg-[#…] values are only justified for genuine data-driven colour (category identity, imagery), and even then belong in one PHP home — an enum or model — never inline per view. Theme classes accept opacity modifiers (bg-theme-primary/15), which is the tonal-fill idiom. Font aliases let you retheme without touching views.
Icons through generated enums.
@use('App\Icons\Ios')
@use('App\Icons\Android')
<native:icon :ios="Ios::Gearshape" :android="Android::Settings" :size="28" class="text-theme-primary" />
Cross-platform names like home resolve on both, but the typed enums are autocompletable and cannot misspell a symbol. Use @use in compiled views (they have no namespace) or fully-qualified cases. Never use emoji in UI text or buttons. When an SF Symbol has no obvious Material equivalent, check 4.6.0's expanded symbol table before hand-picking — it maps far more names than earlier releases.
Drive navigation from the CLI, not your thumbs. native:open-url "<scheme>://<route>" <os> --json gives you deterministic navigation, which combined with native:screenshot after each step is a reproducible screen-by-screen verification pass. Expect a one-time "Open in ?" confirmation on iOS simulators for custom schemes; dismiss once and it stays dismissed for the session.
The build/verify loop is the daily workflow.
edit code
└─ native:run <os> <udid> --build=debug --no-tty --json
├─ ok:false, stage:build|install → read .logTail; fix; rebuild
├─ ok:false, stage:verify → built but not running: native:tail <os> --fatals --json
└─ ok:true
└─ native:screenshot <os> --json → read the PNG
└─ native:open-url "<scheme>://<route>" <os> --json → screenshot each screen
native:run --json emits one JSON object on the last stdout line; exit code is 0 only when ok:true, so shell branching works directly. Define "booted" strictly: ok:true and native:status <os> --json shows running:true a few seconds later and the screenshot isn't the red error screen.
React to device state instead of polling. 4.6.0 adds OrientationChanged and System::orientation(). Thermal state and expanded getInfo telemetry land in the same release — enough to report device health from field/ops apps without touching native code. Declare supported languages in config so localisation is a bounded roadmap item rather than a rewrite.
Top-bar actions are data, not markup. Configure them declaratively and use the opt-in disabled prop (4.6.0) to express "not yet valid" instead of conditionally hiding actions — that avoids layout shift between states. If you're extending the element set with a UI plugin, register custom Blade @event names through the plugin hook (also 4.6.0) rather than inventing your own dispatch.
CI and headless pipelines are first-class. 4.6.0 lets you build a simulator app without a running simulator, decoupling build verification from scarce simulator capacity. Pair that with boot-fatal reporting and pipelines can prove the app booted rather than asserting success from an exit code.
A clean laravel.log is not evidence the app booted. Apps that die before Laravel is up write nothing there. When native:run --json returns ok:false, stage:verify, go straight to native:tail <os> --fatals --json and read each event's exception.app_frame — the first line in your code, usually the one to fix. Anything thrown while the app runs (including native screens) reports through Laravel normally: native:tail <os> --lines=100 --json. An empty events list means nothing crashed; don't read absence of fatals as success if the screenshot is the red error screen.
Never edit files under nativephp/ios or nativephp/android. They're generated; your changes vanish on the next build.
Loop hygiene. Never use --watch for discrete iterations (it blocks). One rebuild at a time per app repo; don't run two agents against the same repo. Never simctl shutdown/erase a simulator you didn't personally boot. Remove throwaway debug code before the final rebuild.
Deep-link scheme is baked at build time. Changing NATIVEPHP_DEEPLINK_SCHEME later costs a full rebuild, and native:open-url silently fails to navigate if you never set it.
Theme class edge cases. A theme border can overrule an explicit border-{width} if you don't pin both. On iOS, inline top-bar backgrounds had paint issues around 26.x (fixed in 4.6.0) — if you're on an earlier 4.x, prefer theme tokens over inline top-bar styling.
Android packaging bites. Keystore passwords containing special characters were mangled before 4.6.0 (fixed). The Android APP_KEY now lives in the Keystore instead of being bundled — if you were depending on the old behaviour, re-read your release setup before shipping.
Play Store commits can fail when changes cannot be sent for review; 4.6.0 fixes the commit path, but treat store submission as a separate verification stage from build success.
Callback and expression parsing. 4.6.0 fixed two classes of bug that used to silently drop arguments in callback expressions. If you're upgrading from an early 4.x, re-test any non-trivial event wiring — code that "worked" may have been working with truncated arguments.
Extending the package means respecting its deserialization boundary. If you add a durable or cross-lane transport (spool, cache entry, native-event payload, queue message), authenticate before unserialize() — never after. Use HMAC sign for integrity (GlobalEventTransport::sign()), encrypt-seal for secrecy (AsyncTaskTransport::seal()), bind a purpose string plus any correlating id, verify with hash_equals or decrypt, and discard anything that doesn't line up. Web-view script can reach these paths, so treat payload origins as untrusted. Write a probe class whose __wakeup flips a static flag and assert it stays false for forged, tampered, wrong-key, and unsigned values.
Symbol gaps are a design constraint, not a bug. If you can't find a Material equivalent for an SF Symbol, you have three options in ascending cost: a closer cross-platform name from the expanded table, a paired Ios::/Android:: enum case, or a custom icon font. Don't ship a mismatched glyph and call it done — the two platforms read it differently.
Keep the mobile-only surface in one place. If mobile handlers leak into routes/web.php or shared controllers branch on platform, the "one app, two surfaces" benefit evaporates and every future mobile change becomes a web-deployment risk.
How can I help you explore Laravel packages today?