Skip to content

GraphQL API

The HornetHive GraphQL API for PostgreSQL, powered by Hasura: the endpoint and keys, where your tables are, syncing after DDL, queries, mutations, joins through views, introspection and limits.

Endpoint and keys

Each database has one GraphQL endpoint, https://api.hornethive.io/v1/graphql/{database id}, served by Hasura. Send POST with a JSON body {"query": "…", "variables": {…}} and an API key as Authorization: Bearer <key>.

A key with only the read scope runs queries; a key with write also runs mutations. Either way the request runs as a role that can see this one database's tables and nothing else, and none of your headers reach Hasura.

DB=<database id>
KEY=<api key>
curl -X POST "https://api.hornethive.io/v1/graphql/$DB" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"query":"{ u4f1c2d3e_shop { books(limit: 5) { id title } } }"}'

Where your tables are

Every database's tables sit under one root field named after the database, such as u4f1c2d3e_shop; the database page shows yours. Type names carry the same name as a prefix (u4f1c2d3e_shop_books), so two databases with a books table never clash.

The tables and views of the public schema are served. Other schemas are not; reach those through the REST API.

After you change tables

GraphQL knows the tables that existed at its last sync. A new database is synced when it is created; after you create a table or view, or change its columns, sync again: Sync tables on the database's GraphQL tab, or the sync_graphql tool over MCP. The REST API needs no sync.

Queries

Each table is a field that takes where, order_by, limit, offset and distinct_on. Conditions are _eq, _neq, _gt, _gte, _lt, _lte, _in, _nin, _like, _ilike, _is_null and more, combined with _and, _or and _not.

<table>_aggregate counts and sums, and <table>_by_pk fetches one row when the table has a primary key. Pass values as variables rather than writing them into the query.

query Recent($since: timestamptz!) {
  u4f1c2d3e_shop {
    orders(where: { created_at: { _gte: $since }, status: { _in: ["paid", "shipped"] } },
           order_by: { created_at: desc }, limit: 20) {
      id
      total
    }
    orders_aggregate(where: { status: { _eq: "paid" } }) {
      aggregate { count sum { total } }
    }
  }
}

Mutations

With a write key: insert_<table> and insert_<table>_one, which take on_conflict to upsert on a unique constraint; update_<table> with where and _set or _inc; delete_<table> with where; and the _by_pk forms of update and delete. Each returns affected_rows and the rows in returning. Mutations are under the same root field as queries.

mutation {
  u4f1c2d3e_shop {
    insert_books(objects: [{ title: "Dune", year: 1965 }, { title: "Ubik", year: 1969 }]) {
      affected_rows
      returning { id title }
    }
    update_books(where: { year: { _lt: 1900 } }, _set: { classic: true }) {
      affected_rows
    }
  }
}

Joins

Foreign keys are not turned into nested fields: each table is queried on its own. To read related rows in one request, ask for both tables in one query, or create a view that joins them and sync; the view is then a field like any table.

-- in the SQL editor, then Sync tables
CREATE VIEW order_lines AS
  SELECT o.id AS order_id, o.created_at, b.title, l.qty
  FROM orders o JOIN lines l ON l.order_id = o.id JOIN books b ON b.id = l.book_id;

Schema and tools

The endpoint answers introspection, so GraphiQL, Apollo, Insomnia, Postman and GraphQL Code Generator can read the schema with your key in the Authorization header. A read key sees the query fields only.

Limits and errors

GraphQL takes 100 requests a minute from one IP address, and each request counts against your plan's monthly calls. There are no subscriptions: the endpoint is HTTP POST only.

A refused key or database answers as the REST API does, {"error": "…"} with 401, 403 or 404. Errors in the query itself come from Hasura, as {"errors": [{"message": "…", "extensions": {"code": "…"}}]}.