I Built a TypeScript Object-Graph Database Because I Got Tired of Flattening Everything
Originally published on my blog: https://nicolai.hashnode.dev/i-built-a-typescript-object-graph-database-because-i-got-tired-of-flattening-everything I like rich domain models. Objects that point to other objects. Shared references. Classes with behavior. Maps. Sets. Sometimes cycles, because real-world domains have a rude habit of being less tidy than our storage layers would prefer. In TypeScript, this can feel very natural. You build a root object, hang your domain off it, pass references around, and the shape of the program starts to match the shape of the problem. Then persistence enters the room and quietly starts rearranging the furniture. Tables. Join tables. Mapper code. DTOs. Rehydration logic. ORM behavior you mostly understand until Thursday afternoon. All the familiar stuff. This is not a complaint about Postgres. I like Postgres. I use Postgres. If anything, Postgres has earned the right to look at most new database ideas with mild disappointment. Still, I kept running into cases where the application state wanted to be an object graph, and the persistence model wanted something else entirely. The translation layer became a project inside the project. So I started building GraphVault. The original spark came from EclipseStore. I liked the core idea immediately: persist the object graph itself. Let the application keep its natural shape. Treat the graph as the primary data structure instead of forcing it through a different model first. That idea stuck with me because even a familiar issue tracker becomes graph-shaped once you look past the main list of tickets. The interesting parts are the links: blockers, duplicates, related incidents, epics, customer context, workflow history, audit events, and the chain of "why did this change?" A table can store those facts, of course. But the domain you reason about is the network between them. I wanted to see what this would look like in TypeScript. GraphVault is embedded object-graph persistence for TypeScript and NestJS. You start with a root object, mutate ordinary TypeScript objects, and commit explicitly when you want to persist the current graph. const storage = await EmbeddedStorage.start({ storageDirectory: "./data", root: { documents: [] }, }); storage.root.documents.push({ id: "doc-1", title: "Hello object graph", }); await storage.storeRoot(); The graph can contain shared references, cycles, classes, Map, Set, Date, Buffer, bigint, typed arrays, and other JavaScript values. Object identity is preserved, so two references to the same object still point to the same object after reload. That was the first milestone: store the graph and get it back intact. Then the list grew, as these things do. GraphVault now has transactions with rollback, optimistic and pessimistic locking, WAL recovery, fencing tokens for shared stores, persistent indexes, schema migrations, health checks, NestJS integration, and a graphical admin tool called GraphVault Studio. At some point the weekend experiment started asking for production shoes. There are plenty of excellent databases already. I am not trying to talk anyone out of using them. GraphVault has a specific shape in mind: an application owns a rich object model and wants persistence close to that model. A few examples where this can make sense: issue and case-management systems with deeply connected entities rules engines where state and relationships matter simulations with object identity and references local-first or embedded apps internal admin-heavy tools services exposing bounded subgraphs through REST endpoints TypeScript/NestJS apps where the domain model is already the source of truth In those situations, the usual database mapping layer can become surprisingly noisy. You end up maintaining two models: the one your application wants and the one your database wants. GraphVault tries to reduce that gap. Persistence alone is not enough. Once the graph is stored, you need to inspect it, search it, aggregate over it, and sometimes make controlled updates. So GraphVault includes GVQL, the GraphVault Query Language. It gives you graph patterns, filters, joins across references, grouping, aggregates, execution plans, and preview-first batch updates. The goal is to make stored object graphs operable, not just serializable. That matters a lot for admin tooling. A persisted graph without good inspection tools quickly turns into "good luck, here is a giant blob." GraphVault Studio exists because I did not want the admin story to end at opening files and squinting. GraphVault was built with Codex. I gave the direction, the feature ideas, the quality bar, and the feedback. Codex did the implementation work: architecture, code, refactorings, tests, documentation, benchmarks, CI fixes, release preparation, and plenty of unglamorous glue work in between. The process felt less like "using a code generator" and more like steering a very fast engineering team. I would describe what I wanted, test the result, complain when the API felt wrong, ask for stronger guarantees, push on edge cases, and raise the bar whenever the project started looking too much like a prototype. Codex kept turning those prompts into working software. This is exactly the kind of project that makes me think AI-assisted software development is not just about saving time. It changes what a single developer can attempt. Of course, the result still has to stand on its own. Code does not get extra credit because an AI helped write it. If anything, it deserves more scrutiny. That is why I care so much about tests, benchmarks, explicit boundaries, and feedback from people who know databases well. But I am not going to pretend this was built the old way. It was not. GraphVault is a real experiment in building serious software with Codex in the loop. The question behind GraphVault is simple: Can persistence stay closer to the shape of a TypeScript application? I do not think the answer is always yes. Often the right answer is still a mature database with decades of battle scars and tooling. But there are domains where the object graph is the most honest representation of the state. In those domains, object-graph persistence can feel very natural. The challenge is making it operationally credible. That is where the recent work has gone: transactions, WAL, locking, fencing tokens, indexes, migrations, health checks, benchmarks, package smoke tests, and clearer documentation of the boundaries. GraphVault is young, but usable. It has: npm package: @sprengmeister/graphvault TypeScript-first API NestJS module and transaction decorator persistent indexes GVQL query and batch-update language schema migrations with up and down WAL recovery and transaction metadata optimistic and pessimistic locking health and safety reports GraphVault Studio as an admin UI tests, benchmarks, and package smoke tests The boundaries are important. GraphVault is embedded, application-owned storage. It does not provide SQL wire compatibility, external user/role management, built-in replication, or distributed consensus. That said, if you are building a TypeScript or NestJS app whose natural state is a connected object graph, it might be worth a look. Repo: https://github.com/Sprengmeister-dev/graphvault-library https://github.com/Sprengmeister-dev/graphvault-studio @sprengmeister/graphvault I would love feedback, especially from people who care about databases enough to be skeptical.
