Back to Blog
Architecture March 24, 2026 | 10 min read

Graph Databases in Elixir: KuzuDB Integration

Modeling entity relationships with an embedded graph database

Prismatic Engineering

Prismatic Platform

Why Graph Databases?


Relational databases excel at storing structured records with well-defined schemas. But some data is inherently about relationships. In due diligence investigations, you care less about individual entities and more about how entities connect: who owns what company, which companies share directors, how money flows between accounts, where subsidiaries overlap.


Graph databases model this naturally. Entities are nodes, relationships are edges, and queries traverse the graph to discover patterns that would require complex multi-table JOINs in SQL.


KuzuDB: An Embedded Graph Database


KuzuDB is an embedded, columnar graph database engine written in C++. Unlike Neo4j or ArangoDB, KuzuDB runs in-process -- no separate server, no network overhead, no operational complexity. It is to graph databases what SQLite is to relational databases.


Key characteristics:


  • Embedded: Runs inside the Elixir VM process via NIF bindings
  • Columnar storage: Optimized for analytical graph queries
  • Cypher support: Uses the openCypher query language
  • ACID compliant: Full transaction support
  • Disk-based: Persists data to disk, not limited by RAM

  • The prismatic_storage_kuzudb Adapter


    The adapter wraps KuzuDB in the platform's storage trait system:


    
    

    defmodule PrismaticStorageKuzudb do

    @behaviour PrismaticStorageCore.StorageTrait


    def init(config) do

    db_path = Keyword.fetch!(config, :db_path)

    {:ok, conn} = Kuzu.Database.open(db_path)

    {:ok, %{conn: conn, db_path: db_path}}

    end


    def query(state, cypher_query, params) do

    Kuzu.Connection.execute(state.conn, cypher_query, params)

    end

    end


    The adapter implements the standard StorageTrait interface (init/1, get/2, put/3, delete/2, query/2) while exposing KuzuDB-specific capabilities through an extended API.


    Entity Relationship Modeling


    The DD (Due Diligence) system uses KuzuDB to model entity relationships. The schema defines node and edge types:


    Node Types


    TypePropertiesExample

    |------|-----------|---------|

    Personname, birth_date, nationality, role"Jan Novak", CEO Companyname, ico, registration_date, status"Acme s.r.o.", ICO 12345678 Addressstreet, city, postal_code, country"Vaclavske namesti 1, Praha" BankAccountiban, bank_name, currency"CZ6508000000001234567890" Domainfqdn, registrar, expiry_date"acme-corp.cz"

    Edge Types


    TypeFromToProperties

    |------|------|----|-----------|

    OWNSPersonCompanyshare_percentage, since DIRECTSPersonCompanyrole, since, until REGISTERED_ATCompanyAddresssince LIVES_ATPersonAddresssince OPERATESCompanyDomainsince HOLDSPerson/CompanyBankAccounttype TRANSACTS_WITHCompanyCompanyamount, currency, date

    Graph Traversal Queries


    The power of graph databases shines in traversal queries -- finding paths and patterns across relationships.


    Finding Connected Entities


    
    

    // Find all companies connected to a person within 3 hops

    MATCH (p:Person {name: $name})-[*1..3]-(c:Company)

    RETURN DISTINCT c.name, c.ico


    Detecting Circular Ownership


    
    

    // Find circular ownership chains

    MATCH path = (c1:Company)-[:OWNS*2..5]->(c1)

    RETURN path


    Identifying Shared Directors


    
    

    // Find people who direct multiple companies

    MATCH (p:Person)-[:DIRECTS]->(c:Company)

    WITH p, collect(c) AS companies

    WHERE size(companies) > 1

    RETURN p.name, [c IN companies | c.name] AS directed_companies


    Money Flow Analysis


    
    

    // Trace money flow from source company through intermediaries

    MATCH path = (source:Company {name: $source})-[:TRANSACTS_WITH*1..4]->(dest:Company)

    WHERE dest.name = $destination

    RETURN path, [r IN relationships(path) | r.amount] AS amounts


    Cypher Query Integration


    The adapter provides a safe Cypher query interface that prevents injection through parameterized queries:


    
    

    def find_connected_entities(person_name, max_depth) do

    query = """

    MATCH (p:Person {name: $name})-[*1..#{max_depth}]-(e)

    RETURN labels(e) AS type, properties(e) AS props

    LIMIT 100

    """


    PrismaticStorageKuzudb.query(query, %{name: person_name})

    end


    Note the use of $name parameter binding instead of string interpolation. The max_depth is an integer used in the Cypher pattern syntax (not a string value), so it is safe to interpolate.


    The DD Relationship Explorer


    The relationship explorer is a LiveView component at /hub/dd/graph that visualizes the entity graph:


    Features


  • Interactive graph visualization: D3.js force-directed layout with pan, zoom, and drag
  • Entity search: Find any person, company, or address and center the view
  • Path highlighting: Click two nodes to highlight the shortest path between them
  • Filtering: Show/hide node types and edge types to reduce visual complexity
  • Detail panel: Click any node to see its full property set and direct connections
  • Export: Export visible subgraph as JSON or CSV for external analysis

  • Performance


    KuzuDB's columnar storage provides excellent query performance for graph traversals:


    
    

    Single-hop neighbors: < 5ms (up to 1000 results)

    3-hop path finding: < 50ms (typical case graphs)

    Circular ownership check: < 100ms (up to 10,000 nodes)

    Full graph export: < 500ms (typical case, ~5000 nodes)


    For comparison, equivalent SQL queries with multiple self-JOINs typically take 5-10x longer and become exponentially slower as hop depth increases.


    Schema Migration


    KuzuDB schema changes are managed through the adapter's migration system:


    
    

    def migrate(state, :add_domain_nodes) do

    Kuzu.Connection.execute(state.conn,

    "CREATE NODE TABLE Domain(fqdn STRING, registrar STRING, PRIMARY KEY(fqdn))"

    )

    Kuzu.Connection.execute(state.conn,

    "CREATE REL TABLE OPERATES(FROM Company TO Domain, since DATE)"

    )

    end


    Migrations are idempotent and tracked in a metadata table within the KuzuDB database itself.




    Relationships are data. Graph databases make that data queryable.


    Tags

    kuzudb graph-database cypher relationships elixir