- 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 nreturns your nodes and nothing else.
Quickstart
Authentication
Every request needs your HydraDB API key:403. 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
{"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.
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
paramsrather than string-building it into the query - parameters are bound safely and keep query plans cacheable. - Request bodies are capped at 256 KiB (
413beyond that). For bulk loads, send batches of rows withUNWIND $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
DELETE /byog/databases - drop a database
POST /byog/databases - removes the database itself. The response
lists what was removed:
deleted is
false - manage the database itself through the standard DELETE /databases.
Supported Cypher
Coverage in one line: essentially all of openCypher is supported - only
server-side procedures and file loading are excluded - which is the entire
surface a real application needs for modelling, loading, querying and
maintaining its own graph.
UNWIND, WITH pipelines, MERGE, indexes
(CREATE INDEX FOR (n:Label) ON (n.prop)), and CALL { ... } subqueries.
Graph traversal and exploration - the part that makes this a graph rather
than a table. You are not limited to reading and writing single nodes; you can
walk relationships to arbitrary depth, 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.namereturns 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 -
shortestPathreturns 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.
400 and a message
explaining the reason, and nothing is executed - no partial writes, no
side effects. It is a validation error, not a runtime one, so retrying the
same query fails identically until you change it.
A few dialect notes (each verified against the live service):
- Existence checks are written as bare pattern predicates -
MATCH (p:Person) WHERE (p)-[:KNOWS]->() RETURN p.name AS name. TheEXISTS { ... }block form and theexists()function are not accepted. shortestPathgoes in aRETURNorWITHclause (notMATCH 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: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:
- The
id/labels/relation/source_node_id/target_node_idkeys 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). idvalues 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 patterns below are everything you need to consume query results reliably. They’re shown in Python and TypeScript; the ideas port to any language.A minimal client
Wrap the endpoint once and everything else becomes one-liners. Note the two response shapes: success puts rows indata, errors are wrapped in detail.
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:When you do return nodes
A returned node is one flat object: its properties merged withid and
labels. There is no nesting to unwrap:
id is internal and non-portable - treat it as opaque within a
session and never store it. Key your data on a property you own (ext_id,
email, …).
Relationships and paths
A relationship row carries its endpoints assource_node_id /
target_node_id, which match the id of nodes returned in the same
query - return both sides and join client-side:
nodes and edges in traversal order.
Two constraints to know: shortestPath must appear in a RETURN or WITH
clause (not in MATCH p = ...), and the traversal must be directed (->):
Pagination - the loop to copy
Result sets past the deployment cap are silently truncated, so any read that could be large should page. A stableORDER BY makes pages consistent:
Bulk loading - the loop to copy
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: your Cypher (compiler feedback is passed through) or a query that needsLIMIT/an index (budget timeout). Retrying unchanged will fail identically.429/500- transient; retry with backoff. Writes built onMERGE(as above) are safe to retry; bareCREATEbatches are not idempotent, so a retried chunk can duplicate nodes - one more reason to key on your own id.- A write with no
RETURNsucceeds withdata: []- don’t treat empty as failure.
Errors
Errors use HydraDB’s structured error shape: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.
Practical guidance:
- Paginate anything potentially large:
ORDER BY … SKIP $offset LIMIT $page. Without anORDER BY, rows dropped at the result-set cap are arbitrary. - Chunk bulk imports into
UNWIND $rowsbatches sized to finish inside the 30 s write budget (and the 256 KiB body cap). - 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 CSVis not available - batch data in throughparams.- 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.
