Zova: SQLite-backed embedded storage written in Zig
Zova started from a need in one of my own projects.
I have been building a P2P chat application. Like many local applications, it needed to store messages, metadata, and files. The obvious first design was SQLite for records and a folder on disk for attachments.
That works at first, but I did not like where it was heading long-term.
SQLite would own the structured data. The filesystem would own the bytes. The application would own the convention between them. Then I would need cleanup logic, integrity checks, backup behavior, file references, migration rules, and eventually maybe object storage.
Setting up S3 or an S3-compatible service just for a local chat app also felt wrong. I wanted something embedded and local, not another service.
So I started thinking:
What if SQLite stayed the relational core, but the storage model around it became richer?
That became Zova.
The first idea was objects: content-addressed bytes stored alongside normal SQLite records. Messages and metadata could stay in SQL, while attachments could become first-class objects instead of loose files managed by application conventions.
I chose Zig because working with the SQLite amalgamation felt smooth, and Zig gave me a good low-level foundation for building features around it without hiding what SQLite is doing.
One thing that surprised me was Zig’s C generation path. Being able to keep Zig as the source of truth while producing a generated C source snapshot for package builds helped a lot. It made the project easier to distribute outside the Zig ecosystem without forcing every downstream user to install Zig.
Vectors came next because local applications increasingly need semantic search. For a chat application, that can mean searching messages by meaning, finding related conversations, or attaching embeddings to chunks of text. I did not want vectors to be an external sidecar database. If records and objects were local, vectors should be local too.
Graphs were the missing piece after that.
Once an application has records, objects, and vectors, it still needs to describe how things are connected: a message has an attachment, a message replies to another message, a chunk supports a fact, an entity is mentioned in a document, a vector represents a specific record or chunk.
You can model some of this with foreign keys, but foreign keys are not a general relationship traversal layer. I wanted explicit app-defined nodes and edges that could connect SQL records, objects, chunks, vectors, entities, facts, concepts, and external references without forcing all metadata out of SQLite.
That is why graphs became first-class in Zova too.
Today, Zova has four first-class storage shapes:
- records as normal SQLite tables
- objects as content-addressed bytes
- vectors as named collections with exact search
- graphs as explicit relationships between app-defined nodes
The design boundary is important to me: Zova does not try to replace SQLite or hide it. SQL stays SQLite SQL. Locking stays SQLite locking. Application metadata belongs in normal SQL tables. Zova-owned private tables handle objects, chunks, vector rows, graph topology, extension metadata, and internal state.
After that, I wanted Zova to have its own extension path.
I do not pretend to have the final answer for every vector, graph, search, or indexing use case. Some things should not be forced into the core just because they are useful. Extensions give Zova room to grow without making the base database heavier or more opinionated than it should be.
The first bundled extension is trgm, a fuzzy lookup extension for typo-tolerant target search.
Another design problem I spent time on was distribution.
Zova is written in Zig, but I did not want every downstream user to install Zig just to use a package. So the project now ships a generated C source snapshot for source-based package builds. Rust users can install from crates io with Rust and a normal C compiler/linker, while Zig remains the development language for the project itself.
Python wheels are also published for supported platforms, and GitHub releases include CLI and C ABI artifacts.
I would especially appreciate feedback on:
- Zig API shape
- Extension mechanism
- Generated-C distribution approach
- C ABI boundary
- Nice-to-have features or design gaps you notice
- whether the SQLite/Zova boundary feels clear
Zova is still pre-1.0, but the main storage model is now in place. I am currently migrating my P2P chat application to Zova as the first serious dogfooding target.
Supported Zig versions
Zova currently targets Zig 0.16.0 or newer.
AI / LLM usage disclosure
I use AI tools while working on Zova, mainly for review, documentation help, debugging ideas, and implementation support.
I make the architectural decisions, review the code, run the tests, manage the release pipeline, and maintain the project myself. The repository includes tests, CI/release workflows, a native Zig implementation, C ABI, Rust/Go/Python bindings, generated-C packaging, PyPI wheels, crates io packages, GitHub release artifacts, diagnostics, backup/restore/salvage tooling, explicit project boundaries, and a real application migration target.