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": "…"}}]}.