Database
NativeBlade supports two database strategies that work together:
- SQLite (WASM), Local database inside the app. Offline, instant, persisted to IndexedDB. Used for state, migrations, and local data.
- Native Database, External MySQL, MariaDB, PostgreSQL, or SQLite via Rust bridge. Used for server-side data, shared databases, and enterprise apps.
SQLite (Default)
Works out of the box. No configuration needed.
// Eloquent works normally
$tasks = Task::where('done', false)->get();
Task::create(['title' => 'Buy milk']);
// State management
NativeBlade::setState('user', ['name' => 'John']);
$user = NativeBlade::getState('user');
Migrations run automatically on boot. See Lifecycle.
Native Database
Connect to a real database server using Eloquent, the Rust runtime executes queries via sqlx.
Setup
The native connection is added automatically by nativeblade:install. Configure your credentials in .env:
NB_DB_HOST=127.0.0.1
NB_DB_PORT=3306
NB_DB_DATABASE=myapp
NB_DB_USERNAME=root
NB_DB_PASSWORD=secret
The connection in config/database.php:
'native' => [
'driver' => 'nativeblade-db',
'native_driver' => 'mysql', // mysql, mariadb, pgsql, sqlite
'host' => env('NB_DB_HOST', '127.0.0.1'),
'port' => env('NB_DB_PORT', '3306'),
'database' => env('NB_DB_DATABASE', 'myapp'),
'username' => env('NB_DB_USERNAME', 'root'),
'password' => env('NB_DB_PASSWORD', ''),
'prefix' => '',
],
Usage
Set the connection on your Model:
class Setting extends Model
{
protected $connection = 'native';
protected $fillable = ['key', 'value'];
}
Then use Eloquent normally:
// Read
$settings = Setting::all();
$value = Setting::where('key', 'theme')->value('value');
// Write
Setting::create(['key' => 'language', 'value' => 'en']);
Setting::updateOrCreate(['key' => 'theme'], ['value' => 'dark']);
// Delete
Setting::where('key', 'old_setting')->delete();
// Relationships
$user = User::with('posts.comments')->find(1);
Supported Drivers
native_driver |
Database | Port |
|---|---|---|
mysql |
MySQL 5.7+ | 3306 |
mariadb |
MariaDB 10.5+ | 3306 |
pgsql |
PostgreSQL 12+ | 5432 |
sqlite |
SQLite (native, not WASM) | , |
PostgreSQL Example
// config/database.php
'native' => [
'driver' => 'nativeblade-db',
'native_driver' => 'pgsql',
'host' => env('NB_DB_HOST', '127.0.0.1'),
'port' => env('NB_DB_PORT', '5432'),
'database' => env('NB_DB_DATABASE', 'myapp'),
'username' => env('NB_DB_USERNAME', 'postgres'),
'password' => env('NB_DB_PASSWORD', ''),
],
Native SQLite Example
For a real SQLite file on the filesystem (not WASM):
'native' => [
'driver' => 'nativeblade-db',
'native_driver' => 'sqlite',
'database' => '/path/to/database.sqlite',
],
How It Works
The native database runs in the shell, and PHP waits for each query:
PHP: Setting::where('key', 'theme')->get()
→ NativeConnection sends SQL + bindings to the shell and PHP pauses
→ JS invokes Rust, which executes via sqlx against the real database
→ PHP resumes with the rows and Eloquent hydrates them
php-wasm runs with JSPI (or Asyncify where the WebView lacks it), so a query
is a pause inside the request, not a re-run of it. A failed query throws a
QueryException, as a PDO driver would.
Performance Tips
Use wire:init to avoid blocking navigation while queries execute:
class Settings extends Component
{
public bool $loading = true;
public array $settings = [];
public function loadData()
{
$this->settings = Setting::all()->toArray();
$this->loading = false;
}
}
<div wire:init="loadData">
@if($loading)
<x-nativeblade-skeleton class="h-12 w-full rounded-xl" />
<x-nativeblade-skeleton class="h-12 w-full rounded-xl" />
@else
{{-- Real content --}}
@endif
</div>
Use eager loading to reduce queries:
// Bad: N+1 queries (N+2 PHP executions)
$users = User::all();
foreach ($users as $user) {
$user->posts; // extra query per user
}
// Good: 2 queries (3 PHP executions)
$users = User::with('posts')->get();
HasNativeDatabase Trait
The updateOrCreate and firstOrCreate Eloquent methods do multiple queries internally (SELECT + INSERT/UPDATE), which can cause issues with the bridge cache. Use the HasNativeDatabase trait on your Models to optimize these automatically:
use NativeBlade\Database\HasNativeDatabase;
class Setting extends Model
{
use HasNativeDatabase;
protected $connection = 'native';
protected $fillable = ['key', 'value'];
}
// Now works correctly, uses upsert under the hood
Setting::updateOrCreate(['key' => 'theme'], ['value' => 'dark']);
Setting::firstOrCreate(['key' => 'language'], ['value' => 'en']);
The trait converts updateOrCreate to upsert (single query) when using the native connection. Always add this trait to Models that use protected $connection = 'native'.
Multiple Queries
Any number of queries, in any order, including relationships, transactions
and branches that decide at runtime which query to run. Each one is a round
trip to the device’s database, so the usual Laravel advice applies: eager-load
relations and avoid N+1 patterns, because every query is paid in latency.
Store timestamps in UTC, not a per-request timezone
An on-device workflow is spread across several requests. Anything that
mutates global framework config per request then becomes inconsistent between
those requests, and a value written in one request can be read or compared in
another under a different setting.
Timezone is the classic trap. Setting config('app.timezone') per request (for
example from a cookie) changes how Eloquent writes every timestamp, so a row
written in one request and pruned in another can be compared across a several
hour offset and wrongly treated as stale. The failure is intermittent, since it
depends on which requests carried the setting.
Keep storage canonical and apply presentation at the edge:
- Store timestamps in UTC. Do not drive
config('app.timezone')from device
state. - Keep the device’s timezone in explicit state (
NativeBlade::setState, or your
own value object) and read it only where you format for display or decide what
counts as “today”. config('app.locale')is the same trap. NativeBlade sets it once per request
from the persisted language (see Multi-language); read
the device’s preference from state rather than mutating global config mid-flow.
Transactions
Transactions work within the bridge pattern:
DB::connection('native')->transaction(function () {
Setting::create(['key' => 'a', 'value' => '1']);
Setting::create(['key' => 'b', 'value' => '2']);
});
The Rust side maintains connection pools, so the transaction executes on the same connection.
SQLite vs Native, When to Use What
| Use Case | SQLite (WASM) | Native Database |
|---|---|---|
| Auth state, preferences | ✓ | |
| Offline-first data | ✓ | |
| App cache | ✓ | |
| Shared/team data | ✓ | |
| Server-side database | ✓ | |
| Large datasets | ✓ | |
| Full-text search | ✓ | |
| Complex queries/joins | ✓ | ✓ |
Important Notes
- The default
sqliteconnection always uses the local WASM SQLite,NativeBlade::setState()and migrations always use this - The
nativeconnection uses separate env vars (NB_DB_*) to avoid conflicts - The database server must be accessible from the user’s machine (localhost, LAN, or internet)
- Each query goes through the bridge, use eager loading and
wire:initfor best performance