Praxsuite

PraxQL Overview

Vincent Depassier · August 29, 2026

PraxQL — Overview

PraxQL is Praxsuite's JSON-based declarative query language. It is what you use to read and write data through the API Gateway. Instead of configuring one REST endpoint for every data access pattern, you send a single structured JSON body that describes exactly what you want.


Why PraxQL exists

Praxsuite tables are created by users at runtime, not defined by a developer at deploy time. A workspace's shape changes whenever somebody adds a table or renames a column, and it differs from every other workspace.

That rules out the usual off-the-shelf options. GraphQL wants a schema fixed at deploy time. Auto-generated REST layers assume a database structure that is stable and safe to expose. OData cannot describe a schema that differs per tenant and changes while the service is running.

PraxQL was built for that situation: one endpoint, one request format, and a schema you discover at runtime rather than one you compile against.


The request structure

Every PraxQL call is a POST to one endpoint:

POST https://gateway.praxsuite.com/{workspaceId}/query
Authorization: Bearer sk_live_xxxxx
Content-Type: application/json

That short form is the one to use. The gateway also answers the longer /api/v1/gateway/{workspaceId}/query path, which is what you will see in older examples — same endpoint, same body.

The request body always has two parts:

{
  "refs": {
    "Alias": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "query": { ... }
}

Or, for write operations:

{
  "refs": {
    "Alias": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  "mutation": { ... }
}

query and mutation are mutually exclusive — send one or the other.


refs — the table reference dictionary

The refs object maps your chosen aliases to table GUIDs. It exists because table names are not unique: a workspace can have several tables called "Customers" at different points in time, so a name is not enough to identify one.

{
  "refs": {
    "Customers": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "Orders":    "b2c3d4e5-f6a7-8901-bcde-f12345678901"
  }
}

Rules:

  • Keys are any string you choose — they become your alias in the query / mutation body.

  • Values are the table GUIDs. Get them from the Playground's schema panel or from GET /schema.

  • Every GUID must belong to the authenticated workspace. A reference to another workspace's table is refused.

  • Up to 20 refs per request.

refs is also the complete list of what a request is allowed to touch, which is what lets the gateway check a request against your permissions before doing any work.


query vs mutation

Key

Operations

When to use

query

Read data

Fetching records, filtering, aggregating, joining

mutation

Write data

Inserting rows, updating fields, deleting records


Optional: includeTotalCount

By default the response carries no total count of all matching rows — it is skipped because it costs a second pass over the data. To ask for it:

{
  "refs": { ... },
  "query": { ... },
  "includeTotalCount": true
}

Only ask for it when you are drawing pagination controls that show a total; a plain "next page" button does not need it.


The response shape

{
  "data": [
    { "Name": "Acme Corp", "Email": "hello@acme.com" },
    { "Name": "Beta Inc",  "Email": "info@beta.io" }
  ],
  "meta": {
    "count": 2,
    "limit": 50,
    "offset": 0,
    "total": null,
    "durationMs": 4
  }
}

Field

Meaning

data

Array of row objects. Keys are logical column names.

meta.count

Number of rows in this response

meta.limit

The effective limit applied

meta.offset

The offset applied

meta.total

Total matching rows — only set when includeTotalCount: true

meta.durationMs

Time spent executing, excluding authentication

meta.limit is worth reading rather than assuming: the effective limit can be lower than what you asked for.


Error responses

Every error returns a structured body:

{
  "error": {
    "code": "INVALID_QUERY",
    "message": "Column 'Emaill' not found in table 'Customers'.",
    "details": ["Available columns: Name, Email, Phone, Status"]
  }
}

HTTP

Code

Cause

400

INVALID_QUERY

Malformed query, unknown column, invalid operator

400

INVALID_REFS

Unknown table GUID, a GUID from another workspace, empty refs

401

UNAUTHORIZED

Invalid or missing API key

403

SCOPE_VIOLATION

Table or column not in scope, aggregation not allowed

403

FORBIDDEN

Key revoked or expired, or the principal is suspended

408

QUERY_TIMEOUT

Query exceeded the 30-second timeout

429

RATE_LIMITED

Key exceeded its request quota

500

EXECUTION_ERROR

Internal error, details recorded server-side

SCOPE_VIOLATION is worth understanding: a table your key has no permission for answers 403, not 404, and the message does not confirm whether the table exists. That is deliberate — a 404 would let a caller map your workspace by probing.


What happens to a request

Every call is authenticated, checked against the permissions of the credential that sent it, translated from the logical names you used into whatever the storage layer needs, executed, and then filtered again on the way out so that masking rules apply to the results.

The last part matters for how you design queries: permissions are enforced on the way in and on the way out. A column you may filter on but not read still participates in the where clause while never appearing in the response.

Every request is also recorded in the audit log — including the ones that failed.