Cache
NativeBlade auto-wires Laravel’s Cache::* facade to a SQLite-backed store that persists across app cold starts on the device. You don’t need to touch config/cache.php or .env, Cache::put, Cache::get, Cache::remember, Cache::lock, and the rest of the contract work out of the box.
Quick start
use Illuminate\Support\Facades\Cache;
// Store with TTL (seconds)
Cache::put('weather.lisbon', $payload, 3600);
// Read back
$payload = Cache::get('weather.lisbon');
// Memoize an expensive call (computes once, caches the result)
$weather = Cache::remember('weather.lisbon', 3600, function () {
return Http::get('https://api.weather.com/lisbon')->json();
});
// Idempotency marker
Cache::put('order:processed:' . $orderId, true, 300);
if (Cache::has('order:processed:' . $orderId)) {
return;
}
// Atomic lock (prevents two concurrent operations on the same key)
Cache::lock('sync.contacts', 30)->block(5, function () {
syncContactsToServer();
});
// Drop a single key, or wipe everything
Cache::forget('weather.lisbon');
Cache::flush();
Everything else in the Laravel cache documentation applies, increment(), decrement(), pull(), add(), forever(), rememberForever(). Tags are not supported (see “Limitations”).
How it works under the hood
When NativeBlade boots, NativeBladeServiceProvider::registerNativeCache() does three things:
- Registers a new cache store named
nativebladeconfigured with thedatabasedriver against thesqliteconnection. - Sets
config('cache.default') = 'nativeblade', so calls to theCachefacade go through this store by default. - Creates two tables on the
sqliteconnection if they don’t exist:nativeblade_cache, columnskey TEXT PRIMARY KEY,value TEXT,expiration INTEGERnativeblade_cache_locks, columnskey TEXT PRIMARY KEY,owner TEXT,expiration INTEGER
The sqlite connection is the same one that powers NativeBlade::setState and any Eloquent models you wire to it. The physical file lives at /app/database/database.sqlite inside the PHP-WASM virtual filesystem and is checkpointed to IndexedDB transparently, so cache survives cold starts without any work from your side.
State vs Cache: when to choose which
Both live in the same SQLite file. The contracts differ:
NativeBlade::setState |
Cache::* |
|
|---|---|---|
| Lifetime semantics | Definitive, exists until you forget() it |
Best-effort, can expire or be evicted |
| TTL support | No (lives until manually cleared) | Yes (per-key expiration) |
| Scoping | scope argument, bulk clear via flush($scope) |
Tags (limited on database driver) |
| Storage table | nativeblade_state |
nativeblade_cache |
| Use for | Identity, configuration, user preferences | Derived data, memoized computations, throttle counters |
Rule of thumb: if losing the value should be invisible to the user (you can just recompute), use Cache. If losing it would break the app (user gets logged out, locale resets), use setState.
// State, identity / config / definitive truth
NativeBlade::setState('auth.user', $user);
NativeBlade::setState('locale.current', 'pt_BR');
// Cache, derived / disposable / recomputable
Cache::remember('feed:trending', 600, fn () => Trending::query()->limit(20)->get());
Cache::put('throttle:login:' . $ip, $attempts + 1, 60);
Common patterns
Memoize remote API calls
public function fetchExchangeRates(): array
{
return Cache::remember('rates:USD', 3600, function () {
return Http::get('https://api.exchangerate.host/latest?base=USD')->json('rates');
});
}
Rate-limit on the client
public function attemptUnlock(string $pin): bool
{
$key = 'pin.attempts:' . auth()->id();
$attempts = Cache::increment($key);
Cache::put($key, $attempts, 300);
if ($attempts > 5) {
$this->message = __('Try again in 5 minutes.');
return false;
}
return $this->verifyPin($pin);
}
Idempotent push handlers
public function handle(PushPayload $payload): void
{
$key = 'push:handled:' . $payload->messageId;
if (Cache::has($key)) {
return;
}
Cache::put($key, true, 86400);
$this->processPayload($payload);
}
Cross-component locks
public function syncTrail(): void
{
Cache::lock('trail.sync', 30)->block(5, function () {
// Only one component can run this block at a time across the whole app
TrailSync::run();
});
}
Maintenance
Clearing the cache
php artisan cache:clear
Or programmatically:
Cache::flush();
Pruning expired entries
Laravel’s DatabaseStore does not auto-delete expired rows. They’re filtered out on read (returning null past expiration), but they accumulate in the table. To clean them up:
php artisan cache:prune-stale-tags
You can also wire this into the NativeBlade schedule:
// app/Console/Kernel.php or wherever you register schedules
Schedule::command('cache:prune-stale-tags')->daily();
In practice the cache table stays small on a device app, so pruning is rarely urgent.
Overriding the default
NativeBlade only sets cache.default = 'nativeblade' during its own boot(). If you have a reason to use a different store, override it from your AppServiceProvider (which boots after NativeBlade’s provider):
// app/Providers/AppServiceProvider.php
public function boot(): void
{
config(['cache.default' => 'array']); // for tests
}
Or pick a store per call without changing the default:
Cache::store('array')->put('ephemeral', $value, 60);
The nativeblade store remains registered either way, so Cache::store('nativeblade') always works.
Limitations
- Cache tags are not supported.
Cache::tags(['users'])->put(...)throwsBadMethodCallException: This cache store does not support tagging, because Laravel’sDatabaseStore(which thenativebladedriver delegates to) doesn’t implementTaggableStore. Tag-capable drivers in Laravel core areredis,memcached, anddynamodb, none of which fit a device-local app. Workaround: key your entries with a prefix (Cache::put("users:{$id}:profile", ...)) and use a key pattern for bulk invalidation. - Concurrent writes rely on SQLite’s file lock. In normal NativeBlade single-process operation this is fine; if you’re running scheduled jobs while the user navigates, lock contention is possible but rare.
- Large blobs (more than a few MB) are not a great fit. The cache row stores
valueas text and the whole SQLite file is checkpointed to IndexedDB after writes, bigger file means slower checkpoint. For binary blobs preferStorage::disk('nativeblade')(see Filesystem).