AYOUB.*
← ALL ARTICLESREPOS ↗
Case Study· Updated ·11 min read

PhoenixDB: An ACID Embedded Database for Flutter and Dart

A case study of PhoenixDB's Rust core, Dart FFI boundary, and when an embedded database with vector search actually makes sense for mobile and desktop apps.

[TL;DR // EXECUTIVE SUMMARY]

  • PhoenixDB is an ACID embedded database written in Rust with a Dart/Flutter FFI binding.
  • It offers B+Tree storage, MVCC transactions, WAL recovery, optional SQL, and HNSW vector search in one package.
  • The async API runs on a worker isolate so the UI never blocks, and Android ships prebuilt binaries.
  • The main trade-off is Rust toolchain complexity for contributors and a smaller ecosystem than SQLite.

I built PhoenixDB because I needed a local store that could do two things SQLite wasn't built for: run vector similarity search on device and expose a typed API to Flutter without blocking the UI. This case study covers how it's put together and when it's the right choice.

PhoenixDB is an ACID-compliant embedded database engine written in Rust and exposed to Dart and Flutter through a zero-overhead dart:ffi layer. You can use it as a key/value store, as typed preferences, for HNSW vector similarity search, or through a small optional SQL front end. It ships B+Tree storage, MVCC transactions, a write-ahead log, and runtime-dispatched SIMD kernels.

The Rust core owns the storage engine. The Dart side owns the API surface. The boundary between them is pure C ABI generated by cbindgen, which means no bridging runtime, no extra allocations, and no surprises when a panic crosses the FFI.

What is an embedded database with FFI bindings?

An embedded database lives inside your process. You don't run a server, you don't open a socket, you don't negotiate a connection string. The storage engine is a library your app links to, and all reads and writes stay on device.

FFI bindings are the glue between two language runtimes. PhoenixDB's Rust core compiles to a C ABI, and Dart's dart:ffi calls it directly. That's different from a platform channel or a REST wrapper. The call crosses the boundary in one direction, returns a handle or a value, and the Dart garbage collector knows how to clean it up through NativeFinalizer.

The advantage is speed and control. The disadvantage is that every type crossing the boundary must be pinned, sized, and freed explicitly. Get that wrong and you leak memory or corrupt a transaction. PhoenixDB's handle tagging and constant-time compares are guardrails, not guarantees.

How does PhoenixDB store and recover data?

PhoenixDB stores data in 4 KiB slotted pages arranged in B+Tree indexes. Each page carries a CRC32 checksum, so corruption returns an error instead of silently serving bad bytes. The WAL records mutations before they commit, and fsync at commit time means a power loss recovers to the last durable state, not the last in-memory guess.

MVCC gives snapshot isolation. Multiple readers can walk the same tree without blocking each other, and a single writer commits in sequence. That model fits mobile and desktop apps where background sync, local caching, and offline writes happen at the same time.

Transactions are explicit. You call Begin, insert or delete inside a block, then Commit or Rollback with a transaction id. The API is small enough to fit in your head and strict enough that a misuse shows up as an error, not silent corruption.

How does PhoenixDB compare to SQLite and other local stores?

The comparison most developers care about is SQLite, because SQLite is the default answer for embedded storage. The table below is intentionally specific: each column states what PhoenixDB can do and what it can't, because the absence is what decides whether you should choose it.

DimensionPhoenixDBSQLite
Storage engineB+Tree with CRC32 per page and WAL recovery. Purpose-built for embedded and mobile use.B+Tree with WAL and extensive battle testing across every platform.
InterfaceRust core with Dart/Flutter FFI. Async API on a worker isolate keeps the UI thread free.C library bound directly to Dart through package:sqlite3 or similar.
Query surfaceOptional SQL front end, plus KV, typed preferences, and HNSW vector search.Mature SQL with virtual tables, FTS, JSON extensions, and a huge ecosystem.
Vector searchNative HNSW with cosine, L2, and dot product metrics. Runs on device without a server.No native vector index; you need an extension or external service.
Flutter storyPrebuilt Android binaries, no Rust toolchain needed on the developer machine.Works on Flutter, but the native build path is more involved.
EcosystemSmall, purpose-built, opinionated. The API surface is narrow on purpose.Ubiquitous, decades of tooling, admin interfaces, and production evidence.

If your app needs vector search, typed local preferences, or a Rust-core data layer with Flutter bindings, PhoenixDB covers that gap. If you need a general-purpose SQL database with a massive ecosystem, SQLite still wins.

What are the different operating modes?

PhoenixDB ships several modes through feature flags, and they compose differently depending on what you need. The table below compares the modes by what they give you and what they require.

ModeWhat it gives youWhat it costs
KV storeRaw key/value inserts, gets, and deletes with ACID guarantees. Fastest path with the smallest binary.No SQL, no vector index, no JSON helpers.
Typed preferencesPhoenixPrefs wraps the same store with typed getters and setters. Good for settings, tokens, and small user state.Still single-process, still local.
SQLCREATE, INSERT, SELECT, UPDATE, DELETE through a small SQL front end. Lets you migrate schema-driven apps onto PhoenixDB.Compiles extra Rust code; larger binary and longer build time.
VectorHNSW index for cosine, L2, and dot-product similarity. Useful for on-device semantic search and retrieval.Memory-heavy at high dimensions; needs dimension planning.

Most Flutter apps start in KV or preferences mode and graduate to SQL or vector once the schema stabilizes. The feature flags let you opt in without pulling in code you don't use.

How does the async API keep the UI responsive?

The synchronous API opens the database and runs calls on the calling isolate. That's fine for scripts, backend services, and tests. In Flutter, blocking the UI thread for a write or a vector search is a frame drop users notice.

AsyncPhoenixDB runs every call on a dedicated worker isolate. You send a message, the worker executes the Rust FFI call, and the result comes back as a future. The UI thread stays free to render frames, animate transitions, and respond to touches.

The worker also isolates panics. If the Rust core panics inside the FFI boundary, the worker dies and the Dart side gets an error state instead of crashing the app. That containment matters on mobile, where a crash in the background is a review kill.

What security properties does PhoenixDB provide?

PhoenixDB ships with FFI guardrails, handle tagging, constant-time compares, and panic containment. Handle tagging means every native pointer is paired with metadata so the runtime can detect use-after-free and double-close patterns instead of segfaulting silently.

CRC32 checksums run on every page read and write, so bit rot or tampering shows up as an Err. The WAL plus fsync model means a compromised process can't hide an incomplete commit — the recovery path replays or aborts, never invents data.

The optional encryption feature flag encrypts data at rest within the store. That's useful when the device itself isn't fully trusted, or when regulatory requirements demand that offline caches stay encrypted even if the filesystem is accessible.

Who should use PhoenixDB?

PhoenixDB is built for Flutter and Dart developers who need an embedded data layer that goes beyond key/value storage without reaching for a server. Teams shipping offline-first apps with local semantic search, typed preferences, or ACID transaction needs on mobile and desktop are the natural fit.

It's also a strong choice if your team already knows Rust and wants the storage engine to be auditable. The BSD-3-Clause license lets you read every line, and the Rust core is small enough to review without a dedicated database team.

If your app needs a general-purpose SQL database, complex migrations, or a large extension ecosystem, SQLite or a managed local database may be the better starting point. PhoenixDB trades ecosystem breadth for a tighter, more opinionated API.

FAQ

What is PhoenixDB?
PhoenixDB is an ACID-compliant embedded database written in Rust and exposed to Dart and Flutter through a zero-overhead dart:ffi binding.

Does PhoenixDB support SQL?
Yes. It offers an optional SQL front end with CREATE, INSERT, SELECT, UPDATE, and DELETE, enabled through feature flags.

What is HNSW vector search?
HNSW is a graph-based index for approximate nearest neighbor search. PhoenixDB supports cosine, L2, and dot-product metrics on device.

Is PhoenixDB safe for Flutter apps?
The async API runs on a worker isolate so the UI never blocks, and panic containment prevents Rust panics from crashing the app.

Does PhoenixDB need a Rust toolchain?
No. Flutter apps consume prebuilt binaries on Android, and the pub.dev package handles the native build.

How does PhoenixDB handle corruption?
Every page carries a CRC32 checksum. Corruption returns an error instead of serving bad data, and the WAL replays or aborts on recovery.

Can I use PhoenixDB for typed preferences?
Yes. PhoenixPrefs wraps the same store with typed getters and setters for small app state and settings.

What platforms does PhoenixDB support?
Dart VM, browser, and Flutter on mobile and desktop. The pure C ABI makes portability straightforward.

Is PhoenixDB open source?
Yes. BSD-3-Clause, published at github.com/ayoubzulfiqar/phoenixdb with a verified pub.dev publisher.

What are the limits?
Max key is 1 MiB, max value is 10 MiB, max vector dimensions are 65,536, and max k per search is 4,096.

Related Articles

For the identity layer behind secure local storage, see machine identity security at scale — local secrets still need identity boundaries.

For zero-trust principles on device, see zero trust for AI workloads — the same verification mindset applies to local data pipelines.

For API exposure after local storage, see API security for AI agents — a locked local store still leaks through an open endpoint.