Skip to content

Query API

Each running ReflexDB instance exposes a single query endpoint. Queries are plain-text selections sent as a POST body.

POST https://<instance-id>.reflexdb.cloud/query

Pass your API key as a Bearer token:

Authorization: Bearer rxk_<keyId>.<hmac>

API keys are created in the dashboard (Database → API Keys) or via the control plane API.


The request body is plain text (not JSON). The Content-Type header is not required.

<table> { <fields> }
users { id name email }

Use * to select all scalar fields, or ... to select all scalars plus one level of relations:

users { * }
users { ... }

Wildcards can be combined with explicit fields — explicit fields override wildcard-generated ones:

users { * posts { id title } }

Filter by any column using a SQL-style predicate in parentheses:

users(plan = "pro") { id name email }

Supported operators: =, !=, <, <=, >, >=, LIKE, ILIKE, IN (...), IS NULL, IS NOT NULL, BETWEEN x AND y.

Boolean literals true and false are supported:

posts(published = true) { id title }

Multiple conditions can be combined with AND and OR. Parentheses override precedence:

posts(published = true AND user_id = 42) { id title }
users(plan = "pro" OR (plan = "free" AND verified = true)) { id name }

Filter parent rows by conditions on related tables using dot notation:

users(posts.published = true) { id name }

Dot chains can traverse multiple levels:

users(posts.comments.approved = true) { id name }

For to-many relations, ANY semantics apply — the parent matches if at least one related row satisfies the predicate.

Embed related rows in a single request by selecting fields from a relation:

posts { id title author { id name } comments { id body } }

Forward relations (FK on this table) return an object; reverse relations return an array.

[
{
"id": 1,
"title": "Hello world",
"author": { "id": 42, "name": "Alice" },
"comments": [
{ "id": 100, "body": "Great post!" }
]
}
]

Relations can be nested to any depth and filtered independently:

users { id posts(published = true) { id title } }

Add an ORDER BY clause after the closing brace:

posts { id title } ORDER BY created_at DESC

Sort order is ASC (default) or DESC. Multiple sort fields are separated by commas:

posts { id title } ORDER BY published_at DESC, id ASC

Nested relations support their own ORDER BY:

users { id posts { id title } ORDER BY title ASC }

Add LIMIT and/or OFFSET after the closing brace. Clauses must appear in this order: ORDER BY → LIMIT → OFFSET.

users { id name } ORDER BY id LIMIT 20 OFFSET 40
ClauseDefaultNotes
LIMIT1000Prevents accidental full-table dumps
OFFSET0

Nested relations also support LIMIT and OFFSET:

users { id posts { id title } ORDER BY title ASC LIMIT 5 }

ReflexDB supports COUNT, SUM, and AVG aggregation functions. Aggregates are specified inline in the field list.

FunctionOutput keyNotes
COUNT(*)countCount of all rows
COUNT(field)countCount of non-NULL values
SUM(field)sum_<field>Sum of a numeric column
AVG(field)avg_<field>Average of a numeric column
orders { COUNT(*) }
[{"count": 1482}]

Any non-aggregate field in the selection is an implicit GROUP BY key:

orders { status COUNT(*) }
[
{"status": "pending", "count": 42},
{"status": "shipped", "count": 1440}
]
orders { SUM(total_cents) AVG(total_cents) }
[{"sum_total_cents": 9840200, "avg_total_cents": 6640}]
orders(status = "shipped") { user_id SUM(total_cents) } ORDER BY total_cents DESC LIMIT 10

Aggregate functions also work inside embedded relations — useful for counting child rows per parent:

users { id name posts { COUNT(*) } }
[
{"id": 1, "name": "Alice", "posts": [{"count": 12}]},
{"id": 2, "name": "Bob", "posts": [{"count": 3}]}
]

A successful response is a JSON envelope with data and meta keys:

{
"data": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
],
"meta": {
"table": "users",
"query": {
"fields": ["id", "name", "email"]
},
"pagination": {
"count": 2,
"total_matched": 2,
"has_more": false,
"limit": 1000
},
"timing_ms": 0.42
}
}
  • data — array of matching rows
  • meta.table — the queried table name
  • meta.query — echo of the parsed query parameters (fields, aggregates, filter, order_by, limit, offset — only present when specified)
  • meta.pagination.count — number of rows in this page
  • meta.pagination.total_matched — total rows matching the filter before limit/offset
  • meta.pagination.has_moretrue if more rows exist beyond this page
  • meta.timing_ms — server-side query execution time in milliseconds

ReflexDB maps source database columns to JSON types as follows:

SQL typeJSON typeExample
INT, BIGINT, SMALLINT, TINYINTnumber42
FLOAT, DOUBLE, REALnumber3.14
DECIMAL, NUMERICnumber99.99
BOOLEAN, BIT(1), TINYINT(1)booleantrue
CHAR, VARCHAR, TEXTstring"hello"
JSON, JSONBstring"{\"key\":\"value\"}"
UUID, INET, MACADDRstring"550e8400-..."
ENUMstring"active"
DATEstring"2024-06-15"
DATETIME, TIMESTAMP, TIMESTAMPTZstring"2024-06-15T12:30:00Z"
TIME, TIMETZnumber (seconds)45000
INTERVALstring"1 year 2 months"
BINARY, VARBINARY, BLOB, BYTEAstring (hex)"deadbeef"
Nullable columnsnull when NULLnull

Each instance also serves its own OpenAPI spec describing the exact tables and fields in its compiled schema:

GET https://<instance-id>.reflexdb.cloud/openapi.json

This is also proxied through the control plane at:

GET https://api.reflexdb.cloud/v1/databases/<id>/openapi.json

All errors return a JSON envelope with a nested error object:

HTTPerror.codeMeaning
400parse_errorMalformed query syntax
400executor_errorValid syntax but unknown field or invalid query structure
404unknown_tableTable name not found in the schema
401unauthorizedMissing or invalid API key
503Instance is starting up or unhealthy
{ "error": { "code": "unknown_table", "message": "unknown table: 'foobar'" } }