Auto-Update
NativeBlade has two complementary update mechanisms:
| Shell Update | Bundle Push | |
|---|---|---|
| What updates | The native binary (Rust, plugins, Tauri shell) | Just the Laravel bundle (PHP, Blade, Livewire, CSS, JS) |
| Size | 50–200MB | 5–15MB |
| Desktop | Tauri updater downloads + restarts | Downloaded on boot (splash held), applied same session |
| Mobile | Modal → redirect to store | Downloaded on boot (splash held), applied same session |
| Store review | Required (mobile) | Not required |
| Frequency | Rare (when plugins/Rust change) | Frequent (any Laravel-side fix) |
Most updates only need bundle push. Shell update kicks in when you change the plugins([...]) declaration, native plugin code, or anything else that changes the binary.
Shell Update, How It Works
| Platform | Behavior |
|---|---|
| Desktop | Tauri updater downloads and installs automatically |
| Mobile | Shows “Update Available” modal → redirects to store |
Configuration
Desktop
NativeBladeConfig::desktop(function (DesktopConfig $config) {
$config->updateUrl('https://releases.myapp.com/version.json');
});
The Tauri updater checks the endpoint on boot. If a new version is found, it downloads and prompts the user to restart.
Android
NativeBladeConfig::android(function (AndroidConfig $config) {
$config->updateUrl('https://releases.myapp.com/version.json')
->storeUrl('https://play.google.com/store/apps/details?id=com.myapp.app');
});
iOS
NativeBladeConfig::ios(function (IosConfig $config) {
$config->updateUrl('https://releases.myapp.com/version.json')
->storeUrl('https://apps.apple.com/app/idXXXXXXXXX');
});
Version Endpoint
Host a JSON file at your updateUrl:
{
"version": "1.1.0",
"notes": "Bug fixes and performance improvements",
"forceUpdate": false,
"platforms": {
"windows-x86_64": {
"url": "https://releases.myapp.com/app-1.1.0.msi",
"signature": "..."
},
"darwin-x86_64": {
"url": "https://releases.myapp.com/app-1.1.0.dmg",
"signature": "..."
},
"linux-x86_64": {
"url": "https://releases.myapp.com/app-1.1.0.AppImage",
"signature": "..."
}
}
}
The version, notes, and forceUpdate fields are used by the mobile update modal. The platforms object is used by the Tauri desktop updater.
Mobile Update Modal
On mobile, 3 seconds after boot, the app fetches the version endpoint. If a newer version exists:
- Shows a modal with “Update Available” and the version number
- Shows release notes if provided
- “Update Now” button opens the store URL
- “Later” button dismisses (unless
forceUpdate: true)
With forceUpdate: true, the “Later” button is hidden and the user must update to continue.
Where to Host
- GitHub Releases, upload the JSON as a release asset
- S3 / Cloudflare R2, static file hosting
- Your own server, any URL that returns the JSON
Desktop Signing
The Tauri updater requires signature verification. Generate a key pair:
npx tauri signer generate -w ~/.tauri/myapp.key
This creates a private key (for signing builds) and a public key (for the app to verify updates). Add the public key to your config:
$config->updateUrl('https://releases.myapp.com/version.json');
The public key is automatically included in tauri.conf.json by nativeblade:config.
Sign your build artifacts with the private key before uploading:
npx tauri signer sign -k ~/.tauri/myapp.key build/desktop/1.1.0.msi
This generates a .sig file, include the signature content in the platforms.*.signature field of your version JSON.
Bundle Push
Push Laravel updates to all installed apps without rebuilding the native shell or going through the App Store / Play Store. The shell stays the same; only laravel-bundle.json.gz is replaced.
Configuration
NativeBladeConfig::bundlePush(
url: 'https://releases.myapp.com/version.json',
autoApply: true,
);
Run php artisan nativeblade:config once to publish the runtime config to public/nativeblade-config.json. From there, every app boot checks the URL and, when a strictly newer bundle is available, holds the splash while it downloads (showing progress) and applies it in the same session, no reload needed. A failed or absent update never blocks boot: the app falls back to the bundle already on disk and continues.
Manifest format
The same version.json your Tauri shell update uses, plus a bundle block:
{
"version": "1.1.0",
"platforms": { "...": "..." },
"bundle": {
"version": "1.0.5",
"url": "https://releases.myapp.com/laravel-bundle-1.0.5.json.gz",
"minShellVersion": "1.0.0"
}
}
| Field | Required | Description |
|---|---|---|
bundle.version |
yes | Semver. Downloaded only when strictly higher than the locally installed bundle version (an equal or lower version is ignored, so there is no accidental downgrade). |
bundle.url |
yes | URL to the new laravel-bundle.json.gz. |
bundle.minShellVersion |
no | Skip the bundle if the shell is older. Useful when the bundle calls into a plugin you only added in a newer shell. |
Release channels
By default the manifest has a single bundle entry, which is the stable channel. To ship pre-release bundles to testers without touching stable, bake a channel into the build and add a channels map to the manifest:
NativeBladeConfig::bundlePush(
url: 'https://releases.myapp.com/version.json',
channel: 'beta',
);
{
"bundle": { "version": "1.0.1", "url": ".../laravel-bundle-1.0.1.json.gz", "minShellVersion": "1.0.5" },
"channels": {
"beta": { "version": "1.1.0-beta.2", "url": ".../laravel-bundle-1.1.0-beta.2.json.gz", "minShellVersion": "1.0.5" }
}
}
A build configured with channel: 'beta' reads channels.beta. Every other build, and any older shell, reads bundle exactly as before, so existing apps are unaffected. The channel is build-time only: distribute the beta build through the store’s test track (TestFlight, Play testing tracks), and the channel is just metadata matching that build to its bundles.
Publish a channel bundle with --channel:
php artisan nativeblade:bundle --tag=1.1.0-beta.2 --channel=beta
The command prints the channels.{channel} block for you to merge into the existing version.json next to bundle.
Notes:
- Versions are tracked per channel, so promoting a beta build to stable through the store still picks up stable bundles correctly.
- A channel build with no matching
channelsentry stays on its current bundle rather than dropping to stable.
Storage
Downloaded bundles are stored in IndexedDB (works on web, Tauri desktop, Android, iOS, no platform-specific code). On boot, the app loads from cache if present, otherwise falls back to the bundle that shipped in the binary.
Rollback
If a downloaded bundle is corrupt or fails to decompress, the cache is cleared automatically and the app falls back to the binary’s bundled version on the next boot.
What works with bundle push
- Laravel routes, controllers, models, migrations, service providers
- Blade templates, Livewire components
- Public CSS/JS assets (Tailwind, Vite output)
- Composer dependencies (anything in
vendor/)
What does NOT work with bundle push
- Adding/removing plugins (
Plugin::CAMERA, etc.), changes Cargo features and native permissions - Changing native plugin code (push, media, custom Tauri plugins)
- Changing iOS/Android
permissions()config, affects Info.plist / AndroidManifest
For any of these, ship a shell update via the store (or notarized desktop updater).
How to publish a bundle
For bundle push you don’t need to rebuild the native shell. Use the dedicated bundle command:
# 1. Build only the Laravel bundle (fast, no Tauri build)
php artisan nativeblade:bundle --tag=1.0.5
# Output:
# public/laravel-bundle.json.gz (canonical, always overwritten)
# public/laravel-bundle-1.0.5.json.gz (versioned copy)
#
# 2. Upload public/laravel-bundle-1.0.5.json.gz to your CDN
#
# 3. Update version.json on your server:
{
"bundle": {
"version": "1.0.5",
"url": "https://releases.myapp.com/laravel-bundle-1.0.5.json.gz"
}
}
nativeblade:bundle skips the native build entirely, it only runs composer install --no-dev + the bundle script. Typical run is 10-30 seconds vs minutes for a full nativeblade:build.
Recommendations
- Keep
minShellVersionaccurate, bumping it when you add a plugin saves users from a broken app - Use a CDN with proper cache headers (e.g. CloudFront, Cloudflare R2)
- Version your bundle URLs (
bundle-1.0.5.json.gz) so old shells keep working with old bundles
User-driven update controls
By default the bundle check runs on boot and downloads a newer bundle while the splash is held. If you also want to expose the flow as a UI button (“Check for updates”, “Update now”) from inside the running app, wire up the dedicated actions on the facade.
NativeBlade::checkUpdate()
Asks the JS bridge to probe the manifest and report back. Does NOT download. Result arrives via the nb:update-check Livewire event:
use Livewire\Attributes\On;
use NativeBlade\Facades\NativeBlade;
class Settings extends Component
{
public ?array $update = null;
public function checkForUpdates()
{
return NativeBlade::checkUpdate()->toResponse();
}
#[On('nb:update-check')]
public function onUpdateCheck($available, $currentVersion = null, $nextVersion = null, $reason = null, $error = null)
{
$this->update = compact('available', 'currentVersion', 'nextVersion', 'reason', 'error');
}
}
$reason is one of:
| Value | Meaning |
|---|---|
| (absent) | An update is available; check $nextVersion |
up-to-date |
No newer bundle on the server |
not-configured |
bundlePush(url: ...) was never called |
fetch-failed |
Network error reaching the manifest |
invalid-manifest |
Manifest JSON missing bundle.version or bundle.url |
shell-too-old |
Bundle requires a newer shell version |
NativeBlade::forceUpdate()
Triggers the full download + cache step. The new bundle is stored in IndexedDB and applies on the next app launch (it does NOT swap the running bundle). Result via nb:update-applied:
public function applyUpdate()
{
return NativeBlade::forceUpdate()->toResponse();
}
#[On('nb:update-applied')]
public function onUpdateApplied($applied, $version = null, $reason = null, $error = null)
{
if ($applied) {
$this->message = "Update ready! Restart the app to apply v{$version}.";
} else {
$this->message = "Update failed: " . ($reason ?? $error ?? 'unknown error');
}
}
Showing the current version
NativeBlade::version() and NativeBlade::buildNumber() read straight from your AppServiceProvider’s DesktopConfig::version/AndroidConfig::version/IosConfig::version based on the running platform. No async, no event, synchronous accessors.
<p class="text-xs text-zinc-400">
Version {{ NativeBlade::version() }} ({{ NativeBlade::buildNumber() }})
</p>
Returns 'dev' / 0 when running in web dev mode without a declared version.
Putting it together
<div class="space-y-3">
<p>Current: v{{ NativeBlade::version() }} ({{ NativeBlade::buildNumber() }})</p>
<button wire:click="checkForUpdates">Check for updates</button>
@if($update?['available'])
<p>New version: v{{ $update['nextVersion'] }}</p>
<button wire:click="applyUpdate">Download now</button>
@elseif($update)
<p>{{ $update['reason'] === 'up-to-date' ? 'You are on the latest version.' : ($update['error'] ?? $update['reason']) }}</p>
@endif
</div>