tmarois/filebase
tmarois/filebase is a lightweight flat-file database for Laravel and PHP. It stores records in JSON files and provides a simple, Eloquent-like API for creating, querying, and managing data without configuring a traditional database—ideal for small apps, prototypes, and local tools.
composer require tmarois/filebase
Three steps to first value:
storage/app/filebase. Create it and make it writable by the PHP user (chown -R www-data:www-data, mode 775). The package will fail silently-ish or throw on an unwritable path, so verify this first.// app/Providers/FilebaseServiceProvider.php
use Filebase\Filebase;
public function register(): void
{
$this->app->singleton(Filebase::class, fn () => new Filebase(
storage_path('app/filebase'),
['pretty' => true, 'lock' => true],
));
}
$db->set('site.theme', 'dark');
$db->get('site.theme'); // 'dark'
$db->exists('site.theme'); // true
$db->delete('site.theme');
Where to look first: the README's API surface (set, get, exists, delete, flush, dot-notation keys) and the constructor config array (pretty, lock). Then read the source once for the locking and read/write path — you need to know whether a write rewrites the whole store, because that dictates everything about how you should (and shouldn't) use it.
Wrap it behind an interface from day one. The whole value proposition of Filebase is that you can swap it for Redis or a real table later without touching call sites.
interface SettingsStore {
public function get(string $key, mixed $default = null): mixed;
public function set(string $key, mixed $value): void;
public function forget(string $key): void;
}
final class FilebaseSettingsStore implements SettingsStore { /* delegates to Filebase */ }
Bind the interface in the container. When the flat file outgrows itself, you write one new class and change one binding.
Use dot notation for documents, not for records. Nesting config-shaped data (mail.host, mail.port) is exactly the right granularity. Using it to model thousands of "rows" (users.1043.email) is exactly the wrong one — see Gotchas.
One instance per concern. Rather than one giant store, give each domain its own directory (storage/app/filebase/settings, /content, /feature-flags). Isolation limits blast radius, makes backups obvious, and keeps individual files small enough to inspect in an editor.
Cache reads aggressively. On a request that reads the same key three times, you're opening and decoding a file three times unless the library memoizes (don't assume it does). Wrap reads in Laravel's cache with explicit invalidation on set:
public function get(string $key, mixed $default = null): mixed
{
return Cache::rememberForever("fb:{$key}", fn () => $this->db->get($key) ?? $default);
}
public function set(string $key, mixed $value): void
{
$this->db->set($key, $value);
Cache::forget("fb:{$key}");
}
Seed and test with a throwaway directory. In tests, point the store at a sys_get_temp_dir() path or a per-test folder and tear it down in tearDown(). Never let the suite touch the real store — flat files don't roll back, and a stray flush() in a test is unrecoverable.
Commit the store when it's config. For site settings or feature flags that change rarely, checking the JSON into git gives you reviewable diffs, history, and rollback. Turn on pretty so those diffs are readable.
Backups are just cp. No dump tooling required — snapshot the directory before deploys that write to it, or version the files. That's a genuine advantage over a database you don't have ops access to.
'lock' => true and understand it's file locking for the write window, not a transactional guarantee. If your workload has concurrent writers, this is the wrong tool..bak copy or the git history above).DateTime round-trips as a string — cast explicitly on read. Enums and value objects need manual serialization.../ away from writing outside your store. Whitelist the key shape with a regex at the repository boundary, not inside Filebase..gitignore unless it's config-as-code. Mixing the two — tracked settings plus runtime state in the same directory — produces constant noisy merge conflicts. Separate directories, separate rules.cat the file, run it through json_decode, check json_last_error(). Turn on pretty while debugging; it costs nothing at this scale.How can I help you explore Laravel packages today?