> ## Documentation Index
> Fetch the complete documentation index at: https://cortex-e852fafe-t3code-rewrite-docs-declutter.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Bring Your Own Graph: Cypher collections

> BYOG: full Cypher access to graph collections you own end-to-end.

Bring Your Own Graph (BYOG) lets you store and query a graph you define.
**Cypher** is the query language for creating and finding its nodes (entities)
and relationships. You define the data model and write the queries; HydraDB
stores the graph and runs them. It is built for teams migrating an
existing property-graph workload (for example from Neo4j) who want to keep
their Cypher and their data model as-is. To attach your own entities and
relations to one ingested document, app source, or memory instead, see
[Bring Your Own Graph: per source](/essentials/v2/bring-your-own-graph).

* **Databases** group your collections. A BYOG database appears in your
  dashboard and in the standard database APIs like any other HydraDB database.
* **Collections** are independent graphs inside a database. Queries run
  against exactly one collection. Collections never see each other's data.
* **Full Cypher support:** reads and writes alike (`CREATE`, `MERGE`,
  `MATCH`, `SET`, `DELETE`), plus the graph-native surface: multi-hop and
  variable-length traversal, relationship expansion, and shortest-path
  finding. Your query is sent verbatim; HydraDB never rewrites it.
* **Isolation is structural:** Each collection is a completely separate graph
  owned by your organization. There is no cross-tenant data to reach:
  `MATCH (n) RETURN n` returns *your* nodes and nothing else.

## Quickstart

```bash theme={"dark"}
BASE=https://api.hydradb.com
KEY="your_api_key"

# 1. Create a database (ready immediately)
curl -X POST "$BASE/byog/databases" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"database": "crm"}'

# 2. Write data - collections auto-create on first use
curl -X POST "$BASE/byog/query" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "database": "crm",
    "collection": "contacts",
    "query": "UNWIND $rows AS row CREATE (p:Person) SET p = row RETURN count(p) AS created",
    "params": {"rows": [{"name": "Alice", "role": "admin"},
                        {"name": "Bob",   "role": "analyst"}]}
  }'

# 3. Read it back
curl -X POST "$BASE/byog/query" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "database": "crm",
    "collection": "contacts",
    "query": "MATCH (p:Person) RETURN p.name AS name, p.role AS role ORDER BY name"
  }'
```

## Authentication

Every request needs your HydraDB API key:

```
Authorization: Bearer <api key>
```

A missing or invalid key returns `401`. Databases are scoped to the
organization that owns the API key. Another organization's database names
are invisible to you (they behave exactly like names that don't exist).

## Endpoints

### `POST /byog/databases`: create a database

```json theme={"dark"}
{ "database": "crm" }
```

Returns immediately with `{"database": "crm", "status": "ready"}`: there is
no provisioning wait. Creating a name that already exists returns `409`.

The database also shows up everywhere your other HydraDB databases do:
`GET /databases` lists it, `GET /databases/status` reports it ready,
`GET /databases/collections` includes its collections, and it is visible on
the dashboard. Deleting it through the standard `DELETE /databases` flow
removes its graph collections as well.

A BYOG database holds only Cypher graphs. Sending it to `/context/ingest` or
`/query` returns `400` with the error code `BYOG_DATABASE`.

### `POST /byog/query`: run Cypher

```json theme={"dark"}
{
  "database":   "crm",
  "collection": "contacts",
  "query":      "CREATE (n:Person {name: $name}) RETURN n",
  "params":     { "name": "Alice" }
}
```

* **Collections auto-create:** there is no create-collection call. The first
  write brings the collection into existence; reading a collection you never
  wrote to simply returns zero rows.
* Collection names must match `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`.
  Database names have no charset restriction.
* Always pass user data through `params` rather than string-building it into
  the query. Parameters are bound safely and keep query plans cacheable.
* Request bodies are capped at **256 KiB** (`413` beyond that). For bulk
  loads, send batches of rows with `UNWIND $rows AS row CREATE ...`.

### `GET /byog/collections?database=crm`: list collections

Returns the collection names that exist in the database. An unknown database
returns `404`.

### `DELETE /byog/collections`: drop one collection

```json theme={"dark"}
{ "database": "crm", "collection": "contacts" }
```

Drops the collection and all its data. Deleting a collection that does not
exist is a success (idempotent).

### `DELETE /byog/databases`: drop a database

```json theme={"dark"}
{ "database": "crm" }
```

Drops every collection in the database, and (if the database was created
through `POST /byog/databases`) removes the database itself. The response
lists what was removed:

```json theme={"dark"}
{ "database": "crm", "deleted": true, "deleted_collections": ["contacts"] }
```

If the database was created through the standard database API (and merely has
graph collections in it), only the collections are dropped and `deleted` is
`false`. Manage the database itself through the standard `DELETE /databases`.

## Supported Cypher

Two kinds of work are supported:

**Modelling and CRUD**, the day-to-day read/write surface: pattern matching,
aggregation, `UNWIND`, `WITH` pipelines, `MERGE`, indexes
(`CREATE INDEX FOR (n:Label) ON (n.prop)`), and `CALL { ... }` subqueries.

**Graph traversal and exploration:** walk relationships, expand a node's
neighborhood, follow chains of edges, and find paths between nodes:

* **Multi-hop patterns:** chain relationships across as many hops as you need
  in one `MATCH`:
  `MATCH (a:Person)-[:KNOWS]->(b)-[:WORKS_AT]->(c:Company) RETURN c.name`.
* **Variable-length traversal:** follow a relationship an unbounded or bounded
  number of hops with `*`:
  `MATCH (a:Person {name:$n})-[:KNOWS*1..4]->(reach) RETURN DISTINCT reach.name`
  returns everyone within four degrees.
* **Neighborhood expansion:** pull a node's edges and neighbors in a single
  query, in any direction:
  `MATCH (p:Person {name:$n})-[r]-(nbr) RETURN type(r) AS rel, nbr.name AS neighbor`.
* **Path finding:** `shortestPath` returns the actual path: nodes and edges
  in traversal order, not just the two endpoints. See
  [Relationships and paths](#relationships-and-paths).
* **Directed, typed, filtered traversal:** restrict to outgoing (`->`),
  incoming (`<-`), or either (`-`) edges, filter by relationship type
  (`[:KNOWS]`), and constrain node or edge properties anywhere along the walk.

Unsupported constructs are rejected before execution with `400` and an
explanation. No partial writes occur; change the query before retrying.

<Warning>
  The following constructs always return `400` and are never executed:

  * **Procedure calls:** `CALL some.procedure(...)`. Procedures are
    engine-specific internals that HydraDB does not commit to supporting.
    (`CALL { ... }` *subqueries* are fine.)
  * **`LOAD CSV`:** server-side file/URL loading. Send data through `params`
    instead.
</Warning>

A few dialect notes:

* **Existence checks:** write them as bare pattern predicates:
  `MATCH (p:Person) WHERE (p)-[:KNOWS]->() RETURN p.name AS name`.
  The `EXISTS { ... }` block form and the `exists()` function are not
  accepted.
* **`shortestPath`:** goes in a `RETURN` or `WITH` clause (not `MATCH p = …`)
  and the traversal must be directed (see the paths example above).

### Traversal examples

Expand a node's relationships, follow chains of edges, and find the shortest
path between two nodes, all in plain Cypher:

```cypher Expand a node's neighborhood theme={"dark"}
-- Every relationship on Alice and the node on the other end, in any direction.
MATCH (p:Person {name: $name})-[r]-(nbr)
RETURN type(r) AS rel, labels(nbr) AS kind, nbr.name AS neighbor
ORDER BY rel, neighbor
```

```cypher Multi-hop relationship expansion theme={"dark"}
-- Friends-of-friends up to 3 hops away, following only outgoing KNOWS edges.
MATCH (a:Person {name: $name})-[:KNOWS*1..3]->(reach:Person)
WHERE reach.name <> $name
RETURN DISTINCT reach.name AS name
ORDER BY name
```

```cypher Shortest path between two nodes theme={"dark"}
-- The actual path (nodes + edges in order), not just the endpoints.
MATCH (a:Person {name: $from}), (b:Person {name: $to})
RETURN shortestPath((a)-[:KNOWS*..8]->(b)) AS path
```

```cypher Filtered, typed traversal theme={"dark"}
-- Who does Alice know that works at a company in "fintech"?
MATCH (a:Person {name: $name})-[:KNOWS]->(f:Person)-[:WORKS_AT]->(c:Company)
WHERE c.sector = "fintech"
RETURN f.name AS person, c.name AS company
```

## Response format

Successful calls return the standard HydraDB envelope:

```text theme={"dark"}
{
  "success": true,
  "data": [ ... ],
  "error": null,
  "meta": { "request_id": "9be86a4e-…", "latency_ms": 12.4 }
}
```

For `POST /byog/query`, `data` is always a JSON **array of row objects**, one
per result row, keyed by your `RETURN` column names. Unaliased expressions use
the expression text as the key: **alias everything you plan to parse**
(`RETURN n.name AS name`). A pure write with no `RETURN` yields `data: []`.

### How values are rendered

| Cypher value | JSON |
| - | - |
| string / boolean / null | JSON string / boolean / null |
| integer | JSON number. Graph integers are 64-bit; values beyond 2⁵³ lose precision in languages that parse numbers as doubles. Keep your own ids inside the safe range, or return them as strings |
| float | JSON number |
| list / map | JSON array / object (rendered recursively) |
| **node** | object with all node properties, plus `id` and `labels` |
| **relationship** | object with all relationship properties, plus `id`, `relation`, `source_node_id`, `target_node_id` |
| **path** | `{ "nodes": [...], "edges": [...] }` in traversal order |

Example (`RETURN n` where `n` is a node):

```json theme={"dark"}
{ "data": [ { "n": { "id": 0, "labels": ["Person"], "name": "Alice", "age": 34 } } ] }
```

Two things to know about ids:

* The `id` / `labels` / `relation` / `source_node_id` / `target_node_id` keys
  are added by the renderer. If you store a property with one of those names,
  it will be shadowed in the *response* (the stored value is unaffected).
  Avoid those property names or alias explicitly (`RETURN n.id AS my_id`).
* `id` values are internal and stable only within the life of a collection:
  they can be reused after deletions and do not survive an export/re-import.
  Key your application on a property you own.

## Using results in your code

The examples below show how to consume results in Python and TypeScript.

### A minimal client

Success puts rows in `data`; errors put the code and message in `error`,
mirrored in `detail`, which the examples read.

```python theme={"dark"}
import requests

class HydraGraph:
    def __init__(self, base_url, api_key, database, collection="default"):
        self.base, self.db, self.col = base_url, database, collection
        self.headers = {"Authorization": f"Bearer {api_key}"}

    def query(self, cypher, params=None):
        r = requests.post(f"{self.base}/byog/query", headers=self.headers, json={
            "database": self.db, "collection": self.col,
            "query": cypher, "params": params or {},
        })
        body = r.json()
        if not r.ok:
            err = body.get("detail", {})
            raise RuntimeError(f"{r.status_code} {err.get('error_code')}: {err.get('message')}")
        return body["data"]          # always a list of row dicts

g = HydraGraph("https://api.hydradb.com", "<api key>", "crm", "contacts")
```

```typescript theme={"dark"}
async function query(cypher: string, params: object = {}): Promise<Record<string, any>[]> {
  const res = await fetch(`${BASE}/byog/query`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ database: "crm", collection: "contacts", query: cypher, params }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.detail?.error_code}: ${body.detail?.message}`);
  return body.data;                 // always an array of row objects
}
```

### Prefer scalars with aliases: they parse themselves

The easiest results to consume are the ones you shape in the query. Instead of
returning whole nodes and digging properties out client-side, project exactly
the fields you need, aliased:

```python theme={"dark"}
rows = g.query("MATCH (p:Person) RETURN p.name AS name, p.role AS role ORDER BY name")
# rows == [{"name": "Alice", "role": "admin"}, {"name": "Bob", "role": "analyst"}]
names = [r["name"] for r in rows]
```

This also makes your code robust to schema growth: adding properties to nodes
never changes what your queries return.

### When you do return nodes

A returned node is one flat object: its properties merged with `id` and
`labels`. There is no nesting to unwrap:

```python theme={"dark"}
rows = g.query("MATCH (p:Person {name: $n}) RETURN p", {"n": "Alice"})
alice = rows[0]["p"]        # {"id": 0, "labels": ["Person"], "name": "Alice", "role": "admin"}
props = {k: v for k, v in alice.items() if k not in ("id", "labels")}
```

### Relationships and paths

A relationship row carries its endpoints as `source_node_id` /
`target_node_id`, which match the `id` of nodes returned **in the same
query**. Return both sides and join client-side:

```python theme={"dark"}
rows = g.query("""
    MATCH (a:Person)-[k:KNOWS]->(b:Person)
    RETURN a, k, b
""")
for row in rows:
    print(f'{row["a"]["name"]} knows {row["b"]["name"]} since {row["k"]["since"]}')
```

A path is already assembled for you: `nodes` and `edges` in traversal order.
Remember the [`shortestPath` rules](#supported-cypher) above (in `RETURN` or
`WITH`, and directed):

```python theme={"dark"}
rows = g.query("""
    MATCH (a:Person {name:$x}), (b:Person {name:$y})
    RETURN shortestPath((a)-[*..6]->(b)) AS p
""", {"x": "Alice", "y": "Bob"})
hops = [n["name"] for n in rows[0]["p"]["nodes"]]   # ["Alice", ..., "Bob"]
```

### Pagination

Result sets past the deployment cap are silently truncated, so any read that
*could* be large should page. A stable `ORDER BY` makes pages consistent:

```python theme={"dark"}
def all_rows(cypher_body, page=500, params=None):
    offset = 0
    while True:
        rows = g.query(f"{cypher_body} SKIP $offset LIMIT $limit",
                       {**(params or {}), "offset": offset, "limit": page})
        yield from rows
        if len(rows) < page:
            return
        offset += page

people = list(all_rows("MATCH (p:Person) RETURN p.name AS name ORDER BY name"))
```

### Bulk loading

Chunk rows to stay inside the 256 KiB body cap and the 30 s write budget;
`MERGE` on your own key makes the load re-runnable after a failure:

```python theme={"dark"}
def load(rows, chunk=500):
    for i in range(0, len(rows), chunk):
        g.query("""
            UNWIND $rows AS row
            MERGE (n:Person {ext_id: row.ext_id})
            SET n += row
        """, {"rows": rows[i:i+chunk]})
```

### Handling failures

* **`400`:** the message tells you what to fix, either your Cypher (compiler
  feedback is passed through) or a query that needs `LIMIT`/an index (budget
  timeout). Retrying unchanged will fail identically.
* **`429` / `500`:** transient; retry with backoff. Writes built on `MERGE`
  (as above) are safe to retry; bare `CREATE` batches are not idempotent, so
  a retried chunk can duplicate nodes (one more reason to key on your own id).
* A write with no `RETURN` succeeds with `data: []`. Don't treat empty as
  failure.

## Errors

Errors use the standard HydraDB error envelope, with the code and message mirrored in a `detail` object:

```json theme={"dark"}
{
  "success": false,
  "data": null,
  "error": { "code": "…", "message": "…" },
  "meta": { "request_id": "…" },
  "detail": { "success": false, "message": "…", "error_code": "…" }
}
```

| Status | Meaning |
| - | - |
| `400` | Invalid request (missing fields, bad collection name), unsupported construct, **Cypher errors** (the compiler's message is passed through so you can fix the query), or **query timeout** |
| `401` | Missing or invalid API key |
| `404` | Unknown database. Create it with `POST /byog/databases` |
| `409` | `POST /byog/databases` with a name that already exists |
| `413` | Request body over 256 KiB |
| `429` | Rate limit exceeded. Back off and retry |
| `500` | Something failed on our side. Retry with backoff; see [Handling failures](#handling-failures) for writes that are not idempotent |
| `503` | The database's dedicated graph cluster is still being provisioned. Retry after the `Retry-After` interval |

## Limits & timeouts

| Limit | Value | On exceeding |
| - | - | - |
| Request body | 256 KiB | `413` |
| Read query execution | 8 s | `400`: "query exceeded the execution time budget; simplify it, add LIMIT, or create an index" |
| Write query execution | 30 s | same `400` |
| Result set size | deployment-configured cap; rows beyond it are **silently dropped** | no error: paginate |

A query counts as a **write** (and gets the larger budget) when it contains
any write clause: `CREATE`, `MERGE`, `SET`, `DELETE`, `REMOVE`, `FOREACH`.

Paginate large reads with a stable `ORDER BY` (see [Pagination](#pagination)); without one, rows dropped at the result-set cap are arbitrary. Create indexes for properties you filter on (`CREATE INDEX FOR (n:Person) ON (n.name)`): long-running reads are usually missing one.

## Migrating from Neo4j

Most application Cypher ports directly. The differences you are most likely
to notice:

* `CALL db.*` / `CALL apoc.*` procedures are not available. The equivalents
  are either plain Cypher or not part of the supported surface.
* `LOAD CSV` is not available. Batch data in through `params`.
* Internal node ids are not portable (true in Neo4j as well). Migrate using
  your own key properties, e.g.
  `UNWIND $rows AS row MERGE (n:Person {ext_id: row.ext_id}) SET n += row`.

One collection is a natural migration unit: export a graph, replay it into
one collection, verify, repeat.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.