11 min read

Build Your First Mobile App with Laravel and NativePHP v3 (Free, Step-by-Step)

Learn how to build a native mobile app using Laravel and NativePHP v3. Free, open-source, and no Swift or Kotlin needed.

Build Your First Mobile App with Laravel and NativePHP v3 (Free, Step-by-Step)

You can build native mobile apps with Laravel now. Not a wrapped website. Not a PWA. Actual native apps that run on your phone, work offline, and access the camera, biometrics, and file system. All with PHP.

NativePHP v3 (also called NativePHP Air) shipped in February 2026 as a free, MIT-licensed framework. Before v3, you needed a paid license. Now? Three Composer commands and you're running a Laravel app on your phone. That's not marketing speak. I'm going to prove it in this post.

We're going to build a "Quick Notes" app from scratch. It stores notes locally with SQLite, lets you share notes with other apps, and shows native confirmation dialogs before deleting. You'll test it on your actual phone using Jump (no Xcode or Android Studio required to get started). And the whole thing runs on Livewire + Tailwind, so it'll feel like writing any other Laravel app.

Let's get into it.

What Changed in NativePHP v3 (and Why You Should Care)

If you tried NativePHP before, forget what you remember. v3 is a completely different beast.

The biggest change is that it's free. The core framework and essential plugins are MIT-licensed. No license keys, no private Composer repositories, no monthly subscription to start building. This alone removes the main barrier that kept most Laravel devs from trying it.

But that's just one piece. v3 also moved from a monolithic architecture to a plugin system. Instead of bundling every native feature into one massive package, you now install only what you need:

composer require nativephp/mobile-camera
composer require nativephp/mobile-dialog
composer require nativephp/mobile-share

Each plugin is a standalone Composer package with its own Swift and Kotlin code. Your app stays smaller and app store reviews go smoother because you're not requesting permissions for features you don't use.

Here's what's free (MIT):

  • Camera, Browser, Device, Dialog, File, Microphone, Network, Share, System plugins
  • The Jump testing app
  • The full core framework

And what's premium (one-time purchase):

  • Biometrics, Firebase Push Notifications, Geolocation, Scanner, SecureStorage

Plus there's a new feature called EDGE components that gives you truly native UI elements like top bars and bottom navigation. Not web view approximations. Actual native components rendered by the OS.

If you've been building with Laravel's AI SDK or exploring new ecosystem tools like Laravel Blaze, NativePHP v3 fits right into that same wave of the Laravel ecosystem expanding into new territory.

Prerequisites

Before we start, here's what you need:

  • PHP 8.2+ (Laravel Herd is the easiest way if you don't have it)
  • Composer
  • Node.js and npm
  • A phone with the Jump app installed (free, available on Android via Google Play and iOS via TestFlight)
  • Your phone and computer on the same Wi-Fi network

That's it for getting started. You don't need Xcode or Android Studio right away. Jump lets you test your app on a real device without compiling anything. You'll only need those tools when you're ready to create production builds for the app stores.

Step 1: Create a Fresh Laravel Project

Start with a new Laravel app. NativePHP works with existing apps too, but a fresh one keeps things clean for this tutorial.

laravel new quick-notes
cd quick-notes

Choose the Livewire starter kit when prompted (or go with a blank install and add Livewire manually). We'll use Livewire for our UI since it keeps everything in PHP and plays nicely with NativePHP.

Step 2: Install NativePHP for Mobile

This is where it gets real. Three commands:

composer require nativephp/mobile

php artisan native:install

php artisan native:run

The native:install command scaffolds everything NativePHP needs. It creates the native project files, sets up the configuration, and installs a handy native script wrapper you can use going forward.

After installation, you'll see a config/nativephp.php file. Open it and make sure your app identifier uses only lowercase letters, numbers, and periods:

// config/nativephp.php
return [
    'app_id' => 'com.yourname.quicknotes',
    // ...
];

This identifier matters. Special characters like hyphens or underscores will break the build later. Trust me on this one.

Step 3: Install the Plugins We Need

Our notes app needs two native features: dialogs (for delete confirmation) and sharing. Both are free plugins.

composer require nativephp/mobile-dialog
composer require nativephp/mobile-share

After installing plugins, you need to register them so their native code gets compiled into your app. First, publish the NativeServiceProvider if you haven't already:

php artisan vendor:publish --tag=nativephp-provider

Then open app/Providers/NativeServiceProvider.php and add your plugins. Each plugin's documentation tells you which service provider class to register:

<?php

namespace App\Providers;

use Native\Mobile\NativeServiceProvider as BaseServiceProvider;

class NativeServiceProvider extends BaseServiceProvider
{
    /**
     * Register plugins for native compilation.
     */
    public function plugins(): array
    {
        return [
            \NativePHP\Dialog\DialogServiceProvider::class,
            \NativePHP\Share\ShareServiceProvider::class,
        ];
    }
}

Important: Always check each plugin's README or marketplace page for the exact service provider class name to register. You can verify your installed plugins with php artisan native:plugin:list.

This registration step is a security feature. It prevents random Composer dependencies from sneaking native code into your app without your knowledge.

Step 4: Set Up the Database

NativePHP uses SQLite automatically. You don't need to configure anything. When your app runs on a device, NativePHP creates the SQLite database in the app container, runs your migrations on startup, and handles everything for you.

So just create a standard Laravel migration:

php artisan make:model Note -m

Open the migration file:

public function up(): void
{
    Schema::create('notes', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('body');
        $table->timestamps();
    });
}

And set up the model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Note extends Model
{
    protected $fillable = ['title', 'body'];
}

Nothing special. That's the beauty of it. It's just Laravel.

Step 5: Build the UI with Livewire

Let's create two Livewire components: one for the notes list and one for creating/editing notes.

php artisan make:livewire NotesList
php artisan make:livewire NoteEditor

The Notes List Component

This is where NativePHP gets interesting. We're going to use the Dialog and Share facades to trigger real native functionality from PHP.

NativePHP's Dialog plugin uses an event-based pattern. You call Dialog::alert() to show a native dialog, give it an .id() so you can identify it later, and then listen for the ButtonPressed event in your Livewire component using the #[OnNative] attribute. The Share plugin is simpler: call Share::file() with a title, message, and an optional file path to open the native share sheet.

Here's how it all comes together:

<?php

namespace App\Livewire;

use App\Models\Note;
use Livewire\Component;
use Native\Mobile\Facades\Dialog;
use Native\Mobile\Facades\Share;
use Native\Mobile\Attributes\OnNative;
use Native\Mobile\Events\Alert\ButtonPressed;

class NotesList extends Component
{
    public function confirmDelete(int $noteId): void
    {
        Dialog::alert(
            'Delete Note',
            'Are you sure you want to delete this note?',
            ['Cancel', 'Delete']
        )
            ->id("delete-note-{$noteId}")
            ->show();
    }

    #[OnNative(ButtonPressed::class)]
    public function handleDialogButton($index, $label, $id = null): void
    {
        if ($label === 'Delete' && str_starts_with($id ?? '', 'delete-note-')) {
            $noteId = (int) str_replace('delete-note-', '', $id);
            Note::destroy($noteId);
        }
    }

    public function shareNote(int $noteId): void
    {
        $note = Note::findOrFail($noteId);

        Share::file(
            $note->title,
            $note->body,
            ''
        );
    }

    public function render()
    {
        return view('livewire.notes-list', [
            'notes' => Note::latest()->get(),
        ]);
    }
}

Look at that confirmDelete method. Dialog::alert() triggers a real native OS dialog (not a JavaScript confirm()). The .id() method tags the dialog so when the user taps a button, the ButtonPressed event carries that ID back to your Livewire component. You match on the button label and the ID to figure out which note to delete.

And Share::file() opens the native share sheet so your users can send notes to WhatsApp, email, or whatever apps they have installed. Pass an empty string as the third argument when you're sharing text without a file attachment. This is real native functionality, written in PHP.

The Notes List Blade Template

<div class="min-h-screen bg-gray-50 p-4">
    <div class="flex items-center justify-between mb-6">
        <h1 class="text-2xl font-bold text-gray-900">My Notes</h1>
        <a href="/notes/create"
           class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium">
            + New Note
        </a>
    </div>

    @forelse($notes as $note)
        <div class="bg-white rounded-xl shadow-sm p-4 mb-3">
            <a href="/notes/{{ $note->id }}/edit" class="block">
                <h3 class="font-semibold text-gray-900">{{ $note->title }}</h3>
                <p class="text-gray-500 text-sm mt-1 line-clamp-2">{{ $note->body }}</p>
                <span class="text-xs text-gray-400 mt-2 block">
                    {{ $note->updated_at->diffForHumans() }}
                </span>
            </a>
            <div class="flex gap-2 mt-3 pt-3 border-t border-gray-100">
                <button wire:click="shareNote({{ $note->id }})"
                        class="text-blue-600 text-sm font-medium">
                    Share
                </button>
                <button wire:click="confirmDelete({{ $note->id }})"
                        class="text-red-500 text-sm font-medium ml-auto">
                    Delete
                </button>
            </div>
        </div>
    @empty
        <div class="text-center py-12">
            <p class="text-gray-400 text-lg">No notes yet</p>
            <p class="text-gray-400 text-sm mt-1">Tap + to create your first note</p>
        </div>
    @endforelse
</div>

The Note Editor Component

<?php

namespace App\Livewire;

use App\Models\Note;
use Livewire\Component;

class NoteEditor extends Component
{
    public ?Note $note = null;
    public string $title = '';
    public string $body = '';

    public function mount(?int $id = null): void
    {
        if ($id) {
            $this->note = Note::findOrFail($id);
            $this->title = $this->note->title;
            $this->body = $this->note->body;
        }
    }

    public function save(): void
    {
        $validated = $this->validate([
            'title' => 'required|max:255',
            'body' => 'required',
        ]);

        if ($this->note) {
            $this->note->update($validated);
        } else {
            Note::create($validated);
        }

        $this->redirect('/');
    }

    public function render()
    {
        return view('livewire.note-editor');
    }
}
<div class="min-h-screen bg-gray-50 p-4">
    <div class="flex items-center justify-between mb-6">
        <a href="/" class="text-blue-600 font-medium">&larr; Back</a>
        <h1 class="text-lg font-bold text-gray-900">
            {{ $note ? 'Edit Note' : 'New Note' }}
        </h1>
        <div class="w-12"></div>
    </div>

    <form wire:submit="save" class="space-y-4">
        <div>
            <input type="text"
                   wire:model="title"
                   placeholder="Note title..."
                   class="w-full px-4 py-3 bg-white rounded-xl border border-gray-200
                          focus:border-blue-500 focus:ring-1 focus:ring-blue-500
                          text-gray-900 text-lg" />
            @error('title')
                <span class="text-red-500 text-sm mt-1">{{ $message }}</span>
            @enderror
        </div>

        <div>
            <textarea wire:model="body"
                      placeholder="Write your note..."
                      rows="12"
                      class="w-full px-4 py-3 bg-white rounded-xl border border-gray-200
                             focus:border-blue-500 focus:ring-1 focus:ring-blue-500
                             text-gray-900"></textarea>
            @error('body')
                <span class="text-red-500 text-sm mt-1">{{ $message }}</span>
            @enderror
        </div>

        <button type="submit"
                class="w-full bg-blue-600 text-white py-3 rounded-xl font-semibold text-lg">
            {{ $note ? 'Update Note' : 'Save Note' }}
        </button>
    </form>
</div>

Set Up the Routes

// routes/web.php
use App\Livewire\NotesList;
use App\Livewire\NoteEditor;

Route::get('/', NotesList::class);
Route::get('/notes/create', NoteEditor::class);
Route::get('/notes/{id}/edit', NoteEditor::class);

Standard Laravel routing. Nothing NativePHP-specific here.

Step 6: Configure Vite for NativePHP

One thing that trips people up. You need to add the NativePHP Vite plugin so your assets build correctly for mobile:

// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite';
import {
    nativephpMobile,
    nativephpHotFile
} from './vendor/nativephp/mobile/resources/js/vite-plugin.js';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
            hotFile: nativephpHotFile(),
        }),
        tailwindcss(),
        nativephpMobile(),
    ],
});

Then build your assets before running the app:

npm install
npm run build

If you want hot reloading during development (which is amazing on a simulator), you can run npm run dev in a separate terminal. Just make sure your phone is on the same Wi-Fi as your dev machine.

Step 7: Test with Jump

This is the fun part. Jump lets you run your app on a real phone without touching Xcode or Android Studio.

php artisan native:jump

This starts a local dev server and shows a QR code in your terminal. Open the Jump app on your phone, scan the code, and your Laravel app loads right on your device.

That's it. Your notes app is running natively on your phone.

You can create notes, edit them, delete them (with native confirmation dialogs), and share them using the native share sheet. The SQLite database lives on the device. It works offline. And you wrote exactly zero lines of Swift or Kotlin.

One bonus: Jump includes all first-party NativePHP plugins (even the premium ones), so you can experiment with Biometrics, Geolocation, and other paid features during development without buying anything.

Step 8: Going Beyond Jump (Production Builds)

Jump is perfect for development and testing. But when you're ready to ship to the App Store or Google Play, you'll need to compile actual builds.

For iOS, you need a Mac with Xcode:

php artisan native:run ios

For Android:

php artisan native:run android

NativePHP handles the compilation. You don't need to open Xcode or Android Studio directly (though you can if you want to). The framework takes care of bundling PHP, your Laravel code, and all the native plugins into a proper app.

For deployment to the stores, NativePHP provides the native:package command that handles signing, bundling, and even uploading. The process can get complex with certificates and provisioning profiles, but the official deployment docs walk through everything.

If you want to skip the deployment headache entirely, the NativePHP team offers Bifrost, a paid service that handles builds and distribution. Set it up once and push updates without managing certificates yourself. Not required, but worth knowing about.

Where Does NativePHP Fit? An Honest Take

Let me be real about where NativePHP v3 shines and where it doesn't.

It's great for:

  • Internal tools and admin apps that your team uses
  • MVPs where you need to validate an idea fast
  • Companion apps for existing Laravel web projects
  • CRUD-heavy apps (notes, task managers, inventory trackers)
  • Offline-first apps that sync with a Laravel API

It's not the best choice for:

  • Games or apps with complex animations
  • Apps that need pixel-perfect native UI controls throughout
  • Performance-critical apps processing heavy media or real-time data
  • Apps where platform-specific design patterns (Material Design, Human Interface Guidelines) are essential

Think of it this way. If you're a Laravel team and a client asks for a mobile app, NativePHP means you can say yes without hiring mobile developers or learning React Native. For MVPs and internal tools, that's huge. For a consumer app competing with Spotify or Instagram, you'd still want a dedicated native or cross-platform team.

The fact that it's free now makes it a no-brainer to try. Before v3, you had to weigh the license cost against the experiment. Now you can spin something up in an afternoon and see if it fits your use case.

If you're building AI-agent-friendly Laravel apps or working with the Laravel + Claude Code ecosystem, NativePHP opens up a new distribution channel for your work. Imagine building an AI-powered tool that runs entirely on a user's phone with no server costs. That's possible now.

Frequently Asked Questions

Do I need a Mac to build iOS apps with NativePHP?

Yes, for production iOS builds you need a Mac with Xcode installed. This is an Apple restriction, not a NativePHP one. But you can develop and test using Jump on any operating system, which covers most of your day-to-day workflow. You only need the Mac when you're ready to compile and submit to the App Store.

Can I use my existing Laravel app or do I need to start from scratch?

You can add NativePHP to an existing Laravel app. Just run composer require nativephp/mobile and php artisan native:install in your project. Keep in mind that NativePHP uses SQLite on the device, so your app needs to work with SQLite. If you're using MySQL-specific features in your migrations, you'll need to adjust those.

How does NativePHP compare to React Native or Flutter?

NativePHP lets you use your existing PHP and Laravel skills. React Native requires JavaScript/TypeScript knowledge and Flutter requires Dart. For a Laravel team, NativePHP means no new language to learn. But React Native and Flutter have larger ecosystems, more UI components, and better support for complex native UIs. Choose based on your team's skills and the complexity of your app.

Is NativePHP v3 production-ready?

Apps built with NativePHP are already live on both the App Store and Google Play. The framework is actively maintained with regular releases. For CRUD apps, internal tools, and MVPs, it's solid. For complex consumer apps, evaluate whether the available plugins cover your needs before committing.

What about app size? How big are NativePHP apps?

NativePHP apps include a bundled PHP runtime, so they're larger than minimal native apps. Expect around 15-30 MB depending on your plugins and assets. The v3 plugin system helps here because you only include what you actually use, which keeps the footprint smaller than v1 or v2.

Wrapping Up

NativePHP v3 changes the equation for Laravel developers. Mobile development used to mean learning a completely new stack. Now it means adding a Composer package and writing the same PHP you've been writing for years.

The Quick Notes app we built here is simple on purpose. But the same patterns (Eloquent models, Livewire components, native plugins) scale to much more complex apps. Add the Camera plugin to let users attach photos. Add the Network plugin to sync notes with a remote API. Add Biometrics to lock sensitive notes behind Face ID.

You already know how to build this stuff. NativePHP just gives you a new place to put it.

If you're ready to go deeper, the NativePHP docs are excellent and there's a free Laracasts course covering the basics. And if you're a founder looking to ship an MVP mobile app fast, I can help with that.


Got a Product Idea?

I build MVPs, web apps, and SaaS platforms in 7 days. Fixed price, real code, deployed and ready to use.

⚡ Currently available for 2-3 new projects

Hafiz Riaz

About Hafiz

Full Stack Developer from Italy. I help founders turn ideas into working products fast. 9+ years of experience building web apps, mobile apps, and SaaS platforms.

View My Work →

Get web development tips via email

Join 50+ developers • No spam • Unsubscribe anytime