Jim Ezesinachi - ezesinachijim@gmail.com · github.com/jimezesinachi
Ahnlich is an open-source, in-memory vector database and AI proxy written in Rust. It has two core services: ahnlich-db, which stores and retrieves vectors using similarity algorithms (cosine, euclidean, HNSW), and ahnlich-ai, which takes raw inputs like text or images, converts them into embeddings using ML models, and stores them in the database. The two services communicate over a custom binary protocol defined in Protocol Buffers.
I'm an active contributor to Ahnlich. My merged work includes the AI proxy decoupling (PR #247), and I currently have draft PRs for a PostgreSQL extension (#253) and a Raft-based horizontal scalability design (#276, #325) that I'm authoring as an RFC.
The thing that drew me to Ahnlich was the opportunity to work on systems-level architecture in Rust with real consequences for correctness. Ahnlich has SDKs in Rust, Python, Go, and TypeScript, a CLI, Docker support, and a growing user base. Every architectural decision ripples across the entire stack. The interplay between Rust's ownership model and the need for shared, concurrent access to data (via Arc, async runtimes, and Protocol Buffers) made even "simple" changes require careful thought about lifetimes and memory safety.
What I find most interesting right now is the horizontal scalability work. I'm authoring an RFC and prototype for Raft-based consensus clustering, designing how multiple Ahnlich nodes can replicate state, elect leaders, and maintain linearizable read consistency. It's the kind of distributed systems problem where you have to reason about failure modes, network partitions, and snapshot management, all while keeping the Rust implementation zero-copy and memory-safe.
When I joined the project, the AI proxy (ahnlich-ai) was tightly coupled to the database (ahnlich-db). Every request to generate embeddings required a live database connection, so you couldn't use the AI service independently. This was a real limitation: users who just wanted embedding generation without persistence were forced to spin up and maintain a database they didn't need, and it made testing, deployment, and horizontal scaling significantly harder.
The challenge wasn't just "make the database optional." It was doing so without breaking the existing contract between the AI service and every SDK client, while preserving the ability for the two services to work together seamlessly when the database is present.
I approached this in three layers:
1. Making the database connection optional at the server level. I modified AIProxyServer so that db_client became an Option<Arc<DbClient>> rather than a required field. This meant every handler in the request pipeline needed to be updated to check whether a database was available before attempting persistence operations. In Rust, this is where the type system really helps: the compiler enforced that every code path handled the None case, so there was no risk of a null pointer at runtime.
2. Extracting embedding generation into a standalone capability. I implemented AiService::convert_store_input_to_embeddings(), a new method that generates embeddings from raw inputs without attempting to store them. This was the core of the decoupling: it meant the AI service could fulfill its primary purpose (turning text/images into vectors) independently of whether those vectors would be persisted. The method uses Arc<StoreInputs> for the input data, which was a deliberate choice: since embedding generation can be parallelized across models, using Arc allowed multiple async tasks to share the input data without cloning, keeping memory usage predictable under load.
3. Updating the CLI and configuration layer. I added a --without-db flag to the CLI and updated the configuration parsing to support optional database parameters with proper conflict constraints (you can't specify a database host and --without-db). This sounds simple, but it required touching the Protocol Buffer definitions, the configuration structs, and the argument parser, making sure the types flowed correctly all the way from CLI input to server initialization.
I wrote comprehensive tests covering the new code paths, including tests for the Go, Python, and Rust SDK clients, to ensure that existing integrations continued to work identically when the database was present, and that the standalone mode behaved correctly when it wasn't.
The PR was 15 commits, 1,640 additions and 258 deletions across 32 files, and it merged after review.
This refactor unblocked two subsequent initiatives: my PostgreSQL extension draft (which embeds Ahnlich's AI capabilities directly into Postgres, where a standalone embedding service is essential), and the horizontal scalability design (where nodes need to operate in degraded states when parts of the cluster are unavailable). It also made the project more accessible to users who just want a fast, local embedding service without the overhead of a vector store.
SynthQL is a full-stack, type-safe client for PostgreSQL. It lets you write database queries in TypeScript with end-to-end type safety, from the query builder, through the execution engine, all the way to the React hooks that consume the results. Think of it as a layer that sits between your application and Postgres, ensuring that your queries are valid at compile time and your results are correctly typed at runtime.
I joined SynthQL early and became a core contributor, authoring 45 merged pull requests that span the entire stack: CI/CD pipelines, the CLI, the schema generator, the query engine, framework integrations (Express, Next.js), and the React client. I effectively helped build the project from its first CI workflow (PR #1) through to its permissions and middleware systems.
The most intellectually rewarding part of working on SynthQL was the type-level programming. TypeScript's type system is Turing-complete, and SynthQL pushes it hard. For example, when I built the permissions middleware (PR #61), the type signatures had to express things like "this middleware only applies to queries whose context includes a permissions field, and the permissions must be a subset of the ones defined on the query." Getting that right meant working with conditional types, mapped types, and generic constraints that compose across the query builder, the engine, and the middleware layer.
Similarly, when I mapped all of PostgreSQL's built-in data types to JSON Schema definitions (PR #51), I had to reason about how Postgres types like INTERVAL, MACADDR8, BOX, and nested arrays map to TypeScript types, and ensure those mappings were correct not just at the schema level but all the way through to the query results the developer sees in their IDE.
The hardest problem I faced was implementing comprehensive PostgreSQL data type support with array handling (PR #51). SynthQL generates a TypeScript schema from your Postgres database, and that schema drives the entire type-safety guarantee. But the initial implementation only covered basic types. Postgres has over 40 built-in types, many of which have surprising behaviors (for instance, NUMERIC and DECIMAL are the same type, SERIAL types are actually integers with sequences, and array types can be nested arbitrarily). If the schema generator got any of these wrong, the entire type-safety promise of the project would be broken: developers would get incorrect autocompletions and miss real bugs.
The specific challenge was handling arrays. In Postgres, any type can be an array, and arrays can be multi-dimensional. The schema generator needed to detect when a column type was an array, recursively resolve its element type, and generate the correct JSON Schema definition, including for edge cases like arrays of composite types or arrays of types that themselves have special serialization rules (like TIMESTAMP WITH TIME ZONE).
I started by auditing every built-in Postgres type and categorizing them by how they should be represented in JSON Schema. This gave me a mapping table of roughly 40 types across categories: numeric (BIGINT, SERIAL, DECIMAL, REAL, etc.), temporal (DATE, TIME, TIMESTAMP, INTERVAL, with and without timezone), network (CIDR, INET, MACADDR), geometric (BOX, CIRCLE, LINE, POINT, POLYGON), and specialized (UUID, JSON, JSONB, XML, BYTEA, BIT, MONEY).
For each type, I defined the correct JSON Schema representation in a function called createWellKnownDefs(). The tricky part was that some Postgres types map to strings in JSON (like UUID, INET, MACADDR), some to numbers (like BIGINT, SERIAL), and some to complex objects (like geometric types), and the mapping had to be consistent with how Postgres's wire protocol actually serializes these values.
For arrays, I implemented two key functions: isExpandedTypeAnArray(), which inspects the type metadata from Postgres's pg_catalog to detect array types, and createArrayDef(), which recursively generates JSON Schema for nested arrays. The recursion was important because Postgres supports multi-dimensional arrays (e.g., INTEGER[][]), and the schema generator needed to produce { type: "array", items: { type: "array", items: { type: "integer" } } }, correctly nested to arbitrary depth.
I also added an excludeTablesAndViews configuration option, because as I was testing against real databases, I found that many Postgres installations have system tables and views whose schemas would pollute the generated output. This wasn't part of the original scope, but it emerged naturally from testing and made the tool significantly more practical for real-world use.
The type system work in PR #51 became the foundation for my subsequent contributions:
Middleware API (PR #60): I designed a middleware system that lets developers plug custom logic into the query execution pipeline. Each middleware has a predicate (when it should apply) and a transformQuery function (what it should do). The type signatures ensure that a middleware can only transform queries it's compatible with, so you can't accidentally attach a user-filtering middleware to a query that doesn't have a user context.
Permissions / ACL (PR #61): Building on the middleware system, I implemented an access control layer. Queries can declare required permissions via a .permissions() builder method, and the engine validates those permissions against the request context at runtime. I added a dangerouslyIgnorePermissions escape hatch for testing and development, with the naming convention borrowed from React's dangerouslySetInnerHTML to make it clear that bypassing permissions is an intentional, flagged decision.