GraphQL
A Relay-style GraphQL API, generated from your database schema. It runs on the same connection, keys, and Row-Level Security as the REST API — a GraphQL reader sees exactly what the same principal's REST read would.
One endpoint, POST only, with the usual GraphQL JSON body.
Every request carries Authorization: Bearer <key-or-token>,
the same credential you use for REST. A well-formed request always answers
200 with a { "data": …, "errors": [ … ] } body;
only a body that is not valid JSON gets a 400.
curl -X POST "https://<ref>.kethosbase.com/graphql/v1" \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{"query": "{ todosCollection(first: 20) { edges { node { id task } } } }"}'
The schema
The schema is projected from your public tables — no schema
file to write or keep in sync. Table and column names are used verbatim.
Introspection (__schema, __type) is filtered by the
caller's privileges, so it exposes only the tables and columns that principal
may read.
Querying: collections and connections
Each table gets one query field named <table>Collection
that returns a Relay connection. Select edges
(each with a cursor and a node),
pageInfo, and totalCount:
{
todosCollection(
filter: { done: { eq: false } }
orderBy: [{ id: DescNullsLast }]
first: 20
) {
edges {
cursor
node { id task done }
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
Arguments
| Argument | Meaning |
|---|---|
filter | A tree of column conditions — see operators below. |
orderBy | List of { column: Direction }. Directions: Asc / AscNullsFirst / AscNullsLast and the Desc… equivalents. |
first / after | Forward pagination — the first n rows after a cursor. |
last / before | Backward pagination. first and last together is an error. |
offset | Skip rows from the start. |
A page is capped at 1000 rows and defaults to 100. Cursors are opaque;
pass an endCursor straight back as after to page on.
Filter operators
The same operator set as REST: eq, neq,
gt, gte, lt, lte,
like, ilike, match,
imatch, isdistinct, in (a list), and
is (NULL / NOT_NULL). Combine
conditions with and, or (lists), and
not (an object), nested freely:
{
todosCollection(filter: {
or: [
{ done: { is: NOT_NULL } }
{ priority: { gte: 5 } }
]
}) { edges { node { id } } }
}
Related tables
A foreign key becomes a nested field: a to-one relationship embeds an object, a to-many relationship embeds a nested connection. Nested reads run under the same RLS as the rest of the query.
{
booksCollection(orderBy: [{ title: AscNullsLast }]) {
edges {
node {
title
author { name } # to-one → object
reviewsCollection { # to-many → connection
edges { node { rating } }
}
}
}
}
}
filter, orderBy, pagination) are not supported yet
and are rejected with a clear error — nested connections accept only
edges { node { … } }. Filter, order, and pagination on the
top-level …Collection field work as documented above.Mutations
Each table gets insert, update, and delete mutations. They return the
affected records (scalar columns) and an
affectedCount. Update and delete require a
filter — there is no accidental full-table write — and
accept an optional atMost that rolls the whole mutation back if
more rows than that would change.
# insert
mutation {
insertIntoTodosCollection(objects: [{ task: "ship it" }]) {
records { id task }
affectedCount
}
}
# update (filter required; atMost guards the blast radius)
mutation {
updateTodosCollection(
set: { done: true }
filter: { id: { eq: 1 } }
atMost: 1
) { affectedCount }
}
# delete (filter required)
mutation {
deleteFromTodosCollection(filter: { done: { is: NOT_NULL } }) {
affectedCount
}
}
Errors and safety
Field-level failures come back in errors with a
path; a table the caller cannot read resolves to
null with a field-scoped error rather than leaking its
existence. As on REST, only the secret key
(service_role) sees raw database messages — anonymous and
authenticated callers get generic text. Every request runs in one
transaction with a 10-second statement timeout.
Guard rails bound query cost: selection depth 16, filter nesting 12, up to 2000 fields and 50 related-table joins per query.
Not supported yet
- Subscriptions — use Realtime for live updates.
- Fragments, inline fragments, and
@directives— rejected by the parser with a clear message. - Arguments on nested relationships — see the note above.
- Selecting related tables inside a mutation's
records— scalar columns only there.
See Limits & errors for status codes and quotas, and Row-Level Security for how access is decided.