---
title: Architecture
description: "relate, the embedded Cypher graph database (relatedb.ai): How relate is built, from the Rust crates and the in-memory graph to the planner, indexes and storage, and the surfaces that wrap one engine."
type: explanation
surfaces:
  - cli
  - python
  - node
  - browser
  - rust
  - c
  - java
  - go
  - swift
  - bolt
  - http
  - mcp
status: stable
verified: 0.1.2
verified_by:
  - cypher
  - cli
canonical_url: https://relatedb.ai/docs/concepts/architecture/
prerequisites: []
related:
  - concepts/where-relate-fits
  - concepts/transactions-and-concurrency
  - concepts/storage-and-durability
  - guides/cypher/tune-a-query
last_updated: 2026-11-09
---

# Architecture

How relate is built, from the Rust crates and the in-memory graph to the planner, indexes and storage, and the surfaces that wrap one engine.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

How is relate put together? It is one engine written in Rust, with every front door wrapped around it. A statement is parsed, bound against the catalog, planned, and executed against a graph held in memory; each commit goes to a write-ahead log on its way in. The CLI, the language clients, the browser build and the network servers all call the same `relate::Database`, so a statement means the same thing through each of them.

## The layers

```mermaid
flowchart TB
  accTitle: The layers of relate
  accDescr {
    Surfaces sit on top: the CLI and relate serve with Bolt, HTTP and MCP; the Python client and the WebAssembly build; and the C ABI that the Node.js, Java, Go and Swift clients load.
    Every surface calls one public Rust API, relate::Database.
    Under it, a statement goes through the parser, the planner and the executor, which runs against the in-memory graph and its indexes.
    Storage keeps the graph durable with a write-ahead log and snapshots in a directory, a .rl file or memory.
  }
  S1["CLI, and relate serve<br/>for Bolt, HTTP, MCP"] --> DB
  S2["Python client,<br/>WebAssembly build"] --> DB
  S3["C ABI: Node.js,<br/>Java, Go, Swift"] --> DB
  DB["relate::Database"] --> Q["Parser, planner, executor"]
  Q --> GRAPH[("In-memory graph<br/>and indexes")]
  Q -->|"commit"| STORE[("WAL and snapshots")]
  STORE -->|"open: load, replay"| GRAPH
```

Surfaces sit on top: the CLI and relate serve with Bolt, HTTP and MCP; the Python client and the WebAssembly build; and the C ABI that the Node.js, Java, Go and Swift clients load. Every surface calls one public Rust API, relate::Database. Under it, a statement goes through the parser, the planner and the executor, which runs against the in-memory graph and its indexes. Storage keeps the graph durable with a write-ahead log and snapshots in a directory, a .rl file or memory.

## The crate map

The source is one Cargo workspace, split into groups by role. Engine crates depend only on other engine crates; [surfaces](https://relatedb.ai/docs/reference/glossary/#surface) depend on the engine and on each other; a boundary check in the build keeps it that way.

| Group      | Crates                                                                                                                                                                                                 | Role                                                                                                             |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| Engine     | `relate`, `relate-parser`, `relate-front`, `relate-ir`, `relate-planner`, `relate-executor`, `relate-storage`, `relate-catalog`, `relate-core`, `relate-model`, `relate-error` and a few small helpers | The public API, the query pipeline, the live graph, and the write path                                           |
| Surfaces   | `relate-cli`, `relate-python`, `relate-ffi`, `relate-wasm`, `relate-serve`, `relate-bolt`, `relate-http`, `relate-mcp`, `relate-auth`, `relate-formatter`, `relate-arrow`, `relate-load`               | Each front door, plus the output formats, Arrow conversion, served-surface auth, and the Neo4j loader they share |
| Clients    | Node.js, Java, Go, and Swift packages                                                                                                                                                                  | Thin wrappers over the C ABI that `relate-ffi` builds                                                            |
| Extensions | `relate-wire` and the first-party extensions                                                                                                                                                           | The protocol `relate run` speaks to extension packages                                                           |

## A statement's path

The parser turns Cypher text into a syntax tree, and a front end lowers it into the planner's own representation. Binding resolves labels, relationship types, properties and variables against the catalog without running anything, which is where an undefined variable fails; a missing parameter fails when the statement runs, not under `EXPLAIN`. The planner then produces a logical plan, and the executor runs that plan against the graph as of the statement's pinned commit.

The cells below follow one statement through the pipeline on the shared example graph, in this page's database. The first writes the graph:

```cypher run name=seed
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
```

```table
(no rows) · nodes created: 5, relationships created: 5, properties set: 13, labels added: 5
```

`EXPLAIN` stops after the planner: the statement is parsed, bound and planned, and the logical plan comes back without being executed.

```cypher run name=plan
EXPLAIN MATCH (p:Person {name: 'Ada'})-[:WORKS_ON]->(r:Project) RETURN r.name AS project
```

```table
(no rows)
```

The plan viewer shows that plan. In a browser it draws the operator tree; **PROFILE** executes the plan and adds what each operator did, and **Compare with the index** shows the planner choosing another start once a range index exists.

The plan recorded for cell `plan`, as `relate --mode table` prints it:

```text
Query: MATCH (p:Person {name: 'Ada'})-[:WORKS_ON]->(r:Project) RETURN r.name AS project
Plan:
+------------------+----+-------------------------------------+
| Operator         | Id | Details                             |
+------------------+----+-------------------------------------+
| + ProduceResults | 0  | 1 item(s)                           |
+------------------+----+-------------------------------------+
| | + Expand       | 1  | (r:Project)<-[:WORKS_ON]-(p:Person) |
+------------------+----+-------------------------------------+
Notifications:
Id 1: INFORMATION 03C91 Relate.Notification.Statement.MissingIndex: the equality predicate on name filters :Person without a range index on (:Person {name}) Remedy: CREATE INDEX relate_Person__name FOR (n:Person) ON (n.name)
```

Notifications, each with its remedy:

- [`03C91` Missing index](https://relatedb.ai/docs/reference/errors/#notif-03c91) on operator 1: the equality predicate on name filters :Person without a range index on (:Person {name}). Remedy: `CREATE INDEX relate_Person__name FOR (n:Person) ON (n.name)`

In a browser, the viewer's comparison runs `CREATE INDEX relate_Person__name FOR (n:Person) ON (n.name)` on the page's in-memory database only.

Live: the plan is drawn as a tree, and PROFILE runs the statement on the page's database in your browser and adds each operator's rows, time and database hits as bars; the index button creates the index on the page's database, plans the statement with and without it side by side with the changes, and drops it again; a PROFILE downloads as a speedscope file.

Without a prefix, the same statement goes through all three phases. After a run in your browser, the bar under the result splits the engine's elapsed time into parse, plan and execute:

```cypher run name=run timing
MATCH (p:Person {name: 'Ada'})-[:WORKS_ON]->(r:Project) RETURN r.name AS project
```

```table
| project  |
|----------|
| "relate" |
1 row
```

**The in-memory graph.** Labels, relationship types and property keys are interned, so each is stored once and compared as a number. Each node keeps its relationships in lists split by type, so expanding over `[:WORKS_ON]` walks only that type's list. A change installs a new version of the node or relationship stamped with its commit, which is how readers keep a consistent view while writers work; [transactions and concurrency](https://relatedb.ai/docs/concepts/transactions-and-concurrency/) covers that model. Disk holds the durable copy, and queries never scan it.

**The planner.** It validates the statement, then rewrites each `MATCH`: equality predicates fold into the pattern so they can seek, and the start of each pattern and the order of comma-separated patterns come from estimates the graph keeps: label counts and index entries. Other clauses run in the order written. `EXPLAIN` shows the result without running the statement.

**Indexes.** Label lookups and the typed relationship lists come with the graph. `CREATE INDEX` adds a range index on a label and property, which equality and range predicates seek through; vector and full-text indexes serve the `SEARCH` clause, and point indexes serve spatial predicates. The planner notices a missing range index and says so. One command on `:memory:`, the default store, shows the difference and writes nothing to disk: it creates the example graph, explains a statement, adds a range index on `:Person(name)`, and explains the same statement again:

```bash verify
relate --mode table "CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace);
EXPLAIN MATCH (p:Person {name: 'Ada'})-[:WORKS_ON]->(r:Project) RETURN r.name AS project;
CREATE INDEX person_name FOR (p:Person) ON (p.name);
EXPLAIN MATCH (p:Person {name: 'Ada'})-[:WORKS_ON]->(r:Project) RETURN r.name AS project"
```

The first plan has no index to use: it starts from `:Project`, which has fewer nodes, walks back to `:Person`, and the [notification](https://relatedb.ai/docs/reference/glossary/#notification) names the index that would help. The second plan seeks Ada through the new index and estimates one row:

```text
Query: MATCH (p:Person {name: 'Ada'})-[:WORKS_ON]->(r:Project) RETURN r.name AS project
Plan:
+------------------+----+-------------------------------------+
| Operator         | Id | Details                             |
+------------------+----+-------------------------------------+
| + ProduceResults | 0  | 1 item(s)                           |
+------------------+----+-------------------------------------+
| | + Expand       | 1  | (r:Project)<-[:WORKS_ON]-(p:Person) |
+------------------+----+-------------------------------------+
Notifications:
Id 1: INFORMATION 03C91 Relate.Notification.Statement.MissingIndex: the equality predicate on name filters :Person without a range index on (:Person {name}) Remedy: CREATE INDEX relate_Person__name FOR (n:Person) ON (n.name)
Query: MATCH (p:Person {name: 'Ada'})-[:WORKS_ON]->(r:Project) RETURN r.name AS project
Plan:
+-------------------+----+------------------------------------------------------------------------------------+----------------+
| Operator          | Id | Details                                                                            | Estimated Rows |
+-------------------+----+------------------------------------------------------------------------------------+----------------+
| + ProduceResults  | 0  | 1 item(s)                                                                          | -              |
+-------------------+----+------------------------------------------------------------------------------------+----------------+
| | + NodeIndexSeek | 1  | RANGE INDEX p:Person(name) WHERE name = 'Ada', (p:Person)-[:WORKS_ON]->(r:Project) | 1              |
+-------------------+----+------------------------------------------------------------------------------------+----------------+
```

**Storage.** A commit appends physical changes to the write-ahead log ([WAL](https://relatedb.ai/docs/reference/glossary/#wal)), then applies them to the graph. A [checkpoint](https://relatedb.ai/docs/reference/glossary/#checkpoint) writes the committed graph to a snapshot in one [codec](https://relatedb.ai/docs/reference/glossary/#codec), such as Parquet (the default) or JSON, and recycles old log segments. A virtual file system puts the same files in a directory, packs them into one `.rl` file, or keeps them in memory. [Storage and durability](https://relatedb.ai/docs/concepts/storage-and-durability/) explains when a commit is safe on disk.

## The surfaces around the core

The CLI links the engine into one binary. The Python client calls it in-process through a native extension module. The C ABI exposes it as a typed C interface, and the Node.js, Java, Go and Swift clients load that one library instead of each building its own binding. The WebAssembly build runs it in a browser page. `relate serve` opens one store and hosts the Bolt, HTTP Query API, and MCP servers over it, with one accept loop and one connection cap for all three.

## A large graph in a browser tab

The graph lives in the memory of the process that opens the store, and in a browser that process is the tab running the WebAssembly build. The three cells in this section build a tree and read it back: node `i`, from 1 up, is the child of node `(i - 1) / 2`, which makes it a binary tree as shallow as its size allows. They take `n`, the number of nodes, and `start`, the node the walk begins at, from this block:

```json data=tree
{"n": 1000, "start": 1}
```

The first cell writes `n` nodes labelled `Item` and joins each one, except node 0, to its parent with a `CHILD_OF` relationship:

```cypher run name=tree-build params=tree
UNWIND range(0, $n - 1) AS i
CREATE (item:Item {i: i})
WITH collect(item) AS items
UNWIND range(1, size(items) - 1) AS i
WITH items[i] AS child, items[(i - 1) / 2] AS parent
CREATE (child)-[:CHILD_OF]->(parent)
RETURN count(*) AS relationships
```

```table
| relationships |
|---------------|
| 999           |
1 row · nodes created: 1000, relationships created: 999, properties set: 1000, labels added: 1000
```

The second counts the nodes and the relationships between them:

```cypher run name=tree-count
MATCH (item:Item)
OPTIONAL MATCH (item)-[r:CHILD_OF]->()
RETURN count(item) AS nodes, count(r) AS relationships
```

```table
| nodes | relationships |
|-------|---------------|
| 1000  | 999           |
1 row
```

The third follows every chain of `CHILD_OF` relationships that ends at node `start`, and counts the nodes it reaches:

```cypher run name=tree-walk params=tree
MATCH (:Item {i: $start})<-[:CHILD_OF*]-(descendant:Item)
RETURN count(descendant) AS descendants
```

```table
| descendants |
|-------------|
| 510         |
1 row
```

The scale demo runs the same three cells with `n` up to 100,000, on a database of its own, and shows what each returns and how long the engine took. Nothing runs until **Build the tree** is pressed; the tree then holds memory in the tab until **Free** closes the demo's engine.

The scale demo runs cells `tree-build`, `tree-count` and `tree-walk` again with `n` set to 1,000, 10,000 or 100,000, on an in-memory database of its own in the browser tab: a second copy of the engine, so the page's database never holds the large graph. `tree-build` creates the tree, `tree-count` counts its nodes and relationships, and `tree-walk` follows every chain of relationships down from one node. It shows each cell's first row and the time the engine took, beside the cell's recording. It offers 100,000 nodes first on a wide screen and 10,000 on a narrow one, says how much memory the second engine holds, and closes that engine on Free, on the engine bar's Reset, and when the page is left.

| Cell         | Recorded on this page, with `n` = 1,000 |
| ------------ | --------------------------------------- |
| `tree-build` | relationships: 999                      |
| `tree-count` | nodes: 1,000, relationships: 999        |
| `tree-walk`  | descendants: 510                        |

Live: a button runs the three cells again at the size picked (the largest by default, the middle size on a narrow screen) on a separate in-memory database in your browser, apart from the page's database, and shows each cell's result and engine time; a second button runs the count and the walk again from a node you name; it shows how much memory that database's engine holds; Free closes it and returns the memory.

## What the layering buys, and what it costs

One engine under every surface means one behavior to test and document: an error, a result type or a notification is the same in the CLI, in Python, and over Bolt. A fix lands everywhere at once, and the clients stay thin. The same engine crates also compile to WebAssembly, which is how the browser build exists.

The cost is in what one engine cannot be. Every surface inherits the engine's shape: one process per store, the graph in that process's memory, and the client running in the same address space as the engine, so a client process that holds a large graph holds it in its own memory. The four clients that share the C ABI also share its contract of one call at a time per connection. And the planner's rewrites cover `MATCH` patterns while other clauses run in the order written, so how a statement is written still matters; `EXPLAIN` and `PROFILE` show what it chose.

## Where this shows up

- [Tune a query](https://relatedb.ai/docs/guides/cypher/tune-a-query/): reading `EXPLAIN` and `PROFILE`, and acting on index notifications.
- [Transactions and concurrency](https://relatedb.ai/docs/concepts/transactions-and-concurrency/): versions, pinned views and conflicts in the in-memory graph.
- [Storage and durability](https://relatedb.ai/docs/concepts/storage-and-durability/): the write path, checkpoints, and codecs.
- [Storage formats](https://relatedb.ai/docs/reference/storage/formats/): what a directory store and a `.rl` file hold.
- [Where relate fits](https://relatedb.ai/docs/concepts/where-relate-fits/): the conditions that follow from this design.

## Terms used on this page

- **surface**: Any front door to the relate engine: the CLI, a language client, the C ABI, the WebAssembly build, or a server that `relate serve` hosts.
- **notification**: Advice the planner attaches to a plan, such as a missing index, with a GQLSTATUS code, a severity and a remedy; derived only under `EXPLAIN` or `PROFILE`.
- **WAL**: Append-only log each commit goes to before it applies; under the default `full` durability mode it is synced first, so a crash loses no acknowledged write.
- **checkpoint**: The step that writes a fresh snapshot of committed state and recycles old WAL segments: every 300 seconds by default, on clean exit, and on `.checkpoint`.
- **codec**: The on-disk encoding of a store's snapshot: Parquet (the default), JSON or markdown, fixed when the store is created.
