Skip to main content
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.
  • 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

Authentication

Every request needs your HydraDB 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

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

  • 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

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

DELETE /byog/databases: drop a database

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:
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.
  • 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.
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.
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:
Expand a node's neighborhood
Multi-hop relationship expansion
Shortest path between two nodes
Filtered, typed traversal

Response format

Successful calls return the standard HydraDB envelope:
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

Example (RETURN n where n is a node):
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.

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:
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:

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:
A path is already assembled for you: nodes and edges in traversal order. Remember the shortestPath rules above (in RETURN or WITH, and directed):

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:

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:

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:

Limits & timeouts

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); 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.