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 uses a bridge pattern:
PHP: Setting::where('key', 'theme')->get()
→ NativeConnection intercepts the query
→ Serializes SQL + bindings → writes to temp file → exits
→ JS detects pending query → invokes Rust
→ Rust executes via sqlx against the real database
→ Caches result → PHP re-executes
→ NativeConnection finds cached result → returns to Eloquent
Each query triggers one re-execution of PHP. For a page with N queries, there are N+1 PHP executions.
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
Multiple queries work correctly as long as they run in the same order on every re-execution. Standard Eloquent code is deterministic and works fine:
// OK, queries run in the same order every time
$users = User::all(); // queryIndex 0
$posts = Post::latest()->take(10)->get(); // queryIndex 1
$stats = Task::where('done', true)->count(); // queryIndex 2
// 3 queries = 4 PHP executions
Relationships work:
// OK, deterministic queries
$user = User::find(1); // queryIndex 0
$posts = $user->posts()->get(); // queryIndex 1
// 2 queries = 3 PHP executions
Avoid non-deterministic logic between queries:
// AVOID, different code paths between re-executions
if (rand(0, 1)) {
$a = User::find(1); // queryIndex 0
} else {
$b = Post::find(1); // queryIndex 0, different SQL, cache mismatch!
}
Store timestamps in UTC, not a per-request timezone
An on-device workflow is spread across several requests: each bridge call is its
own re-execution, and multi-step work is deliberately sliced. 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